diff --git a/dev/dev-agent.sh b/dev/dev-agent.sh index 720c785..93acf17 100755 --- a/dev/dev-agent.sh +++ b/dev/dev-agent.sh @@ -30,6 +30,7 @@ source "$(dirname "$0")/../lib/worktree.sh" source "$(dirname "$0")/../lib/pr-lifecycle.sh" source "$(dirname "$0")/../lib/mirrors.sh" source "$(dirname "$0")/../lib/agent-sdk.sh" +source "$(dirname "$0")/../lib/formula-session.sh" # Auto-pull factory code to pick up merged fixes before any logic runs git -C "$FACTORY_ROOT" pull --ff-only origin main 2>/dev/null || true @@ -306,6 +307,10 @@ OPEN_ISSUES_SUMMARY=$(forge_api GET "/issues?state=open&labels=backlog&limit=20& PUSH_INSTRUCTIONS=$(build_phase_protocol_prompt "$BRANCH" "$FORGE_REMOTE") +# Load lessons from .profile repo if available (pre-session) +profile_load_lessons || true +LESSONS_INJECTION="${LESSONS_CONTEXT:-}" + if [ "$RECOVERY_MODE" = true ]; then GIT_DIFF_STAT=$(git -C "$WORKTREE" diff "${FORGE_REMOTE}/${PRIMARY_BRANCH}..HEAD" --stat 2>/dev/null \ | head -20 || echo "(no diff)") @@ -336,6 +341,10 @@ ${GIT_DIFF_STAT} 3. Address any pending review comments or CI failures. 4. Commit and push to \`${BRANCH}\`. +${LESSONS_INJECTION:+## Lessons learned +${LESSONS_INJECTION} + +} ${PUSH_INSTRUCTIONS}" else INITIAL_PROMPT="You are working in a git worktree at ${WORKTREE} on branch ${BRANCH}. @@ -351,6 +360,10 @@ ${OPEN_ISSUES_SUMMARY} $(if [ -n "$PRIOR_ART_DIFF" ]; then printf '## Prior Art (closed PR — DO NOT start from scratch)\n\nA previous PR attempted this issue but was closed without merging. Reuse as much as possible.\n\n```diff\n%s\n```\n' "$PRIOR_ART_DIFF" fi) +${LESSONS_INJECTION:+## Lessons learned +${LESSONS_INJECTION} + +} ## Instructions 1. Read AGENTS.md in this repo for project context and coding conventions. @@ -535,6 +548,12 @@ if [ "$rc" -eq 0 ]; then log "PR #${PR_NUMBER} merged" issue_close "$ISSUE" + # Capture files changed for journal entry (after agent work) + FILES_CHANGED=$(git -C "$WORKTREE" diff "${FORGE_REMOTE}/${PRIMARY_BRANCH}..HEAD" --name-only 2>/dev/null | tr '\n' ',' | sed 's/,$//') || FILES_CHANGED="" + + # Write journal entry post-session (before cleanup) + profile_write_journal "$ISSUE" "$ISSUE_TITLE" "merged" "$FILES_CHANGED" || true + # Pull primary branch and push to mirrors git -C "$REPO_ROOT" fetch "$FORGE_REMOTE" "$PRIMARY_BRANCH" 2>/dev/null || true git -C "$REPO_ROOT" checkout "$PRIMARY_BRANCH" 2>/dev/null || true @@ -548,6 +567,14 @@ else # Exhausted or unrecoverable failure log "PR walk failed: ${_PR_WALK_EXIT_REASON:-unknown}" issue_block "$ISSUE" "${_PR_WALK_EXIT_REASON:-agent_failed}" + + # Capture files changed for journal entry (after agent work) + FILES_CHANGED=$(git -C "$WORKTREE" diff "${FORGE_REMOTE}/${PRIMARY_BRANCH}..HEAD" --name-only 2>/dev/null | tr '\n' ',' | sed 's/,$//') || FILES_CHANGED="" + + # Write journal entry post-session (before cleanup) + outcome="blocked_${_PR_WALK_EXIT_REASON:-agent_failed}" + profile_write_journal "$ISSUE" "$ISSUE_TITLE" "$outcome" "$FILES_CHANGED" || true + CLAIMED=false fi diff --git a/formulas/run-planner.toml b/formulas/run-planner.toml index 2620841..d730b51 100644 --- a/formulas/run-planner.toml +++ b/formulas/run-planner.toml @@ -4,7 +4,7 @@ # planner-run.sh creates a tmux session with Claude (opus) and injects # this formula as context, plus the graph report from build-graph.py. # -# Steps: preflight → triage-and-plan → journal-and-commit +# Steps: preflight → triage-and-plan → commit-ops-changes # # v4 changes from v3: # - Graph report (orphans, cycles, thin objectives, bottlenecks) replaces @@ -13,7 +13,8 @@ # - 3 steps instead of 6. # # AGENTS.md maintenance is handled by the gardener (#246). -# All git writes (tree, journal, memory) happen in one commit at the end. +# All git writes (tree, memory) happen in one commit at the end. +# Journal writing is delegated to generic profile_write_journal() function. name = "run-planner" description = "Planner v4: graph-driven planning with tea helpers" @@ -241,50 +242,13 @@ CRITICAL: If any part of this step fails, log the failure and continue. needs = ["preflight"] [[steps]] -id = "journal-and-commit" -title = "Write tree, journal, optional memory; commit and PR" +id = "commit-ops-changes" +title = "Write tree, memory, and journal; commit and push" description = """ ### 1. Write prerequisite tree Write to: $OPS_REPO_ROOT/prerequisites.md -### 2. Write journal entry -Create/append to: $OPS_REPO_ROOT/journal/planner/$(date -u +%Y-%m-%d).md - -Format: - # Planner run — YYYY-MM-DD HH:MM UTC - - ## Predictions triaged - - #NNN: ACTION — reasoning (or "No unreviewed predictions") - - ## Prerequisite tree updates - - Resolved: - Discovered: - Proposed: - - ## Top 5 constraints - 1. — blocks N objectives — #NNN (existing|filed) - - ## Stuck issues detected - - #NNN: vision-labeled (complexity test failed) — blocked on #NNN - (or "No stuck issues detected") - - ## Vault items filed - - $OPS_REPO_ROOT/vault/pending/.md — — blocks #NNN - (or "No vault items filed") - - ## Issues created - - #NNN: title — why (or "No new issues") - - ## Priority label changes - - Added/removed priority: #NNN (or "No priority changes") - - ## Observations - - Key patterns noticed this run - - ## Deferred - - Items in tree beyond top 5, why not filed - -Keep concise — 30-50 lines max. - -### 3. Memory update (every 5th run) +### 2. Memory update (every 5th run) Count "# Planner run —" headers across all journal files. Check "" in planner-memory.md. If (count - N) >= 5 or planner-memory.md missing, write to: @@ -292,15 +256,19 @@ If (count - N) >= 5 or planner-memory.md missing, write to: Include: run counter marker, date, constraint focus, patterns, direction. Keep under 100 lines. Replace entire file. -### 4. Commit ops repo changes -Commit the ops repo changes (prerequisites, journal, memory, vault items): +### 3. Commit ops repo changes +Commit the ops repo changes (prerequisites, memory, vault items): cd "$OPS_REPO_ROOT" - git add prerequisites.md journal/planner/ knowledge/planner-memory.md vault/pending/ + git add prerequisites.md knowledge/planner-memory.md vault/pending/ git add -u if ! git diff --cached --quiet; then git commit -m "chore: planner run $(date -u +%Y-%m-%d)" git push origin "$PRIMARY_BRANCH" fi cd "$PROJECT_REPO_ROOT" + +### 4. Write journal entry (generic) +The planner-run.sh wrapper will handle journal writing via profile_write_journal() +after the formula completes. This step is informational only. """ needs = ["triage-and-plan"] diff --git a/lib/formula-session.sh b/lib/formula-session.sh index 82696f6..1675ea5 100644 --- a/lib/formula-session.sh +++ b/lib/formula-session.sh @@ -129,6 +129,317 @@ ensure_profile_repo() { return 0 } +# _profile_has_repo +# Checks if the agent has a .profile repo by querying Forgejo API. +# Returns 0 if repo exists, 1 otherwise. +_profile_has_repo() { + local agent_identity="${1:-${AGENT_IDENTITY:-}}" + + if [ -z "$agent_identity" ]; then + if ! resolve_agent_identity; then + return 1 + fi + agent_identity="$AGENT_IDENTITY" + fi + + local forge_url="${FORGE_URL:-http://localhost:3000}" + local api_url="${forge_url}/api/v1/repos/${agent_identity}/.profile" + + # Check if repo exists via API (returns 200 if exists, 404 if not) + if curl -sf -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "$api_url" >/dev/null 2>&1; then + return 0 + fi + return 1 +} + +# _count_undigested_journals +# Counts journal entries in .profile/journal/ excluding archive/ +# Returns count via stdout. +_count_undigested_journals() { + if [ ! -d "${PROFILE_REPO_PATH:-}/journal" ]; then + echo "0" + return + fi + find "${PROFILE_REPO_PATH}/journal" -maxdepth 1 -name "*.md" -type f ! -path "*/archive/*" 2>/dev/null | wc -l +} + +# _profile_digest_journals +# Runs a claude -p one-shot to digest undigested journals into lessons-learned.md +# Returns 0 on success, 1 on failure. +_profile_digest_journals() { + local agent_identity="${1:-${AGENT_IDENTITY:-}}" + local model="${2:-${CLAUDE_MODEL:-opus}}" + + if [ -z "$agent_identity" ]; then + if ! resolve_agent_identity; then + return 1 + fi + agent_identity="$AGENT_IDENTITY" + fi + + local journal_dir="${PROFILE_REPO_PATH}/journal" + local knowledge_dir="${PROFILE_REPO_PATH}/knowledge" + local lessons_file="${knowledge_dir}/lessons-learned.md" + + # Collect undigested journal entries + local journal_entries="" + if [ -d "$journal_dir" ]; then + for jf in "$journal_dir"/*.md; do + [ -f "$jf" ] || continue + # Skip archived entries + [[ "$jf" == */archive/* ]] && continue + local basename + basename=$(basename "$jf") + journal_entries="${journal_entries} +### ${basename} +$(cat "$jf") +" + done + fi + + if [ -z "$journal_entries" ]; then + log "profile: no undigested journals to digest" + return 0 + fi + + # Read existing lessons if available + local existing_lessons="" + if [ -f "$lessons_file" ]; then + existing_lessons=$(cat "$lessons_file") + fi + + # Build prompt for digestion + local digest_prompt="You are digesting journal entries from a developer agent's work sessions. + +## Task +Condense these journal entries into abstract, transferable lessons. Rewrite lessons-learned.md entirely. + +## Constraints +- Hard cap: 2KB maximum +- Abstract: patterns and heuristics, not specific issues or file paths +- Transferable: must help with future unseen work, not just recall past work +- Drop the least transferable lessons if over limit + +## Existing lessons-learned.md (if any) +${existing_lessons:-} + +## Journal entries to digest +${journal_entries} + +## Output +Write the complete, rewritten lessons-learned.md content below. No preamble, no explanation — just the file content." + + # Run claude -p one-shot with same model as agent + local output + output=$(claude -p "$digest_prompt" \ + --output-format json \ + --dangerously-skip-permissions \ + --max-tokens 1000 \ + ${model:+--model "$model"} \ + 2>>"$LOGFILE" || echo '{"result":"error"}') + + # Extract content from JSON response + local lessons_content + lessons_content=$(printf '%s' "$output" | jq -r '.result // empty' 2>/dev/null || echo "") + + if [ -z "$lessons_content" ]; then + log "profile: failed to digest journals" + return 1 + fi + + # Ensure knowledge directory exists + mkdir -p "$knowledge_dir" + + # Write the lessons file (full rewrite) + printf '%s\n' "$lessons_content" > "$lessons_file" + log "profile: wrote lessons-learned.md (${#lessons_content} bytes)" + + # Move digested journals to archive (if any were processed) + if [ -d "$journal_dir" ]; then + mkdir -p "${journal_dir}/archive" + local archived=0 + for jf in "$journal_dir"/*.md; do + [ -f "$jf" ] || continue + [[ "$jf" == */archive/* ]] && continue + local basename + basename=$(basename "$jf") + mv "$jf" "${journal_dir}/archive/${basename}" 2>/dev/null && archived=$((archived + 1)) + done + if [ "$archived" -gt 0 ]; then + log "profile: archived ${archived} journal entries" + fi + fi + + return 0 +} + +# _profile_commit_and_push MESSAGE [FILE ...] +# Commits and pushes changes to .profile repo. +_profile_commit_and_push() { + local msg="$1" + shift + local files=("$@") + + if [ ! -d "${PROFILE_REPO_PATH:-}/.git" ]; then + return 1 + fi + + ( + cd "$PROFILE_REPO_PATH" || return 1 + + if [ ${#files[@]} -gt 0 ]; then + git add "${files[@]}" + else + git add -A + fi + + if ! git diff --cached --quiet 2>/dev/null; then + git config user.name "${AGENT_IDENTITY}" || true + git config user.email "${AGENT_IDENTITY}@users.noreply.codeberg.org" || true + git commit -m "$msg" --no-verify 2>/dev/null || true + git push origin main --quiet 2>/dev/null || git push origin master --quiet 2>/dev/null || true + fi + ) +} + +# profile_load_lessons +# Pre-session: loads lessons-learned.md into LESSONS_CONTEXT for prompt injection. +# Lazy digestion: if >10 undigested journals exist, runs claude -p to digest them. +# Returns 0 on success, 1 if agent has no .profile repo (silent no-op). +# Requires: ensure_profile_repo() called, AGENT_IDENTITY, FORGE_TOKEN, FORGE_URL, CLAUDE_MODEL. +# Exports: LESSONS_CONTEXT (the lessons file content, hard-capped at 2KB). +profile_load_lessons() { + # Check if agent has .profile repo + if ! _profile_has_repo; then + return 0 # Silent no-op + fi + + # Pull .profile repo + if ! ensure_profile_repo; then + return 0 # Silent no-op + fi + + # Check journal count for lazy digestion trigger + local journal_count + journal_count=$(_count_undigested_journals) + + if [ "${journal_count:-0}" -gt 10 ]; then + log "profile: digesting ${journal_count} undigested journals" + if ! _profile_digest_journals; then + log "profile: warning — journal digestion failed" + fi + fi + + # Read lessons-learned.md (hard cap at 2KB) + local lessons_file="${PROFILE_REPO_PATH}/knowledge/lessons-learned.md" + LESSONS_CONTEXT="" + + if [ -f "$lessons_file" ]; then + local lessons_content + lessons_content=$(head -c 2048 "$lessons_file" 2>/dev/null) || lessons_content="" + if [ -n "$lessons_content" ]; then + # shellcheck disable=SC2034 # exported to caller for prompt injection + LESSONS_CONTEXT="## Lessons learned (from .profile/knowledge/lessons-learned.md) +${lessons_content}" + log "profile: loaded lessons-learned.md (${#lessons_content} bytes)" + fi + fi + + return 0 +} + +# profile_write_journal ISSUE_NUM ISSUE_TITLE OUTCOME [FILES_CHANGED] +# Post-session: writes a reflection journal entry after work completes. +# Returns 0 on success, 1 on failure. +# Requires: AGENT_IDENTITY, FORGE_TOKEN, FORGE_URL, CLAUDE_MODEL. +# Args: +# $1 - ISSUE_NUM: The issue number worked on +# $2 - ISSUE_TITLE: The issue title +# $3 - OUTCOME: Session outcome (merged, blocked, failed, etc.) +# $4 - FILES_CHANGED: Optional comma-separated list of files changed +profile_write_journal() { + local issue_num="$1" + local issue_title="$2" + local outcome="$3" + local files_changed="${4:-}" + + # Check if agent has .profile repo + if ! _profile_has_repo; then + return 0 # Silent no-op + fi + + # Pull .profile repo + if ! ensure_profile_repo; then + return 0 # Silent no-op + fi + + # Build session summary + local session_summary="" + if [ -n "$files_changed" ]; then + session_summary="Files changed: ${files_changed} +" + fi + session_summary="${session_summary}Outcome: ${outcome}" + + # Build reflection prompt + local reflection_prompt="You are reflecting on a development session. Write a concise journal entry about transferable lessons learned. + +## Session context +- Issue: #${issue_num} — ${issue_title} +- Outcome: ${outcome} + +${session_summary} + +## Task +Write a journal entry focused on what you learned that would help you do similar work better next time. + +## Constraints +- Be concise (100-200 words) +- Focus on transferable lessons, not a summary of what you did +- Abstract patterns and heuristics, not specific issue/file references +- One concise entry, not a list + +## Output +Write the journal entry below. Use markdown format." + + # Run claude -p one-shot with same model as agent + local output + output=$(claude -p "$reflection_prompt" \ + --output-format json \ + --dangerously-skip-permissions \ + --max-tokens 500 \ + ${CLAUDE_MODEL:+--model "$CLAUDE_MODEL"} \ + 2>>"$LOGFILE" || echo '{"result":"error"}') + + # Extract content from JSON response + local journal_content + journal_content=$(printf '%s' "$output" | jq -r '.result // empty' 2>/dev/null || echo "") + + if [ -z "$journal_content" ]; then + log "profile: failed to write journal entry" + return 1 + fi + + # Ensure journal directory exists + local journal_dir="${PROFILE_REPO_PATH}/journal" + mkdir -p "$journal_dir" + + # Write journal entry (append if exists) + local journal_file="${journal_dir}/issue-${issue_num}.md" + if [ -f "$journal_file" ]; then + printf '\n---\n\n' >> "$journal_file" + fi + printf '%s\n' "$journal_content" >> "$journal_file" + log "profile: wrote journal entry for issue #${issue_num}" + + # Commit and push to .profile repo + _profile_commit_and_push "journal: issue #${issue_num} reflection" "journal/issue-${issue_num}.md" + + return 0 +} + # ── Formula loading ────────────────────────────────────────────────────── # load_formula FORMULA_FILE diff --git a/planner/planner-run.sh b/planner/planner-run.sh index 313f6ef..31f5588 100755 --- a/planner/planner-run.sh +++ b/planner/planner-run.sh @@ -45,6 +45,12 @@ WORKTREE="/tmp/${PROJECT_NAME}-planner-run" log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%S)Z] $*" >> "$LOG_FILE"; } +# Ensure AGENT_IDENTITY is set for profile functions +if [ -z "${AGENT_IDENTITY:-}" ] && [ -n "${FORGE_PLANNER_TOKEN:-}" ]; then + AGENT_IDENTITY=$(curl -sf -H "Authorization: token ${FORGE_PLANNER_TOKEN}" \ + "${FORGE_URL:-http://localhost:3000}/api/v1/user" 2>/dev/null | jq -r '.login // empty' 2>/dev/null || true) +fi + # ── Guards ──────────────────────────────────────────────────────────────── check_active planner acquire_cron_lock "/tmp/planner-run.lock" @@ -72,24 +78,9 @@ $(cat "$MEMORY_FILE") " fi -# ── Read recent journal files ────────────────────────────────────────── -JOURNAL_BLOCK="" -JOURNAL_DIR="$OPS_REPO_ROOT/journal/planner" -if [ -d "$JOURNAL_DIR" ]; then - # Load last 5 journal files (most recent first) for run history context - JOURNAL_FILES=$(find "$JOURNAL_DIR" -name '*.md' -type f | sort -r | head -5) - if [ -n "$JOURNAL_FILES" ]; then - JOURNAL_BLOCK=" -### Recent journal entries (journal/planner/) -" - while IFS= read -r jf; do - JOURNAL_BLOCK="${JOURNAL_BLOCK} -#### $(basename "$jf") -$(cat "$jf") -" - done <<< "$JOURNAL_FILES" - fi -fi +# ── Load lessons from .profile repo (pre-session) ──────────────────────── +profile_load_lessons || true +LESSONS_INJECTION="${LESSONS_CONTEXT:-}" # ── Read scratch file (compaction survival) ─────────────────────────────── SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE") @@ -105,7 +96,11 @@ build_sdk_prompt_footer " PROMPT="You are the strategic planner for ${FORGE_REPO}. Work through the formula below. ## Project context -${CONTEXT_BLOCK}${MEMORY_BLOCK}${JOURNAL_BLOCK} +${CONTEXT_BLOCK}${MEMORY_BLOCK} +${LESSONS_INJECTION:+## Lessons learned +${LESSONS_INJECTION} + +} ${GRAPH_SECTION} ${SCRATCH_CONTEXT:+${SCRATCH_CONTEXT} } @@ -125,5 +120,8 @@ export CLAUDE_MODEL="opus" agent_run --worktree "$WORKTREE" "$PROMPT" log "agent_run complete" +# Write journal entry post-session +profile_write_journal "planner-run" "Planner run $(date -u +%Y-%m-%d)" "complete" "" || true + rm -f "$SCRATCH_FILE" log "--- Planner run done ---"