feat(bin): scaffold an agent-maintained working log in ship and scout briefs - #1248
Open
tomharper wants to merge 2 commits into
Open
feat(bin): scaffold an agent-maintained working log in ship and scout briefs#1248tomharper wants to merge 2 commits into
tomharper wants to merge 2 commits into
Conversation
…elves A crewmate's understanding of its task lived only in its context. When a worker's context filled, later steers that had corrected the task definition were evicted while its visible plan still showed the first instruction, so it kept building an approach that had already been replaced. Recovery required firstmate to rewrite the brief by hand from conversation memory. Ship and scout briefs now scaffold an agent-maintained working log at data/<id>/log.md: established facts, the current approach, approaches tried and rejected and why, what was verified and how, and any steer received after dispatch. The worker writes it as it works and reads it first when resuming. The log is deliberately not the status file. state/<id>.status stays the sparse supervisor channel where every append wakes firstmate; the watcher reads only state/*.status and *.turn-ended, so log appends never wake anyone and the log can be as long as the task needs. It survives teardown with the task's other data, which is what makes relaunch a resume rather than a reconstruction. The brief's stay-inside-the-worktree rule gains the same carve-out the status file already has, since the log lives under data/. stuck-crewmate-recovery now relaunches with the same brief and lets the worker resume from its log, adding a progress note only for what the log cannot hold.
Owner
|
Automated reminder: thanks for the PR! This branch currently has a merge conflict with the base branch. When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again. Noted for firstmate#1248 at |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Give every crewmate a working memory the agent itself maintains, so a worker whose context fills can be relaunched and resume from disk instead of losing its understanding.
The failure this fixes (2026-07-29): a worker hit 100% context while its task definition was being corrected through four successive steers. Its visible task list still showed the FIRST instruction - every later correction had been evicted. Delivery of those steers succeeded; the messages landed and were then lost. It was building an approach the captain had explicitly replaced. Recovery worked only because firstmate rewrote the brief by hand from conversation memory. Durable state already exists for everything else (worktrees, branches, backlog, status files); what had no durable home is what the WORKER knows.
What the captain asked for, and the decisions made along the way:
Hard constraint from the captain: do NOT touch the status-file contract. Appends to state/.status wake firstmate and must stay sparse; the new log must never wake firstmate. I verified this structurally rather than assuming it - bin/fm-watch.sh reads only $STATE/*.status and *.turn-ended, never data/, so log appends cannot wake anyone.
Second constraint: keep the addition proportionate. A section in the generated brief plus the file - no new tooling, no wrapper scripts, no daemon. That is why there is no fm-log.sh helper and no new state machinery: the log is an ordinary Markdown file the agent writes with its own tools, and it survives teardown for free because fm-teardown.sh never removes data//.
Third constraint: bin/fm-brief.sh has a known fragility - an unescaped apostrophe inside a heredoc wrapped in a command substitution broke it before (issues #166, #958, #1069). The new WORKLOG_SECTION deliberately uses the same safe 'IFS= read -r -d ... <<DELIM || true' shape the existing DOD and Herdr builders use, never VAR=$(cat <<EOF). I generated ship and scout briefs for real under both Bash 5 and stock macOS Bash 3.2 (3.2.57) and confirmed both parse and render with no marker leak.
Two judgment calls I made that a reviewer reading only the diff would not know were deliberate:
Scope decisions: the secondmate charter deliberately does NOT get a per-task log (a secondmate is a persistent home with its own data and backlog, not a task). docs/architecture.md gains a short passage stating the two durable channels and their differing wake semantics, because that mechanism boundary is maintainer-architecture knowledge; the exact brief wording stays owned by bin/fm-brief.sh's header per the repo's one-owner rule.
Definition of done: a newly scaffolded ship brief and scout brief both instruct the worker to keep data//log.md, both generate without error, and stuck-crewmate-recovery plus AGENTS.md describe the resume-from-log path. Tests added to tests/fm-brief.test.sh cover both scaffolds, the resume pointer, the rule-2 carve-out, the no-wake separation, and delimiter/heredoc leak guards. bin/fm-lint.sh and bin/fm-doc-audience-check.sh are clean.
What Changed
bin/fm-brief.shnow emits a shared# Working logsection into ship and scout briefs, binding each crewmate todata/<id>/log.md: log continuously rather than at the end, record established facts, rejected approaches, and post-dispatch steers before acting on them, and read the log first when resuming. The section is built with the sameIFS= read -r -d '' … <<DELIM || trueshape as the existing DOD and Herdr builders rather than a command-substituted heredoc, and the secondmate charter deliberately gets no per-task log.fm/<id>branch and read its log instead of dead-ending on a failinggit checkout -b. The worktree-isolation assertion, primary-checkout stop, and thestate/<id>.statuswake contract are untouched..agents/skills/stuck-crewmate-recovery/SKILL.mdreplaces "relaunch with the same brief plus a progress note" with resume-from-log rules (read the log before relaunch, never delete or rewrite it, append a note only for what the log cannot hold, handle an absent or stale log), AGENTS.md section 11 and itsdata/map record the log and its retention requirement, anddocs/architecture.mdstates the two durable channels and their differing wake semantics.tests/fm-brief.test.shgainstest_ship_and_scout_briefs_scaffold_working_logandtest_ship_setup_step_is_resume_aware, covering both scaffolds, the resume pointer, the rule-2 carve-out, the no-wake separation, the secondmate exclusion, and delimiter/heredoc leak guards; the suite passes 18/18 and the documentation-audience suite 5/5.Risk Assessment
✅ Low: The change is confined to generated brief prose, two reference docs, and new tests, with no runtime logic or state-machine change: I confirmed the status-file contract and fm-watch.sh wake path are untouched, fm-teardown.sh never removes data/<id>/, no other file quoted the old rule-2 wording, and both ship and scout briefs render cleanly with no marker leak under Bash 5 and stock macOS Bash 3.2.57.
Testing
I ran the two targeted suites the change touches (tests/fm-brief.test.sh, 18/18, including both new working-log cases, and tests/fm-documentation-audiences.test.sh, 5/5), then went past unit level to show the intent working the way a crewmate and firstmate would actually experience it: I generated real ship and scout briefs and captured them verbatim as artifacts, regenerated both under stock macOS Bash 3.2.57 to exercise the known heredoc fragility with no marker leak, drove a real bin/fm-watch.sh subprocess to prove behaviorally that six appends to data/<id>/log.md leave firstmate asleep while a single status append wakes and exits it, and replayed the originating failure in a throwaway git repo where a relaunched worker hits the exit-128git checkout -bdead-end, follows the new resume clause, and recovers the fourth steer and both rejected approaches off disk. This change has no rendered UI surface, so there is no screenshot; the end-user surface is the generated brief markdown and the CLI/watcher behavior, which the attached briefs and transcripts capture directly. bin/fm-lint.sh and bin/fm-doc-audience-check.sh from the definition of done are static analysis owned by the lint phase and were deliberately not run here. Everything passed and I found nothing actionable.Evidence: Generated ship brief (the end-user surface a crewmate reads)
# Setup ... 1. First action: create your branch:git checkout -b fm/1247-worklog- if you are resuming and it already exists, check it out instead and read your working log below before doing anything else. 2. Runno-mistakes doctor; ... # Working log Keep a working log at<FM_HOME>/data/1247-worklog/log.mdand write it as you go, not at the end. If you are resuming this task, read that log first - it is your memory of this task, and it survives a relaunch that your context does not. Append to it whenever you establish something that would be expensive to rediscover: the task as you currently understand it, what you have established, what you are doing now, what you tried and rejected and why, and what you have verified and how you verified it. Rejected approaches matter as much as the live one - a resuming agent that does not know an approach already failed will spend its context repeating it. When firstmate steers you with a correction, a changed requirement, or a decision, append it to the log before you act on it; a steer that lives only in your context is exactly what a relaunch loses. The log is yours and appending to it never notifies firstmate, so keep it as long and as candid as it needs to be; the status file in the rules below is the separate sparse supervisor channel and its contract is unchanged. # Rules 1. Never push to the default branch. Never merge a PR. 2. Stay inside this worktree; the only files you may write outside it are your working log above and the status file below.Evidence: Generated scout brief (working-log section + rule-2 carve-out for report, log, status)
# Working log Keep a working log at<FM_HOME>/data/1248-scout/log.mdand write it as you go, not at the end. If you are resuming this task, read that log first - it is your memory of this task, and it survives a relaunch that your context does not. ... # Rules 1. Never push to any remote and never open a PR. 2. Stay inside this worktree; the only files you may write outside it are the report, your working log above, and the status file below.Evidence: Hard constraint proven behaviorally: log appends never wake firstmate, a status append does
== 1. crewmate appends to its working log, 6 times, as a real worker would == RESULT: watcher still blocked after 6 log appends - firstmate was NOT woken. watcher output so far: [] log.md is now 8 lines - length costs nothing. == 2. same crewmate appends ONE line to its status file == RESULT: watcher woke and exited - firstmate is summoned. watcher reason: [signal: .../state/task-42.status] == 3. why: the watcher's signal scan only ever looks at state/ == 418: for f in "$STATE"/.status "$STATE"/.turn-ended; doEvidence: End-to-end replay of the originating failure: relaunched worker resumes from disk
### 3. the worker's context fills and it is relaunched (fresh agent, zero memory) 18:1. First action: create your branch:git checkout -b fm/1247-retry-budget- if you are resuming and it already exists, check it out instead and read your working log below before doing anything else. $ git checkout -b fm/1247-retry-budget # what step 1 literally says first fatal: a branch named 'fm/1247-retry-budget' already exists (exit 128) <- without the resume clause this is where a relaunch dead-ends $ git checkout fm/1247-retry-budget # what the brief's resume clause routes it to Switched to branch 'fm/1247-retry-budget' ### 5. what the relaunched worker now knows that the old flow lost latest steer recovered from disk: steer: skip the per-call-site knob entirely; one option on the request is the whole scope. rejected approaches it will not repeat: - Constructor argument on NewClient - rejected by steer #2; every caller would have to change. - Package-level mutable default - rejected: not safe across concurrent clients. ### 6. teardown: the status channel is deleted, the log is not 1232:rm -f "$STATE/$ID.status" "$STATE/$ID.turn-ended" "$STATE/$ID.meta" matches for a data/<id> removal: 0Evidence: The worker log that survived the relaunch (persisted state)
Evidence: Brief generation under stock macOS Bash 3.2.57 (heredoc regression class)
$ /bin/bash --version | head -1 GNU bash, version 3.2.57(1)-release (arm64-apple-darwin24) $ /bin/bash -n bin/fm-brief.sh; echo rc=$? rc=0 $ /bin/bash bin/fm-brief.sh 32-ship myproj (stock macOS Bash 3.2) scaffolded: .../data/32-ship/brief.md (ship, mode=no-mistakes; replace {TASK}) rc=0 $ /bin/bash bin/fm-brief.sh 32-scout myproj --scout scaffolded: .../data/32-scout/brief.md (scout; replace {TASK}) rc=0 $ grep -c 'WORKLOG' data//brief.md # heredoc delimiter leak check (expect 0) .../32-scout/brief.md:0 .../32-ship/brief.md:0 $ grep -n '# Working log' data//brief.md .../32-scout/brief.md:17:# Working log .../32-ship/brief.md:21:# Working logEvidence: Evidence script: real fm-watch.sh no-wake demo
Evidence: Evidence script: end-to-end resume-from-log replay
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
tests/fm-brief.test.sh:330- The secondmate half of test_ship_and_scout_briefs_scaffold_working_log asserts absence with assert_no_grep against a brief that may never have been written, so it can pass vacuously. The test file usesset -uonly (noset -e), and the scaffold call on lines 328-329 discards both output streams and its exit status. Ifbin/fm-brief.sh --secondmate --no-projectsever exits non-zero, brief.md is absent,grep -Fexits 2,!inverts that to success, and the test reports ok having verified nothing. The ship/scout loop guards this correctly withassert_presenton line 301. Fix: addassert_present "$home/data/brief-worklog-secondmate/brief.md" "secondmate charter was not scaffolded"before the assert_no_grep.✅ **Test** - passed
✅ No issues found.
bash tests/fm-brief.test.sh— 18/18 pass, including the newtest_ship_and_scout_briefs_scaffold_working_logandtest_ship_setup_step_is_resume_awarebash tests/fm-documentation-audiences.test.sh— 5/5 pass, covering the AGENTS.md and docs/architecture.md prose editsFM_HOME=<tmp> bin/fm-brief.sh 1247-worklog myprojand... 1248-scout myproj --scout— generated real ship and scout briefs and inspected the rendered markdown a crewmate would read/bin/bash --version(3.2.57),/bin/bash -n bin/fm-brief.sh,FM_HOME=<tmp> /bin/bash bin/fm-brief.sh 32-ship myproj/... 32-scout myproj --scout— stock macOS Bash 3.2 generation withgrep -c WORKLOG= 0 on both briefs (heredoc/delimiter leak guard)Manual no-wake demo driving a realbin/fm-watch.shsubprocess: 6 appends todata/task-42/log.mdleft the watcher blocked with empty output; one append tostate/task-42.statuswoke it and exited withsignal: .../task-42.statusManual end-to-end resume demo in a throwaway git repo: brief scaffolded, worker logs 4 post-dispatch steers, relaunch re-reads brief,git checkout -b fm/<id>fails exit 128, resume clause routes togit checkout+ read log, steer #4 and both rejected approaches recovered from diskgrep -n 'rm -f "$STATE/$ID.status"' bin/fm-teardown.shandgrep -c 'rm -rf.*$DATA/$ID' bin/fm-teardown.sh(0 matches) — confirmed the status file is deleted at teardown while data/<id>/ survivesgrep -rn 'progress note|progress so far' --include='*.md' .— no stale relaunch-with-a-progress-note wording left behindgit status --porcelain— worktree clean, no transient test artifacts left in the repobin/fm-brief.sh:4- Judgment call, left as-is: the header's opening summary still calls the generated contract "the standard Setup/Rules/Definition-of-done contract", which no longer enumerates the new Working log section. I did not edit it because that phrase names the contract shape rather than listing sections, and the same header (lines 42-47) already owns the working-log fact in full; expanding the shorthand would duplicate a fact its owner states six lines later.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.