Skip to content

feat(bin): scaffold an agent-maintained working log in ship and scout briefs - #1248

Open
tomharper wants to merge 2 commits into
kunchenguid:mainfrom
tomharper:fm/crew-worklog
Open

feat(bin): scaffold an agent-maintained working log in ship and scout briefs#1248
tomharper wants to merge 2 commits into
kunchenguid:mainfrom
tomharper:fm/crew-worklog

Conversation

@tomharper

Copy link
Copy Markdown

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:

  1. data//log.md - an agent-maintained working log. The crewmate appends at each meaningful step: what it established, what it tried and rejected and why, what it is doing now, what it has verified and how. Rejected approaches matter as much as the current one, so a resuming agent does not redo them.
  2. Scaffold it in bin/fm-brief.sh for ship and scout briefs. The generated brief must instruct continuous logging (not at the end) and must point at the log as the resume path: 'if you are resuming, read your log first - it is your memory of this task.'
  3. Make relaunch read it, rather than firstmate reconstructing a brief by hand.
  4. Update reference material where an agent will find it: AGENTS.md section 11 and the stuck-crewmate-recovery skill, which previously said to relaunch 'with the same brief plus a progress note'.

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:

  • The brief's rule 2 ('stay inside this worktree; modify nothing outside it') would have forbidden writing the log, since data// lives outside the worktree. I extended rule 2's existing carve-out - which already excepts the status file, and for scouts the report - to include the working log. Without this a careful worker could not obey both rules at once. This is a wording fix to an existing latent inconsistency, not a loosening of worktree isolation: the worktree-isolation assertion and the primary-checkout stop are untouched.
  • A relaunched ship worker re-reads the brief from the top, where step 1 says to create a branch that now already exists. I added one clause routing it to check out the existing branch and read its log first, so the designed resume path does not dead-end on a failing git checkout -b.

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.sh now emits a shared # Working log section into ship and scout briefs, binding each crewmate to data/<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 same IFS= read -r -d '' … <<DELIM || true shape as the existing DOD and Herdr builders rather than a command-substituted heredoc, and the secondmate charter deliberately gets no per-task log.
  • Two brief-wording fixes make the resume path actually usable: rule 2's write-outside-the-worktree carve-out now covers the working log alongside the status file (and the scout report), and the ship brief's first setup step routes a resuming worker to check out its existing fm/<id> branch and read its log instead of dead-ending on a failing git checkout -b. The worktree-isolation assertion, primary-checkout stop, and the state/<id>.status wake contract are untouched.
  • Reference material follows the new path: .agents/skills/stuck-crewmate-recovery/SKILL.md replaces "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 its data/ map record the log and its retention requirement, and docs/architecture.md states the two durable channels and their differing wake semantics.
  • tests/fm-brief.test.sh gains test_ship_and_scout_briefs_scaffold_working_log and test_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-128 git checkout -b dead-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. Run no-mistakes doctor; ... # Working log Keep a working log at &lt;FM_HOME&gt;/data/1247-worklog/log.md and 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.

You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

# Task
{TASK}

# Herdr lifecycle declaration - NOT ENABLED
**HARD SAFETY GATE:** this scaffold cannot inspect the task text that replaces `{TASK}` later.
If the task will start, stop, delete, restart, profile, or otherwise drive Herdr lifecycle behavior, stop and regenerate the brief with `--herdr-lab` before dispatch.
Do not add Herdr lifecycle commands to this unguarded brief by hand.

# Setup
You are in a disposable git worktree of myproj, at a detached HEAD on a clean default branch.

**Verify isolation before anything else.** Run `pwd -P` and `git rev-parse --show-toplevel`; both must resolve to the disposable task worktree you were launched in, such as a treehouse pool path or an Orca-managed worktree, not the primary checkout firstmate operates from.
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/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. Run `no-mistakes doctor`; if it reports the repo is not initialized here, run `no-mistakes init`.

# Working log
Keep a working log at `/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/demo-home/data/1247-worklog/log.md` and 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.
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}" >> '/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/demo-home/state/1247-worklog.status'`
   States: working, needs-decision, blocked, paused, done, failed.
   Each append wakes firstmate, so report sparingly: only phase changes a supervisor
   would act on (setup done, bug reproduced, fix implemented, validation passed) and the
   needs-decision/blocked/paused/done/failed states. No step-by-step FYI progress lines;
   firstmate reads your pane for that.
   A mid-task `working:` line (including setup complete) is nonterminal: do not end the
   turn after it; continue the same stage until a defined `done:` gate under Definition of done.
   Use `paused: {why}` - distinct from `blocked:` - ONLY when you are deliberately idling on a
   known external wait you expect to clear on its own (an upstream release, a rate-limit reset,
   a scheduled window): firstmate then leaves your idle pane alone and rechecks it on a long
   cadence instead of treating it as a possible wedge. Use `blocked:` when you are stuck and need help.
5. If you hit the same obstacle twice, append `blocked: {why}` and stop; firstmate will help.
6. If a decision belongs above the implementation worker (product choices, destructive actions, ask-user findings),
   append `needs-decision: {summary of options}` and stop. Firstmate will apply the configured authority and reply with the decision.
   When firstmate replies or a blocker clears and you resume, append `resolved: {how it was decided or unblocked}` (add the same `[key=<slug>]` if you opened it with one) so the decision or blocker is durably closed and does not keep resurfacing.
7. Never stop, restart, or update the shared `no-mistakes` daemon - it is one instance serving
   every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes
   daemon error, append `blocked: {the daemon error}` and stop; only firstmate manages the daemon.

# Project memory
If `AGENTS.md` or `CLAUDE.md` already exists, or if this task produced durable project-intrinsic knowledge, run `/Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KYQ490VM3MVCC03W858HMHYM/bin/fm-ensure-agents-md.sh .` in the worktree.
Record only project knowledge useful to almost every future session.
For anything the codebase already shows, prefer a pointer to the authoritative file, command, or doc over copying the detail.
If you touch a project `AGENTS.md` that lacks `## Maintaining this file`, add that short self-governance section from `/Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KYQ490VM3MVCC03W858HMHYM/bin/fm-ensure-agents-md.sh` in the same pass.
Keep it proportionate: skip `AGENTS.md` edits for trivial tasks that produced no durable project knowledge.

# Definition of done
The task is complete only when committed on your branch.
When you believe it is complete, append `done: {summary}` to the status file and stop.
Firstmate will then instruct you to run /no-mistakes to validate and ship a PR.

You drive no-mistakes by responding to its gates, not by implementing fixes.
Follow the guidance no-mistakes itself provides for the mechanics: it loads when you invoke /no-mistakes, and `no-mistakes axi run --help` plus the `help` lines in each `axi` response are authoritative and version-matched to the installed binary.
Do not hand-edit, commit, or fix findings yourself while a run is active - the pipeline applies every fix.

Two firstmate-specific rules layer on top of that guidance:
- ask-user findings are never yours to answer: escalate to firstmate (rule 6) and stop.
  Firstmate applies the authority contract in its `AGENTS.md` and obtains any required captain decision.
  When the decision comes back, feed it to the gate with `no-mistakes axi respond` and let the pipeline apply it - do not route the question to "the user" or implement the fix yourself.
- Avoid `--yes`: it would silently bypass firstmate's authority check and any required captain escalation.

After /no-mistakes reports CI green (the CI-ready return point - do not wait for it to keep monitoring in the background until merge), append `done: PR {url} checks green` and stop. You are finished.
Evidence: Generated scout brief (working-log section + rule-2 carve-out for report, log, status)

# Working log Keep a working log at &lt;FM_HOME&gt;/data/1248-scout/log.md and 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.

You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

# Task
{TASK}

# Herdr lifecycle declaration - NOT ENABLED
**HARD SAFETY GATE:** this scaffold cannot inspect the task text that replaces `{TASK}` later.
If the task will start, stop, delete, restart, profile, or otherwise drive Herdr lifecycle behavior, stop and regenerate the brief with `--herdr-lab` before dispatch.
Do not add Herdr lifecycle commands to this unguarded brief by hand.

# Setup
You are in a disposable git worktree of myproj, at a detached HEAD on a clean default branch.
This is a SCOUT task: the deliverable is a written report, not a PR.
The worktree is your laboratory - install, run, edit, and make scratch commits freely; all of it is discarded at teardown.
The report is the only deliverable that survives, so anything worth keeping must be in it.

# Working log
Keep a working log at `/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/demo-home/data/1248-scout/log.md` and 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 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.
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}" >> '/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/demo-home/state/1248-scout.status'`
   States: working, needs-decision, blocked, paused, done, failed.
   Each append wakes firstmate, so report sparingly: only phase changes a supervisor
   would act on and the needs-decision/blocked/paused/done/failed states. No step-by-step
   FYI progress lines; firstmate reads your pane for that.
   Use `paused: {why}` - distinct from `blocked:` - ONLY when you are deliberately idling on a
   known external wait you expect to clear on its own (an upstream release, a rate-limit reset):
   firstmate then leaves your idle pane alone and rechecks it on a long cadence instead of
   treating it as a possible wedge. Use `blocked:` when you are stuck and need help.
5. If you hit the same obstacle twice, append `blocked: {why}` and stop; firstmate will help.
6. If a decision belongs to a human (product choices, destructive actions),
   append `needs-decision: {summary of options}` and stop. Firstmate will reply with the decision.
   When firstmate replies or a blocker clears and you resume, append `resolved: {how it was decided or unblocked}` (add the same `[key=<slug>]` if you opened it with one) so the decision or blocker is durably closed and does not keep resurfacing.
7. Never stop, restart, or update the shared `no-mistakes` daemon - it is one instance serving
   every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes
   daemon error, append `blocked: {the daemon error}` and stop; only firstmate manages the daemon.

# Definition of done
Write your findings to `/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/demo-home/data/1248-scout/report.md`.
The report must stand alone: what you did, what you found, the evidence (commands run, output, file:line references), and what you recommend.
Before reporting done, read and follow `/Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KYQ490VM3MVCC03W858HMHYM/.agents/skills/decision-hold-lifecycle/SKILL.md` and pass its shared completion gate for the report and any visual review.
When the report is complete, append `done: {one-line conclusion}` to the status file and stop.
If your findings reveal work that should ship (e.g. you reproduced a bug and the fix is clear), say so in the report; firstmate may promote this task in place, and you would then receive mode-specific ship instructions as a follow-up message.
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; do

== fixture ==
state/  : task-42.status 
data/   : task-42/log.md

(watcher exited on the pre-seeded status file - restarting it quiet)
signal: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-worklog-nowake.nIbWxu/nowake/state/task-42.status
== 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: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-worklog-nowake.nIbWxu/nowake/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; do
Evidence: 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" &#10;matches for a data/<id> removal: 0

### 1. firstmate scaffolds the brief
warn: no registry at /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/resume-sandbox/fm-home/data/projects.md; defaulting proj to no-mistakes off
scaffolded: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/resume-sandbox/fm-home/data/1247-retry-budget/brief.md (ship, mode=no-mistakes; replace {TASK})

--- the brief tells the worker to keep a log, and where ---
# Working log
Keep a working log at `/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/resume-sandbox/fm-home/data/1247-retry-budget/log.md` and 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.


### 2. the crewmate works, and is steered four times; it logs each steer before acting
worker's log is now 34 lines on disk at data/1247-retry-budget/log.md

### 3. the worker's context fills and it is relaunched (fresh agent, zero memory)
the relaunched worker re-reads the brief from the top. Step 1 says:
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'

### 4. ...and read your working log below before doing anything else
$ cat /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/resume-sandbox/fm-home/data/1247-retry-budget/log.md
    # Working log - 1247-retry-budget
    
    ## Task as I currently understand it
    Make the retry budget configurable per call site.
    
    ## Established
    - The retry loop lives in client/retry.go; the budget is a package-level const.
    - Reproduced the original failure with `go test ./client -run TestBudget`.
    
    ## Now
    Threading a budget parameter through NewClient.
    
    ## Steer received after dispatch (#1)
    steer: budget belongs on the request, not the client.
    Logged before acting on it.
    
    ## Steer received after dispatch (#2)
    steer: do not add a constructor arg - use a functional option.
    Logged before acting on it.
    
    ## Steer received after dispatch (#3)
    steer: default must stay the old const so existing callers do not change behavior.
    Logged before acting on it.
    
    ## Steer received after dispatch (#4)
    steer: skip the per-call-site knob entirely; one option on the request is the whole scope.
    Logged before acting on it.
    
    ## Tried and rejected
    - Constructor argument on NewClient - rejected by steer #2; every caller would have to change.
    - Package-level mutable default - rejected: not safe across concurrent clients.
    
    ## Verified
    - `go test ./client` passes with the request-level option and the old default.

### 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.
    Logged before acting on it.
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
bin/fm-teardown.sh removes state/<id>.* and never touches data/<id>/:
1232:rm -f "$STATE/$ID.status" "$STATE/$ID.turn-ended" "$STATE/$ID.meta" \
matches for a data/<id> removal: 0
Evidence: The worker log that survived the relaunch (persisted state)
# Working log - 1247-retry-budget

## Task as I currently understand it
Make the retry budget configurable per call site.

## Established
- The retry loop lives in client/retry.go; the budget is a package-level const.
- Reproduced the original failure with `go test ./client -run TestBudget`.

## Now
Threading a budget parameter through NewClient.

## Steer received after dispatch (#1)
steer: budget belongs on the request, not the client.
Logged before acting on it.

## Steer received after dispatch (#2)
steer: do not add a constructor arg - use a functional option.
Logged before acting on it.

## Steer received after dispatch (#3)
steer: default must stay the old const so existing callers do not change behavior.
Logged before acting on it.

## Steer received after dispatch (#4)
steer: skip the per-call-site knob entirely; one option on the request is the whole scope.
Logged before acting on it.

## Tried and rejected
- Constructor argument on NewClient - rejected by steer #2; every caller would have to change.
- Package-level mutable default - rejected: not safe across concurrent clients.

## Verified
- `go test ./client` passes with the request-level option and the old default.
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 log

$ /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)
warn: no registry at /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/bash32-home/data/projects.md; defaulting myproj to no-mistakes off
scaffolded: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/bash32-home/data/32-ship/brief.md (ship, mode=no-mistakes; replace {TASK})
rc=0

$ /bin/bash bin/fm-brief.sh 32-scout myproj --scout
scaffolded: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/bash32-home/data/32-scout/brief.md (scout; replace {TASK})
rc=0

$ grep -c 'WORKLOG' data/*/brief.md   # heredoc delimiter leak check (expect 0)
/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/bash32-home/data/32-scout/brief.md:0
/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/bash32-home/data/32-ship/brief.md:0

$ grep -n '# Working log' data/*/brief.md
/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/bash32-home/data/32-scout/brief.md:17:# Working log
/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T/no-mistakes-evidence/01KYQ490VM3MVCC03W858HMHYM/bash32-home/data/32-ship/brief.md:21:# Working log
Evidence: Evidence script: real fm-watch.sh no-wake demo
#!/usr/bin/env bash
# Evidence script (not part of the repo): drive a REAL bin/fm-watch.sh against a
# fixture firstmate home and show the hard constraint from the change's intent -
# appends to data/<id>/log.md never wake firstmate, while an append to
# state/<id>.status still does.
set -u

ROOT=${1:?worktree root}
EV=${2:?evidence dir}
. "$ROOT/tests/wake-helpers.sh"

TMP_ROOT=$(fm_test_tmproot fm-worklog-nowake)
DIR=$(make_case nowake)
STATE="$DIR/state"
DATA="$DIR/data/task-42"
mkdir -p "$DATA"
OUT="$DIR/watch.out"

printf 'working: reproducing the bug\n' > "$STATE/task-42.status"
cat > "$DATA/log.md" <<'LOG'
# Working log - task-42
- established: the failure is in the retry path, not the parser.
LOG

echo "== fixture =="
echo "state/  : $(ls "$STATE" | tr '\n' ' ')"
echo "data/   : task-42/log.md"
echo

# Start a real watcher. It blocks until it decides a wake is actionable.
PATH="$DIR/fakebin:$PATH" FM_STATE_OVERRIDE="$STATE" \
  FM_CREW_STATE_BIN="$DIR/fakebin/fm-crew-state.sh" \
  FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
  bash "$ROOT/bin/fm-watch.sh" > "$OUT" 2>&1 &
WATCHER=$!
sleep 2
# Drain the initial signal from the pre-seeded status file so the demo starts quiet.
kill -0 "$WATCHER" 2>/dev/null || { WATCHER=""; }
if [ -z "$WATCHER" ]; then
  echo "(watcher exited on the pre-seeded status file - restarting it quiet)"
  cat "$OUT"
  PATH="$DIR/fakebin:$PATH" FM_STATE_OVERRIDE="$STATE" \
    FM_CREW_STATE_BIN="$DIR/fakebin/fm-crew-state.sh" \
    FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
    bash "$ROOT/bin/fm-watch.sh" > "$OUT" 2>&1 &
  WATCHER=$!
  sleep 2
fi

echo "== 1. crewmate appends to its working log, 6 times, as a real worker would =="
for i in 1 2 3 4 5 6; do
  cat >> "$DATA/log.md" <<LOG
- tried approach #$i (patch the caller) - rejected: the caller cannot see the retry budget.
LOG
  sleep 0.4
done
sleep 3
if kill -0 "$WATCHER" 2>/dev/null; then
  echo "RESULT: watcher still blocked after 6 log appends - firstmate was NOT woken."
else
  echo "RESULT: FAIL - watcher exited on a working-log append."
fi
echo "watcher output so far: [$(cat "$OUT")]"
echo "log.md is now $(wc -l < "$DATA/log.md" | tr -d ' ') lines - length costs nothing."
echo

echo "== 2. same crewmate appends ONE line to its status file =="
printf 'needs-decision: retry budget belongs to the caller or the client?\n' >> "$STATE/task-42.status"
for _ in $(seq 1 40); do kill -0 "$WATCHER" 2>/dev/null || break; sleep 0.25; done
if kill -0 "$WATCHER" 2>/dev/null; then
  echo "RESULT: watcher still blocked (unexpected)."
  kill "$WATCHER" 2>/dev/null
else
  echo "RESULT: watcher woke and exited - firstmate is summoned."
fi
echo "watcher reason: [$(cat "$OUT")]"
echo

echo "== 3. why: the watcher's signal scan only ever looks at state/ =="
grep -n 'for f in "\$STATE"' "$ROOT/bin/fm-watch.sh"
Evidence: Evidence script: end-to-end resume-from-log replay
#!/usr/bin/env bash
# Evidence script (not part of the repo): replay the exact failure the change
# fixes - a worker whose context fills after four steers - and show that with the
# scaffolded working log the relaunched worker resumes from disk.
set -u
ROOT=${1:?worktree root}
EV=${2:?evidence dir}

SB="$EV/resume-sandbox"; rm -rf "$SB"; mkdir -p "$SB"
HOME_DIR="$SB/fm-home"; mkdir -p "$HOME_DIR/data" "$HOME_DIR/state"
PROJ="$SB/proj"; mkdir -p "$PROJ"
git -C "$PROJ" init -q -b main
printf 'x\n' > "$PROJ/README.md"
git -C "$PROJ" add -A && git -C "$PROJ" -c user.email=t@e -c user.name=t commit -qm init
ID=1247-retry-budget

echo "### 1. firstmate scaffolds the brief"
FM_HOME="$HOME_DIR" bash "$ROOT/bin/fm-brief.sh" "$ID" proj
LOG="$HOME_DIR/data/$ID/log.md"
echo
echo "--- the brief tells the worker to keep a log, and where ---"
sed -n '/^# Working log$/,/^$/p' "$HOME_DIR/data/$ID/brief.md"
echo
echo "### 2. the crewmate works, and is steered four times; it logs each steer before acting"
git -C "$PROJ" checkout -q -b "fm/$ID"
cat > "$LOG" <<'LOG'
# Working log - 1247-retry-budget

## Task as I currently understand it
Make the retry budget configurable per call site.

## Established
- The retry loop lives in client/retry.go; the budget is a package-level const.
- Reproduced the original failure with `go test ./client -run TestBudget`.

## Now
Threading a budget parameter through NewClient.
LOG
for n in 1 2 3 4; do
  case $n in
    1) s="steer: budget belongs on the request, not the client." ;;
    2) s="steer: do not add a constructor arg - use a functional option." ;;
    3) s="steer: default must stay the old const so existing callers do not change behavior." ;;
    4) s="steer: skip the per-call-site knob entirely; one option on the request is the whole scope." ;;
  esac
  cat >> "$LOG" <<LOG

## Steer received after dispatch (#$n)
$s
Logged before acting on it.
LOG
done
cat >> "$LOG" <<'LOG'

## Tried and rejected
- Constructor argument on NewClient - rejected by steer #2; every caller would have to change.
- Package-level mutable default - rejected: not safe across concurrent clients.

## Verified
- `go test ./client` passes with the request-level option and the old default.
LOG
echo "worker's log is now $(wc -l < "$LOG" | tr -d ' ') lines on disk at data/$ID/log.md"
echo
echo "### 3. the worker's context fills and it is relaunched (fresh agent, zero memory)"
echo "the relaunched worker re-reads the brief from the top. Step 1 says:"
grep -n 'First action: create your branch' "$HOME_DIR/data/$ID/brief.md"
echo
echo "\$ git checkout -b fm/$ID     # what step 1 literally says first"
git -C "$PROJ" checkout main -q
out=$(git -C "$PROJ" checkout -b "fm/$ID" 2>&1); rc=$?
printf '    %s\n' "$out"
echo "    (exit $rc) <- without the resume clause this is where a relaunch dead-ends"
echo
echo "\$ git checkout fm/$ID        # what the brief's resume clause routes it to"
git -C "$PROJ" checkout "fm/$ID" 2>&1 | sed 's/^/    /'
echo
echo "### 4. ...and read your working log below before doing anything else"
echo "\$ cat $LOG"
sed 's/^/    /' "$LOG"
echo
echo "### 5. what the relaunched worker now knows that the old flow lost"
echo "latest steer recovered from disk:"
grep -A2 'Steer received after dispatch (#4)' "$LOG" | tail -2 | sed 's/^/    /'
echo "rejected approaches it will not repeat:"
sed -n '/^## Tried and rejected$/,/^$/p' "$LOG" | tail -n +2 | sed 's/^/    /'
echo
echo "### 6. teardown: the status channel is deleted, the log is not"
echo "bin/fm-teardown.sh removes state/<id>.* and never touches data/<id>/:"
grep -n 'rm -f "\$STATE/\$ID.status"' "$ROOT/bin/fm-teardown.sh"
grep -c 'rm -rf.*\$DATA/\$ID' "$ROOT/bin/fm-teardown.sh" | sed 's/^/matches for a data\/<id> removal: /'

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ 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 uses set -u only (no set -e), and the scaffold call on lines 328-329 discards both output streams and its exit status. If bin/fm-brief.sh --secondmate --no-projects ever exits non-zero, brief.md is absent, grep -F exits 2, ! inverts that to success, and the test reports ok having verified nothing. The ship/scout loop guards this correctly with assert_present on line 301. Fix: add assert_present &#34;$home/data/brief-worklog-secondmate/brief.md&#34; &#34;secondmate charter was not scaffolded&#34; before the assert_no_grep.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-brief.test.sh — 18/18 pass, including the new test_ship_and_scout_briefs_scaffold_working_log and test_ship_setup_step_is_resume_aware
  • bash tests/fm-documentation-audiences.test.sh — 5/5 pass, covering the AGENTS.md and docs/architecture.md prose edits
  • FM_HOME=&lt;tmp&gt; bin/fm-brief.sh 1247-worklog myproj and ... 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=&lt;tmp&gt; /bin/bash bin/fm-brief.sh 32-ship myproj / ... 32-scout myproj --scout — stock macOS Bash 3.2 generation with grep -c WORKLOG = 0 on both briefs (heredoc/delimiter leak guard)
  • Manual no-wake demo driving a real bin/fm-watch.sh subprocess: 6 appends to data/task-42/log.md left the watcher blocked with empty output; one append to state/task-42.status woke it and exited with signal: .../task-42.status
  • Manual 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/&lt;id&gt; fails exit 128, resume clause routes to git checkout + read log, steer #4 and both rejected approaches recovered from disk
  • grep -n &#39;rm -f &#34;$STATE/$ID.status&#34;&#39; bin/fm-teardown.sh and grep -c &#39;rm -rf.*$DATA/$ID&#39; bin/fm-teardown.sh (0 matches) — confirmed the status file is deleted at teardown while data/<id>/ survives
  • grep -rn &#39;progress note|progress so far&#39; --include=&#39;*.md&#39; . — no stale relaunch-with-a-progress-note wording left behind
  • git status --porcelain — worktree clean, no transient test artifacts left in the repo
⚠️ **Document** - 1 info
  • ℹ️ bin/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.

…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.
@kunchenguid

kunchenguid commented Jul 30, 2026

Copy link
Copy Markdown
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 ea9c88dc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants