diff --git a/.agents/skills/stuck-crewmate-recovery/SKILL.md b/.agents/skills/stuck-crewmate-recovery/SKILL.md index d97ebee302..ea7f7405ea 100644 --- a/.agents/skills/stuck-crewmate-recovery/SKILL.md +++ b/.agents/skills/stuck-crewmate-recovery/SKILL.md @@ -30,7 +30,10 @@ Use `treehouse status` for treehouse-backed tmux, herdr, zellij, or cmux tasks, Do not sweep another home's endpoints or infer ownership from a matching window label. Before relaunch, prove that no live agent still owns the recorded task and that the existing worktree remains available. -Preserve its uncommitted changes and commits, keep the same task identity, and resume or relaunch the recorded harness in that existing worktree with the same brief plus a concise progress note. +Preserve its uncommitted changes and commits, keep the same task identity, and resume or relaunch the recorded harness in that existing worktree with the same brief. +The relaunched worker resumes from its own working log at `data//log.md`, which the brief already points it at, so never delete or rewrite that log and never reconstruct the worker's understanding by hand when the log holds it. +Read the log yourself before relaunching, and append a short progress note to the brief only for what the log cannot hold: a steer sent after the last log entry, or a correction the worker never recorded. +An absent or clearly stale log means the worker was not maintaining it; reconstruct the minimum needed to restart, and steer the relaunched worker to keep the log from then on. Do not use a fresh generic spawn while the recorded worktree is unaccounted for, because allocating another worktree can split one task across two copies. If the worktree or ownership cannot be reconciled safely, leave all state intact and report the task failed or blocked with the conflicting evidence. @@ -42,7 +45,7 @@ Escalate in order: 2. If the crewmate is waiting on a question its brief already answers, answer in one line via `FM_HOME= bin/fm-send.sh` from an active firstmate session unless `FM_HOME` is already set to the active firstmate home. 3. If the crewmate is confused or looping, interrupt with the adapter's interrupt key, then redirect with one corrective line. For example, for a single-Escape adapter: `FM_HOME= bin/fm-send.sh --key Escape`. -4. If the crewmate is genuinely wedged after redirection, exit the agent with the adapter's exit command and relaunch with the same brief plus a `progress so far` note appended to it. +4. If the crewmate is genuinely wedged after redirection, exit the agent with the adapter's exit command and relaunch with the same brief, letting it resume from its `data//log.md` under the reconciliation rules above. Genuine wedging means looping, unresponsive, repeating the same obstacle, or truly dead. A low context reading is not wedging; modern harnesses auto-compact and keep going. The worktree and commits persist, so relaunch is cheap. diff --git a/AGENTS.md b/AGENTS.md index ee23c840dc..3a6b15a57c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,6 +83,7 @@ data/ personal fleet records; LOCAL, gitignored as a whole secondmates.md secondmate routing table; firstmate-private, maintained by fm-home-seed.sh (section 6) /brief.md per-task crewmate brief, or per-secondmate charter brief when kind=secondmate /report.md scout task deliverable, written by the crewmate; survives teardown + /log.md crewmate working log, maintained by the crewmate as it works; its resume-from-disk memory, survives teardown, and never wakes firstmate projects/ cloned repos; gitignored; read-only except under hard rule 1's concrete captain-approved project operation exception state/ volatile runtime signals; gitignored .status appended by crewmates: ": " wake-event lines, not current-state truth @@ -454,6 +455,7 @@ Use its scaffold as the contract, then replace every `{TASK}` placeholder with a Keep additions task-specific rather than repeating lifecycle instructions, and alter generated sections only when the task genuinely differs from the standard shape. Every ship brief must retain the worktree-isolation assertion and stop if launched in the primary checkout. +Every ship and scout brief must retain the working-log section: the crewmate's own durable memory at `data//log.md`, written as it works, is what a relaunched worker resumes from instead of an understanding firstmate reconstructs by hand. If a ship task touches firstmate's shared tracked material, explicitly require `firstmate-coding-guidelines` before editing. If a task will drive Herdr lifecycle behavior, scaffold with `--herdr-lab`; if that need appears after an unguarded scaffold, stop and regenerate rather than adding commands by hand. The generated Herdr contract must use a named non-`default` isolated lab and its guarded helper for every lifecycle action. diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index 9c98723b01..4f795affca 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -39,6 +39,12 @@ # declared-external-wait verb (FM_CLASSIFY_PAUSED_VERB, default "paused") from # "blocked:": pause for a known external wait expected to clear on its own, # blocked when firstmate must act. +# Ship and scout briefs also scaffold an agent-maintained working log at +# data//log.md: the crewmate's own durable memory, written as it works, +# and the resume path when a relaunch discards its context. It survives teardown +# with the task's other data, and unlike state/.status it never wakes +# firstmate, so it carries the detail the sparse supervisor channel must not - +# established facts, rejected approaches, and steers received after dispatch. # Ship tasks include a project-memory section so durable project-intrinsic # learnings can be committed to AGENTS.md through the project's delivery path; # it carries the AGENTS.md authoring bar (widely useful knowledge only, pointers @@ -247,6 +253,22 @@ EOF HERDR_SECTION=${HERDR_SECTION%$'\n'} fi +# The crewmate's own working memory, shared by ship and scout briefs. It is +# deliberately NOT the status file: status appends wake firstmate and must stay +# sparse, while this log is written for the worker's own future self and can be +# as long as the task needs. A relaunched worker resumes from it instead of from +# an understanding firstmate reconstructs by hand. +IFS= read -r -d '' WORKLOG_SECTION < "$BRIEF" <> $STATUS_FILE\` @@ -371,11 +395,13 @@ You are in a disposable git worktree of $REPO, at a detached HEAD on a clean def The path check is authoritative: \`git rev-parse --git-dir\` and \`git rev-parse --git-common-dir\` can help inspect the repo, but they do not prove you are outside the primary checkout. If the top-level path is the primary checkout or not the worktree you were launched in, STOP - do not branch or commit here - append \`blocked: launched in primary checkout, not an isolated worktree\` to the status file and stop. -1. First action: create your branch: \`git checkout -b fm/$ID\`$SETUP2 +1. First action: create your branch: \`git checkout -b fm/$ID\` - if you are resuming and it already exists, check it out instead and read your working log below before doing anything else.$SETUP2 + +$WORKLOG_SECTION # Rules $RULE1 -2. Stay inside this worktree; modify nothing outside it. +2. Stay inside this worktree; the only files you may write outside it are your working log above and the status file below. 3. Use gh-axi for GitHub operations and chrome-devtools-axi for browser operations. 4. Report status by appending one line: \`echo "{state}: {one short line}" >> $STATUS_FILE\` diff --git a/docs/architecture.md b/docs/architecture.md index bf8b5cb3ec..b8d0c9cc82 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -140,6 +140,11 @@ The helper's header owns the exact signal detection, relocated-home limitation, Ship tasks change projects and ship by project mode (`no-mistakes`, `direct-PR`, or `local-only`); scout tasks leave standalone investigation reports at `data//report.md` and never push. The intake and authority contract in `AGENTS.md` owns when separate scout research is warranted. +Both shapes keep two durable channels with deliberately different wake semantics. +`state/.status` is the supervisor channel: sparse, and every append wakes firstmate. +`data//log.md` is the crewmate's own working log, maintained by the agent as it works and never a wake source, so it can carry the established facts, rejected approaches, and post-dispatch steers that a lost context would otherwise take with it. +It survives teardown with the task's other data, which is what lets a relaunched worker resume from disk rather than from a brief firstmate rebuilds by hand. + ## Dispatch profiles Crewmate and scout dispatch can stay on the static crewmate harness resolved by `config/crew-harness`, or it can use local dispatch profiles in `config/crew-dispatch.json`. diff --git a/tests/fm-brief.test.sh b/tests/fm-brief.test.sh index 0199311824..bf8f57437a 100755 --- a/tests/fm-brief.test.sh +++ b/tests/fm-brief.test.sh @@ -281,6 +281,71 @@ test_ship_project_memory_wording() { pass "fm-brief.sh: ship project-memory wording carries the AGENTS.md authoring bar" } +# The worker's own durable memory. Ship and scout briefs must scaffold it, it +# must be discoverable as the resume path, and it must stay distinct from the +# status file: status appends wake firstmate, log appends must not. The +# secondmate charter is a persistent home with its own data and gets no log. +test_ship_and_scout_briefs_scaffold_working_log() { + local home id brief kind + home="$TMP_ROOT/worklog-home" + mkdir -p "$home/data" + + for kind in ship scout; do + id="brief-worklog-$kind" + if [ "$kind" = scout ]; then + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" someproj --scout >/dev/null 2>&1 + else + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" "$id" someproj >/dev/null 2>&1 + fi + brief="$home/data/$id/brief.md" + assert_present "$brief" "$kind: brief was not scaffolded" + assert_grep "# Working log" "$brief" "$kind brief missing the working-log section" + assert_grep "$home/data/$id/log.md" "$brief" \ + "$kind brief did not bind the task's own log path" + assert_grep "write it as you go, not at the end" "$brief" \ + "$kind brief did not require continuous logging" + assert_grep "If you are resuming this task, read that log first" "$brief" \ + "$kind brief did not name the log as the resume path" + assert_grep "what you tried and rejected and why" "$brief" \ + "$kind brief did not require rejected approaches in the log" + assert_grep "append it to the log before you act on it" "$brief" \ + "$kind brief did not require steers to be recorded before acting" + assert_grep "appending to it never notifies firstmate" "$brief" \ + "$kind brief did not separate the log from the waking status channel" + # The log lives under data//, outside the worktree, so the + # stay-inside-the-worktree rule must permit it the way it permits the + # status file. Without this a careful worker cannot obey both rules. + assert_grep "the only files you may write outside it are" "$brief" \ + "$kind brief forbids writing the working log it requires" + assert_grep "your working log above" "$brief" \ + "$kind brief did not except the working log from the worktree-only rule" + assert_no_grep "WORKLOG" "$brief" \ + "$kind brief leaked the working-log heredoc delimiter" + assert_no_grep "EOF" "$brief" \ + "$kind brief leaked a heredoc EOF marker (unterminated heredoc)" + done + + FM_HOME="$home" FM_SECONDMATE_CHARTER='Supervise the alpha domain.' \ + "$ROOT/bin/fm-brief.sh" brief-worklog-secondmate --secondmate --no-projects >/dev/null 2>&1 + assert_no_grep "# Working log" "$home/data/brief-worklog-secondmate/brief.md" \ + "secondmate charter gained a per-task working log" + pass "fm-brief.sh: ship and scout briefs scaffold the agent-maintained working log" +} + +# A relaunched ship worker re-reads the brief from the top, where step 1 still +# says to create a branch that now exists. That step must route it to its log +# rather than leave it stuck on a failing checkout. +test_ship_setup_step_is_resume_aware() { + local home brief + home="$TMP_ROOT/worklog-resume-home" + mkdir -p "$home/data" + FM_HOME="$home" "$ROOT/bin/fm-brief.sh" brief-worklog-resume someproj >/dev/null 2>&1 + brief="$home/data/brief-worklog-resume/brief.md" + assert_grep "if you are resuming and it already exists, check it out instead and read your working log below" "$brief" \ + "ship brief's first setup step is not resume-aware" + pass "fm-brief.sh: ship setup step routes a resuming worker to its working log" +} + test_herdr_lab_contract_is_explicit_and_complete() { local home id brief home="$TMP_ROOT/herdr-lab-home" @@ -625,6 +690,8 @@ test_ship_modes_generate_clean_briefs test_faster_paths_use_configured_authority_without_stacked_review test_no_mistakes_dod_wording test_ship_project_memory_wording +test_ship_and_scout_briefs_scaffold_working_log +test_ship_setup_step_is_resume_aware test_herdr_lab_contract_is_explicit_and_complete test_herdr_lab_contract_quotes_foreign_firstmate_path test_herdr_lab_omission_is_loud_for_ship_and_scout