Skip to content

feat(bin): add dreamer scout brief, idle watch, and memory grader - #8

Open
BohnBawerick wants to merge 9 commits into
mainfrom
fm/fm-memory-slice3
Open

feat(bin): add dreamer scout brief, idle watch, and memory grader#8
BohnBawerick wants to merge 9 commits into
mainfrom
fm/fm-memory-slice3

Conversation

@BohnBawerick

Copy link
Copy Markdown
Owner

Intent

Build Memory Slice 3 of the adopted Firstmate memory architecture (data/fm-memory-design/report.md sections 16-21): the Dreamer Scout Brief, Idle Trigger, and Independent Grader.

Slice 1 established bin/fm-memory-compile.sh and bin/fm-memory-migrate.sh. Slice 2 established bin/fm-memory-drop.sh, bin/fm-memory-verify.sh, and bin/fm-memory-publish.sh. Slice 3 completes the dreamer loop with three deliverables plus tests.

  1. Dreamer Scout Brief: scaffold the ephemeral dreamer scout brief via a --dreamer flag on bin/fm-brief.sh. The brief instructs the dreamer agent to: read recent status logs, completed task reports, data/memory/drop/, and current published memory; synthesize durable abstractions from tactical drop notes; produce a new immutable generation in data/memory/gen//; run bin/fm-memory-verify.sh on the proposed generation; and never edit published memory in place, never take the session lock, and never address the captain.

  2. Idle Dreamer Watch / Evaluation Helper: bin/fm-dreamer-watch.sh checks whether a dream pass is due (data/memory/drop/ has unconsumed drop files, or data/memory/HEAD age exceeds a threshold, and the fleet has no live non-dreamer workers). It can check or arm the condition watch for firstmate idle notification.

  3. Independent Grader Rubric & Helper: bin/fm-dreamer-grade.sh evaluates proposed core memory diffs against a rubric that rejects tactical scraps, requires valid citations, and rejects contradictions with standing rules, and approves or rejects publication of proposed core modifications.

  4. Automated Tests: a comprehensive suite tests/fm-dreamer.test.sh covering dreamer brief scaffolding and contract rules, idle evaluation logic, grader rubric pass/fail conditions, and full integration with the verify and publish scripts. All scripts must pass bin/fm-lint.sh.

Follow firstmate-coding-guidelines strictly before editing tracked files. Never use em dashes; use plain hyphens. Never add agent co-author lines. The local main branch is authoritative; origin is https://github.com/BohnBawerick/firstmate.git.

What Changed

  • bin/fm-brief.sh accepts a new --dreamer kind that scaffolds the ephemeral dreamer scout contract: read-only scope over status logs, task reports, data/memory/drop/, and published memory; write a new immutable generation under data/memory/gen/<N>/; run bin/fm-memory-verify.sh on it; and never edit published memory in place, take the session lock, or address the captain. The --herdr-lab and --mode argument checks now cover the dreamer kind.
  • New bin/fm-dreamer-watch.sh with check, arm, and mark-due: it reports a dream pass due only when no live non-dreamer worker exists and either the drop tray holds an unconsumed file or data/memory/HEAD is older than FM_DREAM_HEAD_AGE_HOURS (default 12), arms the condition watch through bin/fm-procevent-when.sh at FM_DREAM_WATCH_INTERVAL (default 3600), and writes the durable state/.dream-due marker without spawning any agent. A missing home, a missing state/, or a symlinked data/memory reports not-due.
  • New bin/fm-dreamer-grade.sh with grade and scout: grade delegates mechanical safety to bin/fm-memory-verify.sh, flags tactical scraps in changed core and note statements, and rejects negations or removals of standing core rules; scout scaffolds a fresh-context grader brief limited to the two generations and their cited files. New tests/fm-dreamer.test.sh covers brief scaffolding, watch evaluation, grader rubric, and verify/publish integration, and bin/fm-test-run.sh maps bin/fm-dreamer* and bin/fm-brief.sh onto the session-bootstrap family so those tests get selected. AGENTS.md, docs/configuration.md, and docs/scripts.md document the new marker, env vars, and scripts.

Risk Assessment

✅ Low: The change is purely additive (two new scripts, a new brief kind, and a test-selection map entry), it satisfies every source-verifiable acceptance criterion in the intent, bin/fm-lint.sh is clean, I confirmed each of the five prior fix rounds by running the real scripts, and the only remaining findings are two informational validation/documentation gaps in optional flags.

Testing

I ran the new tests/fm-dreamer.test.sh (35 behavioral checks, all pass) plus the three suites this change touches (fm-brief, fm-test-run, fm-memory-verify), then went beyond unit coverage and drove the whole feature by hand in a disposable firstmate home, recording a CLI transcript as the reviewer-visible artifact. The transcript shows the idle trigger flipping not-due/due on an unconsumed drop and on a stale HEAD, a genuinely live tmux worker blocking the dream, the armed watch argv and the durable state/.dream-due marker, the emitted dreamer brief carrying the never-lock / never-edit-published-memory-in-place / never-address-the-captain contract and the mandatory bin/fm-memory-verify.sh step, and the full happy path where a proposed generation verifies, the grader approves, publish swaps HEAD and the compiler serves the promoted note to a new session. It also shows both reject paths (a core that reverses a standing rule, flagged alongside a tactical-scrap warning, and an uncited claim caught by the mechanical half) with HEAD left untouched, plus the fresh-context grader scout brief. This change is shell CLI only, with no rendered UI surface, so the CLI transcript is the actual end-user experience and no screenshot applies. No test failures, no flaky or setup problems, and the worktree is clean with all temporary homes removed; per this phase's rules I did not run bin/fm-lint.sh.

Evidence: Dreamer loop end-to-end CLI transcript

Source: Dreamer loop end-to-end CLI transcript

FIRSTMATE HOME: /tmp/fm-dream-home.zfXmAS  (gen/0 published, task fm-task-9 finished)

==============================================================
 1. IDLE TRIGGER - is a dream pass due?
==============================================================

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-watch.sh check
DREAM_DUE: not-due reason=no unconsumed drops and HEAD not stale
exit=1

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-memory-drop.sh fm-task-9 --claim Healthlog requires xvfb for playwright
drop: deposited candidate claims for fm-task-9 to data/memory/drop/fm-task-9.md (1 claim(s))
exit=0

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-watch.sh check
DREAM_DUE: due reason=unconsumed candidate files in data/memory/drop
exit=0

-- a live non-dreamer worker must block the dream --

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-watch.sh check
DREAM_DUE: not-due reason=live non-dreamer worker(s): fm-task-live
exit=1

-- HEAD older than the threshold is also due --

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-watch.sh check --head-age 12
DREAM_DUE: due reason=data/memory/HEAD older than 12 hours
exit=0

-- arming the idle notification watch (dry run shows the exact argv) --

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-watch.sh arm --dry-run
would arm: bin/fm-procevent-when.sh arm dream-due --interval 3600 --stable 2 --condition /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-watch.sh check --head-age 12 --home /tmp/fm-dream-home.zfXmAS --action /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-watch.sh mark-due when-dream-due --home /tmp/fm-dream-home.zfXmAS
exit=0

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-watch.sh mark-due drop-tray
dream due marker written for drop-tray
exit=0
-- state/.dream-due --
dream due: drop-tray

==============================================================
 2. DREAMER SCOUT BRIEF - the prompt handed to the dreamer
==============================================================

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-brief.sh fm-dream-1 firstmate --dreamer
scaffolded: /tmp/fm-dream-home.zfXmAS/data/fm-dream-1/brief.md (dreamer; replace {TASK})
exit=0

-- data/fm-dream-1/brief.md --
You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

# Task
You are the DREAMER: an offline memory-consolidation pass for the firstmate home at /tmp/fm-dream-home.zfXmAS.
Your job is to turn tactical records into durable abstractions and to propose a complete new
memory generation, never to report to the captain and never to change what any session already sees.
The `{TASK}` placeholder in a normal scout is filled with the specific home, cursor, and generation
number by firstmate; keep to the contract below regardless of that detail.

## Read, and only read
- The append-only log since the last dream cursor: `state/<id>.status` tails, `data/<id>/report.md`,
  `data/backlog.md`, `data/decisions/*.md`, and the cold archives
  (`data/done-archive.md`, `data/note-archive.md`, `data/memory-archive.md`) when a claim is being corrected.
- The in-band candidate tray: everything under `data/memory/drop/`.
- The current published memory: the generation `data/memory/HEAD` names, plus the compiled catalog.
Do NOT read firstmate's conversation, worker panes, or anything under `projects/`.

## Synthesize, do not copy
Distillation is the differentiator. A tactical scrap is `hz-verify-email-37 timed out in chrome-devtools-axi`.
A durable abstraction generalises to a session that never heard of this task: `Under multi-lane contention on this
host, chrome-devtools-axi times out; Playwright is the substitute`, with a citation. Promote a drop claim only when
it becomes standing knowledge or corrects something already standing; reject the rest.

## Write a new immutable generation
Produce the complete next generation under `data/memory/gen/<N>/` (where N is the next integer past the highest
existing generation) containing at minimum `notes/*.md` (one atomic claim per note, each with a resolvable citation),
`core.md` (the standing constitution, a subset or inspect-then-update of the current core, never a silent deletion),
and the source files the catalog is compiled from. Write only under `data/memory/`; never touch `projects/`.

## Mechanical verification is mandatory
Before you report done, run `bin/fm-memory-verify.sh <N>` on the proposed generation and let its four checks pass
(budget, citations, constitution, diff bounds). If verification fails, revise the generation rather than bypassing it.

# Hard safety contract
1. NEVER take the session lock. The live primary harness holds it; you must never contend for it.
2. NEVER edit published memory in place. Only a new immutable generation plus an atomic `data/memory/HEAD`
   pointer may change what a session sees, and firstmate owns that pointer swap after grading.
3. NEVER address the captain. Do not escalate to the captain; report only through your status file and your report.
4. NEVER write under `projects/` and never read the captain's conversation or worker panes.
5. A single-flight `state/.dream.lock` guarantees one dream at a time; never clear or force it.

# 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 firstmate, at a detached HEAD on a clean default branch.
This is an ephemeral DREAMER task: the deliverable is a proposed memory generation under the firstmate home's
`data/memory/gen/` plus a dream-receipt report, not a PR and not a chat reply.
The worktree is your laboratory; all scratch work in it is discarded at teardown. Anything worth keeping must
land in the generation or the report.

# 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 under the home's `data/memory/`,
   the report, 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}" >> '/tmp/fm-dream-home.zfXmAS/state/fm-dream-1.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 above you (product choices, destructive actions, ask-user findings),
   append `needs-decision: {summary of options}` and stop. Firstmate will apply the configured authority and reply.
   A decision or blocker you opened stays open until a `resolved` line carrying its exact key lands; a later `done:`
   or `working:` line never closes it, even when the answer is what started that work.
   Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply,
   append `resolved: {how it cleared}` yo

... [1306 bytes truncated] ...

================
dreamer wrote data/memory/gen/1/{core.md,notes/n1.md,notes/headless.md}

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-memory-verify.sh 1
VERIFYING GENERATION: 1 (/tmp/fm-dream-home.zfXmAS/data/memory/gen/1)
--------------------------------------------------------------------------------
PASS budget: compiled bundle is 190 estimated tokens <= 7500 cap (core=24 catalog=75 status=within-budget)
PASS citations: all claims cite existing sources (3 verified citations across 2 note(s))
PASS constitution: standing preferences from the published core.md are preserved in core.md
PASS diff-bounds: proposed generation changes are within bounds (0 deleted, 0 modified, 1 added of 1 baseline notes)
--------------------------------------------------------------------------------
VERIFICATION PASSED: all 4 safety checks passed
exit=0

==============================================================
 4. INDEPENDENT GRADER - approve the good generation
==============================================================

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-grade.sh grade gen/0 gen/1
PASS grade: proposed generation passes the mechanical verifier
GRADE APPROVED: no contradiction with standing rules
exit=0

-- firstmate publishes only after the grade approves --

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-memory-publish.sh 1
VERIFYING GENERATION: 1 (/tmp/fm-dream-home.zfXmAS/data/memory/gen/1)
--------------------------------------------------------------------------------
PASS budget: compiled bundle is 190 estimated tokens <= 7500 cap (core=24 catalog=75 status=within-budget)
PASS citations: all claims cite existing sources (3 verified citations across 2 note(s))
PASS constitution: standing preferences from the published core.md are preserved in core.md
PASS diff-bounds: proposed generation changes are within bounds (0 deleted, 0 modified, 1 added of 1 baseline notes)
--------------------------------------------------------------------------------
VERIFICATION PASSED: all 4 safety checks passed
memory: published generation gen/1 to data/memory/HEAD
exit=0
-- data/memory/HEAD --
gen/1

-- what a new session now reads (first 26 lines) --
COMPILED WORKING MEMORY (data/memory/gen/1)
--------------------------------------------------------------------------------
Compiled by bin/fm-memory-compile.sh against a 7500-estimated-token cap.
This is the whole startup memory surface: it is selected and capped, not dumped.
The catalog lists every note that exists, so when a title below matches what
this turn needs and its body was not injected, read that file by its path.

core: data/memory/gen/1/core.md
--------------------------------------------------------------------------------
# Core
<!-- source: data/captain.md -->
- Rule One: always test changes

catalog (compiled from data/memory/gen/1/notes/)
--------------------------------------------------------------------------------
<!-- compiled by bin/fm-memory-compile.sh from data/memory/gen/1/notes/ -->

- Headless browsers on this host need xvfb (headless.md | healthlog, playwright, browser | 2026-08-20)
- Standing Note (n1.md | test | 2026-08-20)

hot note: notes/headless.md
--------------------------------------------------------------------------------
---
title: Headless browsers on this host need xvfb
triggers: healthlog, playwright, browser
updated: 2026-08-20

==============================================================
 5. INDEPENDENT GRADER - reject a bad generation
==============================================================
dreamer proposed gen/2: it reverses a standing rule and adds a dated recap

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-grade.sh grade gen/1 gen/2
PASS grade: proposed generation passes the mechanical verifier
FAIL grade: new core contradicts a standing rule: - Rule One: never test changes
WARN grade: possible tactical scrap in changed note notes/scrap.md: On 2026-08-19 task fm-task-9 timed out.
GRADE REJECTED: 1 rubric violation(s) found
exit=1
(exit=1 is the REJECT verdict; firstmate does not swap HEAD, which still reads:)
gen/1

-- an uncited claim is rejected by the mechanical half of the rubric --

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-grade.sh grade gen/1 gen/3
FAIL grade: proposed generation fails the mechanical verifier
VERIFYING GENERATION: /tmp/fm-dream-home.zfXmAS/data/memory/gen/3 (/tmp/fm-dream-home.zfXmAS/data/memory/gen/3)
--------------------------------------------------------------------------------
PASS budget: compiled bundle is 209 estimated tokens <= 7500 cap (core=24 catalog=94 status=within-budget)
FAIL citations: note notes/uncited.md cites missing or unresolvable source: "data/does-not-exist.md"
PASS constitution: standing preferences from the published core.md are preserved in core.md
PASS diff-bounds: proposed generation changes are within bounds (0 deleted, 0 modified, 1 added of 2 baseline notes)
--------------------------------------------------------------------------------
VERIFICATION FAILED: 1 check(s) failed
GRADE REJECTED: 1 rubric violation(s) found
exit=1

==============================================================
 6. GRADER SCOUT BRIEF - fresh-context judgment half
==============================================================

$ /home/paiva/.no-mistakes/worktrees/3437026af8a8/01M0EG82ZPEZT3WCZZVARCWAKJ/bin/fm-dreamer-grade.sh scout fm-grade-2 firstmate gen/1 gen/2
scaffolded: /tmp/fm-dream-home.zfXmAS/data/fm-grade-2/brief.md (grader scout; APPROVE/REJECT)
exit=0

-- data/fm-grade-2/brief.md --
You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

# Task
You are the INDEPENDENT GRADER for a proposed memory-core change. Firstmate has already run the
mechanical verifier and this rubric's deterministic checks. Your job is the judgment half of the rubric,
in a fresh context with no memory of the dreamer's reasoning.

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

## Inputs - and only these
- The OLD generation: gen/1
- The NEW generation: gen/2
- The cited source files those generations reference.
You must NOT read the dreamer's chain of thought, its chat, or any conversation.

## Rubric
Approve only when every changed or added statement in the new core.md (and in changed notes) is a durable
abstraction that will still be true in a session that never heard of the task that produced it. Reject:
- tactical scraps (a bare task id event, a dated incident recap, "X timed out" with no generalisation);
- uncited claims (every claim must resolve to a real source file on disk);
- contradictions of standing rules (a new statement that reverses a standing preference or safety rule).

## Deliverable
Write your verdict to /tmp/fm-dream-home.zfXmAS/data/fm-grade-2/report.md: a clear APPROVE or REJECT, the evidence (file:line for each
flaw, or a note that no flaw was found), and a one-line recommendation. Do NOT write any memory file. Do NOT
point data/memory/HEAD anywhere. Do NOT address the captain.

# 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 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}" >> '/tmp/fm-dream-home.zfXmAS/state/fm-grade-2.status'`
   States: working, needs-decision, blocked, paused, done, failed.
   Each append wakes firstmate, so report sparingly: only phase changes a supervisor would act on.
5. If you hit the same obstacle twice, append `blocked: {why}` and stop.
6. If a decision belongs above you, append `needs-decision: {summary}` and stop.
7. Never stop, restart, or update the shared `no-mistakes` daemon.

# Definition of done
Append `done: APPROVE` or `done: REJECT` to the status file and stop.

DEMO HOME: /tmp/fm-dream-home.zfXmAS
Evidence: Idle trigger and grader verdicts (excerpt)
$ bin/fm-dreamer-watch.sh check
DREAM_DUE: not-due reason=no unconsumed drops and HEAD not stale
exit=1

$ bin/fm-memory-drop.sh fm-task-9 --claim Healthlog requires xvfb for playwright
drop: deposited candidate claims for fm-task-9 to data/memory/drop/fm-task-9.md (1 claim(s))

$ bin/fm-dreamer-watch.sh check
DREAM_DUE: due reason=unconsumed candidate files in data/memory/drop
exit=0

-- a live non-dreamer worker must block the dream --
$ bin/fm-dreamer-watch.sh check
DREAM_DUE: not-due reason=live non-dreamer worker(s): fm-task-live
exit=1

$ bin/fm-dreamer-grade.sh grade gen/0 gen/1
PASS grade: proposed generation passes the mechanical verifier
GRADE APPROVED: no contradiction with standing rules
exit=0

$ bin/fm-memory-publish.sh 1
VERIFICATION PASSED: all 4 safety checks passed
memory: published generation gen/1 to data/memory/HEAD

$ bin/fm-dreamer-grade.sh grade gen/1 gen/2
PASS grade: proposed generation passes the mechanical verifier
FAIL grade: new core contradicts a standing rule: - Rule One: never test changes
WARN grade: possible tactical scrap in changed note notes/scrap.md: On 2026-08-19 task fm-task-9 timed out.
GRADE REJECTED: 1 rubric violation(s) found
exit=1

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • 🚨 bin/fm-dreamer-grade.sh:294 - The rubric loop inspects EVERY line of the new core, not only changed or added statements (the gate at line 292 compares core file PATHS, which always differ between two generation dirs). Combined with contradicts_standing, a standing rule that the new generation preserved verbatim is matched against itself: keyword overlap is total and the line's own negation word ('never', 'do not', 'must not') trips the reversal test, so the rule is reported as contradicting itself. Reproduced: old core and new core both containing only '- always test changes; never skip the suite' -> 'PASS grade: proposed generation passes the mechanical verifier' followed by 'FAIL grade: new core contradicts a standing rule: - always test changes; never skip the suite' and 'GRADE REJECTED: 1 rubric violation(s)'. This is the normal case: bin/fm-memory-verify.sh check_constitution REQUIRES every standing bullet rule to survive into the new core, so the verifier mandates exactly the input the grader rejects. Fix at the loop boundary: skip any new-core statement that is present verbatim in the old core before running the scrap and contradiction checks, which also makes the code match its documented contract of inspecting 'every changed or newly added statement'.
  • 🚨 bin/fm-dreamer-grade.sh:196 - contradicts_standing lowercases the statement for keyword matching (line 187) but matches the negation words against the RAW $line (line 196). Every negation token in the list is lowercase, so a contradiction written in ordinary sentence case is never detected. Reproduced against a standing rule '- Rule One: always test changes': new core line '- Never test changes under any circumstance' -> 'GRADE APPROVED', while the identical line as '- never test changes under any circumstance' -> 'GRADE REJECTED'. The intent requires the rubric to reject contradictions with standing rules; a bullet starting with a capitalised 'Never' or 'Do not' is the most likely real form and it passes. Fix: compute the lowercased line once and match both the keywords and the negation tokens against it.
  • 🚨 bin/fm-dreamer-grade.sh:187 - contradicts_standing re-lowercases the same new-core statement inside the per-keyword loop, spawning two processes (printf | tr) for every (new-core line x old-core rule x keyword) triple, on top of a sed|tr|tr|awk pipeline per (line x rule) pair. The cost is quadratic in core size with a large process constant. Measured on this worktree: a core with 20 bullet rules took 19.9s real; a core with 80 bullet rules did not complete within a 120s timeout. A realistic constitution therefore makes grade appear hung, and it would also blow the when adapter's default --action-timeout if it were ever wired behind one. Fix: hoist the lowercased $line out of both loops (compute it once per statement) and reuse it; that alone removes the dominant term.
  • ⚠️ bin/fm-dreamer-watch.sh:149 - drop_has_candidates treats 'any *.md in data/memory/drop/' as 'unconsumed', but nothing in the repo ever removes, moves, or marks a drop file consumed: bin/fm-memory-drop.sh only writes into the tray, and the dreamer brief (bin/fm-brief.sh:405) tells the dreamer to 'Read, and only read' it. Concrete sequence: a task drops a claim -> check reports due -> a dream pass consumes it into gen/N -> firstmate publishes and re-arms the watch -> check immediately reports due again on the same already-consumed file -> mark-due fires and another dream is scheduled over nothing new. The stale state/.dream-due marker is likewise never cleared by any code path. Decide who clears the tray (the dreamer, firstmate after publish, or a dream cursor as the brief already alludes to) before this loop can be armed in production.
  • ⚠️ bin/fm-brief.sh:431 - The dreamer brief asserts to the agent as fact: 'A single-flight state/.dream.lock guarantees one dream at a time; never clear or force it.' No code in bin/, tests/, .agents/, or docs/ creates, acquires, checks, or releases state/.dream.lock - grep finds only this brief line and the test that asserts the line is present. The single-flight invariant the brief promises is therefore unenforced, which matters because the drop tray is never consumed (see drop-tray-never-consumed), so two dream passes over the same tray are reachable. Either implement the lock in this slice or reword the brief so it does not state a guarantee that does not exist.
  • ⚠️ bin/fm-dreamer-watch.sh:203 - The documented contract at lines 46-51 and 180-182 is fail-safe: 'A worker whose endpoint cannot be positively confirmed gone is treated as live', and 'only a supported backend whose target does not exist counts as dead'. The code violates that for an empty target: [ -n &#34;$target&#34; ] &amp;&amp; fm_backend_target_exists ... short-circuits to false, so a worker with a supported backend but no resolvable endpoint is silently counted as NOT live. fm_backend_target_of_meta (bin/fm-backend.sh) returns empty whenever the meta has no window= key (and, for orca, no terminal= either), which is what a partially written meta looks like - bin/fm-spawn.sh:777 writes a meta with a plain redirect rather than the tmp+mv used elsewhere. Failure: such a worker is running, check reports due, and a dream is dispatched while a real non-dreamer worker is live. Fix: treat an empty target as live, matching the stated invariant.
  • ⚠️ bin/fm-dreamer-grade.sh:318 - The intent requires 'a rubric that rejects tactical scraps, requires valid citations, and rejects contradictions with standing rules, and approves or rejects publication of proposed core modifications.' The implementation deliberately downgrades tactical scraps to a non-blocking WARN (lines 299-302) and still prints 'GRADE APPROVED' with exit 0 (line 318), delegating rejection to the human-judgment grader scout. That is a defensible design and is documented at lines 33-36, but as written a caller that gates publication on grade's exit code will publish a core containing a dated incident recap. Confirm whether the mechanical grade should hard-fail on scraps, or whether firstmate is required to also run the scout verdict before any pointer swap.
  • ℹ️ bin/fm-dreamer-grade.sh:25 - The header states the rubric inspects 'every changed or newly added statement in the new core.md (and in changed notes)', and the scaffolded grader-scout brief repeats it at line 231. The grade implementation only ever reads NEW_CORE (line 307); no note file is opened. Either drop the '(and in changed notes)' claim from both places or extend the loop over the generation's notes/*.md.
  • ℹ️ bin/fm-dreamer-watch.sh:243 - The comment at lines 238-241 says the condition argv runs 'this script's check with the resolved home and threshold', but only --head-age is placed in CONDITION_ARGV; the home is not pinned and check falls back to ambient FM_HOME at line 59. In practice the runner inherits FM_HOME from the reconcile that owns the claim, so this works today, but it makes the registered spec less self-contained than the comment (and fm-procevent-when.sh's 'exact and deterministic argv' policy) claims. Either add an explicit home flag to check and pass it, or soften the comment to say the home comes from the runner's environment.
  • ℹ️ docs/scripts.md:128 - docs/scripts.md lists every bin script, including all five scripts added by memory slices 1 and 2 (fm-memory-compile/migrate/drop/verify/publish at lines 124-128). The two scripts added by this slice, bin/fm-dreamer-watch.sh and bin/fm-dreamer-grade.sh, are absent from that table, and no doc anywhere mentions fm-dreamer. Nothing enforces this mechanically, but it breaks the convention the sibling slices established.

🔧 Fix: fix grader diff boundary, negation case, cost, and watch liveness
9 issues (1 error, 6 warnings, 2 infos) still open:

  • 🚨 bin/fm-dreamer-grade.sh:182 - load_standing_rules loads EVERY line of the old core as a standing rule. It only strips a leading bullet marker; it never filters to bullet rules, so the mandatory &lt;!-- source: ... --&gt; citation comment (and any heading or prose line) becomes a pseudo-rule whose keywords hard-reject new statements. Reproduced on a real home: gen/0 core = # Core / &lt;!-- source: data/captain.md --&gt; / - Rule One: always test changes, gen/1 adds the legitimate durable rule - Never write a claim without a resolvable source in the data tree -> PASS grade: proposed generation passes the mechanical verifier then FAIL grade: new core contradicts a standing rule: ... and GRADE REJECTED: 1 rubric violation(s) found (exit 1). Deleting ONLY the &lt;!-- source: ... --&gt; line from gen/0 and re-running the identical input -> GRADE APPROVED (exit 0). The comment marker matched keywords source and data, and the new line's never tripped the reversal test. This is not an edge case: bin/fm-memory-verify.sh check_citations requires a non-empty core.md to cite a source, and &lt;!-- source: ... --&gt; is the convention used throughout tests/fm-memory-verify.test.sh, so essentially every real core carries it, and any new negative rule mentioning two of its tokens is falsely rejected. The script's own header (line 29) says the check is over 'every standing bullet rule in the old core', and bin/fm-memory-verify.sh:625 reads standing rules only from grep &#39;^[[:space:]]*[-*][[:space:]]&#39;. Fix at the load boundary: restrict load_standing_rules to that same bullet predicate so the grader and the verifier agree on what a standing rule is.
  • ⚠️ bin/fm-dreamer-grade.sh:258 - inspect_statements skips any line starting with a space or tab (&#39; &#39;*|$&#39;\t&#39;*), so an indented / nested markdown bullet is never inspected by the scrap check or the contradiction check. Reproduced against the same old core used by tests/fm-dreamer.test.sh:305: new core line - never test changes under any circumstance -> GRADE REJECTED, but the identical line written as a nested bullet - never test changes under any circumstance -> GRADE APPROVED (exit 0). Nested bullets are ordinary in a constitution, and bin/fm-memory-verify.sh treats them as standing rules (^[[:space:]]*[-*][[:space:]]), so this is a one-space bypass of the gate. The skip list is documented as covering 'headings, source markers, and note frontmatter'; indentation is not metadata. Fix: skip only headings, HTML comments, and frontmatter, and inspect indented bullets after stripping leading whitespace.
  • ⚠️ bin/fm-dreamer-watch.sh:220 - live_workers resolves every meta with fm_backend_of_meta / fm_backend_target_of_meta, which read the LOCAL backend= and window= keys. bin/fm-spawn.sh:627-645 writes a remote worker's meta with window=remote:$id, no backend= key, and the real endpoint in remote_host=/remote_backend=/remote_target=. fm_backend_of_meta therefore defaults to tmux and the target becomes the fabricated string remote:&lt;id&gt;, so the liveness probe is a local tmux lookup of a target that never exists locally. On a host where firstmate does not run a local tmux server that probe fails, the live remote secondmate is counted dead, and check reports due -> a dream is dispatched while a non-dreamer worker is live. That violates the fail-safe contract this file states at lines 46-51 and 205-210. bin/fm-fleet-snapshot.sh:424-431 - the fleet read this header claims to mirror - branches on remote_host first and uses remote_backend/remote_target for exactly this reason. Fix: apply the same remote_host branch here, and treat a remote meta whose endpoint cannot be positively confirmed gone as live.
  • ⚠️ bin/fm-dreamer-watch.sh:276 - arm pins --home &#34;$FM_HOME&#34; using the raw environment value, without applying the validation check --home enforces at lines 147-159. An absolute FM_HOME whose leaf is a symlink is accepted by every other firstmate script (bin/fm-brief.sh resolve_directory_input returns an absolute path unchanged; only a relative path gets pwd -P), but --home's [ ! -L &#34;$HOME_OPT&#34; ] guard rejects it. Reproduced: FM_HOME=&lt;symlink-to-home&gt; fm-dreamer-watch.sh arm --dry-run exits 0 and registers the spec, then the registered condition fm-dreamer-watch.sh check --head-age 12 --home &lt;symlink&gt; prints --home must name an existing directory and exits 2. Per bin/fm-procevent-when.sh, exit 2 is a condition ERROR, not a false, so the watch consumes its --error-budget and wakes firstmate with condition-error instead of ever evaluating the dream condition. Fix: resolve the home physically (cd -- &#34;$HOME_OPT&#34; &amp;&amp; pwd -P) for absolute inputs too rather than refusing a symlinked home, and run the same resolution/validation in arm so a bad home fails at registration time.
  • ⚠️ bin/fm-dreamer-watch.sh:211 - live_workers counts any state/*.meta whose endpoint exists, filtered only by the fm-dream- task-id prefix. A secondmate is persistent by design: its meta stays in state/ and its pane endpoint stays alive for the life of the home. So in any home running a secondmate, check always returns DREAM_DUE: not-due reason=live non-dreamer worker(s): &lt;secondmate-id&gt; and the idle trigger can never fire, no matter how many drops accumulate or how old HEAD gets. The intent's wording ('the fleet has no live non-dreamer workers') is satisfied literally, but the feature is silently disabled for those homes. bin/fm-fleet-snapshot.sh distinguishes kind=secondmate and reads fm_backend_agent_alive for it rather than treating endpoint existence as activity. Decide whether a persistent secondmate should be excluded like a dreamer, or gated on an actual busy read instead of endpoint existence, before this watch is armed in production.
  • ⚠️ bin/fm-dreamer-grade.sh:348 - The mechanical verify runs as &#34;${VERIFY_ARGS[@]}&#34; &gt;/dev/null 2&gt;&amp;1, so on failure the operator sees only FAIL grade: proposed generation fails the mechanical verifier with no indication of which of the four checks (budget, citations, constitution, diff bounds) failed or why. The same redirect also swallows the verifier's usage error, so an invalid --max-diff-ratio (which grade never validates itself, unlike bin/fm-memory-verify.sh:101-104) is reported to the user as a memory-safety failure rather than a bad flag. Fix: capture the verifier output and echo it to stderr on failure, and validate --max-diff-ratio locally before delegating.
  • ⚠️ bin/fm-brief.sh:422 - The dreamer brief instructs run \bin/fm-memory-verify.sh <N>` on the proposed generationas a bare relative path with no home pinned. The dreamer runs inside a disposable git worktree of the firstmate repo, and bin/fm-spawn.sh only injectsFM_HOME=into the launched pane whenKIND = secondmate(bin/fm-spawn.sh:2951-2968); a scout/ship pane is created by a long-lived tmux/herdr daemon that does not inherit firstmate&#39;s environment. So the invocation as written resolves FM_HOME to FM_ROOT, i.e. the worktree, and looks for<worktree>/data/memory/gen/<N> instead of the home&#39;s generation - the mandatory verification step in the contract cannot succeed as instructed. The same brief already interpolates absolute paths elsewhere ($DATA/$ID/report.mdat the Definition of done,$FM_ROOT/.agents/skills/...), so this is an internal inconsistency rather than a deliberate shorthand. Suggested wording: FM_HOME=$FM_HOME $FM_ROOT/bin/fm-memory-verify.sh <N>`.
  • ℹ️ bin/fm-dreamer-grade.sh:51 - --dry-run is documented as 'verify without publishing (passed to the verifier)', but grade never publishes anything, and bin/fm-memory-verify.sh accepts --dry-run only to suppress the HEAD swap in publish mode - in verify mode it is parsed and ignored (bin/fm-memory-verify.sh:83). The flag therefore has no observable effect on any code path. Either drop the option and its passthrough at lines 345-347, or reword the help so it does not imply grade would otherwise publish.
  • ℹ️ bin/fm-dreamer-grade.sh:284 - The grader scout brief embeds STATUS_FILE=&#34;$STATE/$TASK_ID.status&#34; raw into the status-reporting command (echo &#34;{state}: ...&#34; &gt;&gt; $STATUS_FILE at line 319). bin/fm-brief.sh:218 deliberately passes the same path through shell_quote before embedding it for exactly this reason, so a home path containing a space produces a broken command in the scaffolded brief. Apply the same quoting here.

🔧 Fix: scope standing rules, catch indented bullets, fix remote and symlinked homes
3 issues (1 warning, 2 infos) still open:

  • ⚠️ bin/fm-dreamer-grade.sh:229 - contradicts_standing still reads a standing rule as contradicting itself whenever the dreamer AMENDS its wording, because the round-2 fix only skips statements the old core carries VERBATIM (inspect_statements haystack, line 279). Reproduced in this worktree: gen/0 core = '- Rule One: never skip the test suite'; gen/1 core = '- Rule One: never skip the test suite before merging'. Output: 'PASS grade: proposed generation passes the mechanical verifier' then 'FAIL grade: new core contradicts a standing rule: - Rule One: never skip the test suite before merging' and 'GRADE REJECTED: 1 rubric violation(s) found' (exit 1). The amended line is not verbatim, so it is inspected; it shares >=2 keywords with its own predecessor and carries 'never', so the reversal test at lines 229-234 fires. bin/fm-memory-verify.sh check_constitution ACCEPTS the amendment (it needs only >=50% keyword overlap with some core block, bin/fm-memory-verify.sh:604-620), so the mechanical verifier mandates exactly the input the grader rejects - the same invariant round 1's grader-self-contradiction fix was meant to establish, patched only at the exact-equality boundary. This is the normal path, not an edge case: the dreamer brief at bin/fm-brief.sh:415 explicitly authorises 'a subset or inspect-then-update of the current core'. Second reproduction, same root cause with no amendment involved: gen/0 '- Rule One: always test changes' plus a new line '- Never merge changes that skip the test suite' (a rule that REINFORCES the standing one) -> 'contradicts a standing rule', exit 1. Fix at the contradicts_standing boundary rather than at the caller: skip a standing rule when the statement is that rule's successor (reusing the verifier's best-matching-block computation, which already owns 'is this the same rule'), or when the negation token the reversal test found is already present in the rule that was matched.
  • ℹ️ bin/fm-dreamer-grade.sh:167 - is_scrap_statement gates on *fm-[a-zA-Z0-9-]* AND one of ' failed' / ' timed out' / ' did not' / ' was ' / ' is '. ' is ' matches almost any declarative sentence, and 'fm-' matches every firstmate script name, so ordinary durable rules that name a script are reported as tactical scraps. Reproduced: new core line '- A ship brief is scaffolded with bin/fm-brief.sh before dispatch' -> 'WARN grade: possible tactical scrap in new core: ...' and the approval line reports '1 tactical-scrap candidate(s) surfaced for grader judgment'. This is non-blocking (exit 0), so it is not a merge risk, but this repo's own constitution names fm-*.sh scripts in most rules, so the warning stream handed to the grader scout would be mostly false positives and lose its signal. Narrowing the token test to a task-id shape (fm-<slug>-<digits>, which is what the comment at lines 163-166 actually describes) would keep the intended catch without the noise.
  • ℹ️ bin/fm-dreamer-watch.sh:265 - check guards a symlinked drop tray (line 185) and a symlinked HEAD (line 200), and the comment at lines 182-183 claims consistency with 'the memory guards', but nothing guards a symlinked data/memory itself. [ ! -d &#34;$MEMORY&#34; ] at line 265 follows the link, and neither $MEMORY/drop nor $MEMORY/HEAD is then a symlink, so both per-file guards are bypassed in one step: check reports 'DREAM_DUE: due' from whatever tray the link resolves to, and mark-due writes state/.dream-due for this home on another home's evidence. bin/fm-memory-verify.sh:108 refuses this exact shape ('data/memory is a symlink; refusing to verify through it'). Add the same [ ! -L &#34;$MEMORY&#34; ] refusal before evaluating, reporting not-due with a reason so the watch still does not hard-error.

🔧 Fix: refuse dream evaluation through a symlinked data/memory
2 warnings still open:

  • ⚠️ bin/fm-dreamer-watch.sh:162 - arm pins the resolved home into both registered argv vectors, but the watch REGISTRATION itself still lands in whatever home the exec'd bin/fm-procevent-when.sh resolves from the environment. Line 162 (and line 305) assign FM_HOME=$(resolve_home ...) as a plain shell variable; the assignment only stays visible to the exec at line 323 when FM_HOME was already exported into this process. Reproduced in this worktree: env -u FM_HOME bin/fm-dreamer-watch.sh arm --home /tmp/.../pinned printed armed: when-dream-due, left /tmp/.../pinned/state empty, and wrote state/when/when-dream-due.spec, state/when/when-dream-due.trust, and state/procevent/when-dream-due.source into FM_ROOT instead. The same command with FM_HOME=/tmp/.../ambient exported and --home /tmp/.../pinned correctly registered under the pinned home, so the destination silently depends on whether FM_HOME happened to be exported. The consequence is a watch whose condition/action argv evaluate home X while the spec, trust, fired, and procevent source records live in home Y: fm-procevent.sh reconcile in X never runs it, and Y's reconcile owns a watch for a home it does not manage. The header at lines 39-42 presents --home as the way to pin the home the watch belongs to, and mark-due --home already writes into the pinned home, so this is an internal inconsistency rather than a documented limitation. Fix: export FM_HOME after resolution (both at line 162 and at line 305) so the exec'd registrar inherits the pinned home.
  • ⚠️ bin/fm-test-run.sh:1198 - This change adds a --dreamer code path to bin/fm-brief.sh whose ONLY behavioral coverage is tests/fm-dreamer.test.sh, and it registers that test in the session-bootstrap family (line 411) plus bin/fm-dreamer* in the session-bootstrap changed-path arm (line 1142). It does not touch the bin/fm-brief.sh arm at line 1198, which still emits only pure-contract-unit. Verified against the real runner: bin/fm-test-run.sh --list --family pure-contract-unit does not list tests/fm-dreamer.test.sh, while --list --family session-bootstrap does; and families_for_changed_path matches bin/fm-brief.sh on the exclusive case arm at 1198, so the families_for_test_reference fallback at line 1256 (which would have found the reference) is never reached. Net effect: bin/fm-test-run.sh --changed after any future edit to bin/fm-brief.sh - the KIND dispatch, the --mode refusal, the shared HERDR_SECTION, the heredoc - runs none of the tests that cover the dreamer brief contract. The map's own comment at line 1072 states the invariant this breaks: 'Conservative path -> family map. Over-selects rather than under-selects.' Full CI lanes still run everything, so this is a selection gap, not a CI hole. Fix: add printf &#39;%s\n&#39; session-bootstrap to the bin/fm-brief.sh case arm, as other multi-family arms already do.

🔧 Fix: export pinned home for arm, select dreamer tests on brief change
3 issues (1 warning, 2 infos) still open:

  • ⚠️ bin/fm-dreamer-grade.sh:337 - The grader scout brief hardcodes the pause state verb as the literal paused, while every scaffold in bin/fm-brief.sh interpolates $PAUSED_VERB (bin/fm-brief.sh:87, used at lines 278, 361, 449, 571). bin/fm-classify-lib.sh:55-62 states the invariant explicitly: FM_CLASSIFY_PAUSED_VERB_DEFAULT is "the ONE definition of the verb; both the watcher and the daemon read it here (status_is_paused) rather than hardcoding the literal, so the vocabulary cannot drift between the two consumers", and FM_CLASSIFY_PAUSED_VERB overrides it. Reproduced with the real scripts: with FM_CLASSIFY_PAUSED_VERB=waiting, bin/fm-brief.sh s1 firstmate --scout emits States: working, needs-decision, blocked, waiting, done, failed. while bin/fm-dreamer-grade.sh scout g1 firstmate gen/0 gen/1 emits States: working, needs-decision, blocked, paused, done, failed.. Failure path: in a home that configures a different verb, an idle grader scout follows its brief and appends paused: &lt;reason&gt;; bin/fm-classify-lib.sh:131 status_is_paused compares the verb against the configured value and returns false, so the deliberate external wait is not absorbed by the stale path and the idle pane is escalated as a possible wedge instead. Fix: read the same value the brief scaffolder does (FM_CLASSIFY_PAUSED_VERB with FM_CLASSIFY_PAUSED_VERB_DEFAULT from bin/fm-classify-lib.sh) and interpolate it, rather than embedding the literal.
  • ℹ️ bin/fm-dreamer-grade.sh:340 - The grader scout brief's rule 6 tells the agent to append needs-decision: {summary} and stop, but unlike every scaffold in bin/fm-brief.sh it omits the paragraph that follows there: "A decision or blocker you opened stays open until a resolved line carrying its exact key lands; a later done: or working: line never closes it" plus the instruction to append resolved: itself when a wait clears without a firstmate reply (bin/fm-brief.sh:369-373 for the scout kind, and the same text in the new dreamer kind at bin/fm-brief.sh:463-467). It also omits the decision-hold-lifecycle completion gate that the ship, scout, and dreamer briefs all carry (bin/fm-brief.sh:382, 472, and the new dreamer block), which .agents/skills/firstmate-orca/SKILL.md:78 names as the precondition for tearing scout work down. Consequence: a grader scout that opens a hold and later appends done: APPROVE per its own Definition of done leaves a hold that no resolved line ever closes, and its teardown skips the gate the rest of the fleet applies. Confirm whether the independent grader is deliberately exempt from the shared hold lifecycle, or whether the two paragraphs should be carried over.
  • ℹ️ bin/fm-brief.sh:171 - This change makes --dreamer accept --herdr-lab (the dreamer block interpolates $HERDR_SECTION, and the usage header added at line 17 says "It may be combined with --herdr-lab"), and tests/fm-dreamer.test.sh:97-105 asserts that combination works. The refusal message for a secondmate charter still reads "--herdr-lab applies only to crewmate ship or scout briefs", so the one place that enumerates the valid kinds now omits dreamer. The neighbouring --mode refusal at line 164 was updated in this same change to mention the dreamer; this one was not. Fix: extend the message to "ship, scout, or dreamer briefs".

🔧 Fix: read configured pause verb in grader scout brief
2 infos still open:

  • ℹ️ bin/fm-dreamer-watch.sh:153 - The --head-age and --interval guards use the predicate &#39;&#39;|*[!0-9.]*, which accepts any arrangement of dots: ., .., and 1.2.3 all pass. HEAD_AGE_HOURS is then fed to awk -v h=&#34;$HEAD_AGE_HOURS&#34; &#39;BEGIN { printf &#34;%.0f&#34;, h * 3600 }&#39; at line 215, where awk silently coerces . to 0 and 1.2.3 to 1.2, so a malformed threshold degrades fail-open to "almost always stale" instead of being reported as a usage error. Reproduced in this worktree against a home whose data/memory/HEAD was aged 30 hours: check --head-age . -> DREAM_DUE: due reason=data/memory/HEAD older than . hours (exit 0), and check --head-age 1.2.3 -> DREAM_DUE: due reason=data/memory/HEAD older than 1.2.3 hours (exit 0), while check --head-age 999 correctly reports not-due. That matters because arm copies the value verbatim into the registered condition argv (line 315): a typo at registration time silently arms a watch whose condition is true on every poll, so it fires, writes state/.dream-due, and wakes firstmate regardless of real HEAD age. The same predicate lets --interval . pass this script's validation and then die at the exec with the registrar's generic error: --interval needs a positive number of seconds and exit 1, rather than this script's own exit-2 usage error. Reproduced: arm --interval . --dry-run exits 0 and prints the spec, while arm --interval . exits 1 with the registrar's message. Every other numeric flag in this change validates tightly (--stable is integer-checked at line 159, and bin/fm-dreamer-grade.sh:360-364 range-checks --max-diff-ratio). Fix: require a single optional dot with at least one digit (for example reject any value that does not match ^[0-9]+(\.[0-9]+)?$) for both flags, and mirror the registrar's positive-number rule so arm refuses a bad interval itself.
  • ℹ️ bin/fm-dreamer-watch.sh:103 - INTERVAL=${FM_DREAM_WATCH_INTERVAL:-3600} introduces an environment override that no header line or --help output mentions. The sibling override on the previous line is documented: the OPTIONS block at lines 43-44 reads --head-age &lt;hours&gt; HEAD age threshold (default: FM_DREAM_HEAD_AGE_HOURS or 12), while line 45 reads only --interval &lt;secs&gt; arm only: when poll cadence (default 3600). Since usage() at line 80 renders the header verbatim as --help, the poll-cadence override is undiscoverable from the script's own documented interface, and firstmate-coding-guidelines tier 7 makes the script header plus its --help output the single owner of exact flags and knobs. Fix: extend the --interval line to (default: FM_DREAM_WATCH_INTERVAL or 3600), matching the --head-age entry directly above it.
✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-dreamer.test.sh (35/35 ok, exit=0)
  • bin/fm-test-run.sh tests/fm-brief.test.sh tests/fm-test-run.test.sh tests/fm-memory-verify.test.sh (3/3 scripts pass)
  • Manual: bin/fm-dreamer-watch.sh check on an empty home (not-due), after bin/fm-memory-drop.sh fm-task-9 --claim ... (due), with a real live tmux worker registered in state/*.meta (blocked), and with --head-age 12 against a 30-hour-old data/memory/HEAD (due)
  • Manual: bin/fm-dreamer-watch.sh arm --dry-run and bin/fm-dreamer-watch.sh mark-due drop-tray (state/.dream-due written)
  • Manual: bin/fm-brief.sh fm-dream-1 firstmate --dreamer and inspection of the emitted data/fm-dream-1/brief.md contract prompt
  • Manual: bin/fm-memory-verify.sh 1 -> bin/fm-dreamer-grade.sh grade gen/0 gen/1 (GRADE APPROVED) -> bin/fm-memory-publish.sh 1 -> bin/fm-memory-compile.sh compile showing the promoted note in the compiled startup memory
  • Manual reject paths: bin/fm-dreamer-grade.sh grade gen/1 gen/2 (contradiction FAIL + tactical-scrap WARN, exit=1, HEAD unchanged) and bin/fm-dreamer-grade.sh grade gen/1 gen/3 (mechanical citation failure, exit=1)
  • Manual: bin/fm-dreamer-grade.sh scout fm-grade-2 firstmate gen/1 gen/2 and inspection of the emitted fresh-context grader brief
⚠️ **Document** - 1 info
  • ℹ️ docs/architecture.md:311 - docs/architecture.md "Operational memory routing" covers /stow routing only and does not describe the dream loop's architectural boundary (why the dreamer cannot grade its own core rewrite). I left that safety rationale in bin/fm-dreamer-grade.sh's "WHY THIS EXISTS" header and routed the operator-facing lifecycle to docs/configuration.md's "Compiled working memory" section, which slices 1 and 2 already established as the owner. Adding an architecture-level narrative for the whole memory-generation mechanism is a separate consolidation worth a follow-up once the design's remaining slices land, not part of this change.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Slice 3 of the adopted memory architecture. Adds the ephemeral dreamer scout
brief (fm-brief.sh --dreamer), the idle dream evaluation/arm helper
(fm-dreamer-watch.sh), and the independent grader rubric helper
(fm-dreamer-grade.sh) plus a comprehensive behavioral test suite.

The dreamer brief enforces the hard safety contract: never take the session
lock, never edit published memory in place, never address the captain, and
always pass the mechanical verifier before reporting done. The idle watch
reports due when the fleet has no live non-dreamer worker and either the drop
tray holds an unconsumed candidate or data/memory/HEAD is stale, and can arm
the deterministic when watch that wakes firstmate. The grader runs the
mechanical verifier plus a rubric that surfaces tactical scraps and rejects
contradictions of standing rules, and scaffolds a fresh-context grader scout
for the judgment half.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant