feat(bin): inject the plain-English reply rule before every captain-facing response - #2781
feat(bin): inject the plain-English reply rule before every captain-facing response#2781EternalTime wants to merge 32 commits into
Conversation
…em back in the folder that holds the project
…local-only landing
…ge range, document timer
Land the captain's edits: refreshed nautical-seasoning examples, a pointer to ~/VOICE.md when writing under his identity, a new general-guidelines section for all crewmates, and a plain-language rule in section 9. Give the sentence-per-line, plain-dash, and no-agent-co-author rules a single owner in that new section, replacing the copies in the coding guidelines skill, CONTRIBUTING.md, and section 1 with cross-references.
…line, brief guidelines
…resent A watcher close publishes a downtime episode unconditionally, and resurface_after_downtime woke firstmate for any pending episode without testing whether anything actually needed resurfacing. With a torn-down fleet that produced a self-sustaining loop: each re-arm closed the previous watcher, which republished downtime, so the next successor delivered another "check: rearm-resurface" whose handling turn found an empty queue, no work under way and nothing armed. Gate the wake on the three sources docs/watcher-continuity.md says the recovery presentation contains - unacknowledged queue rows, still-open decisions, and unread informational status lines - and retire an episode proved empty so later arms cannot re-fire it. The emptiness re-check and the generation-bound acknowledgement share the queue lock fm_wake_append holds, so a wake racing that decision keeps its episode, and any failure to read a source is treated as work present. Delivering the wake still does not retire the episode. That is the durability guarantee behind release-lock-existing: the wake is delivered but not yet handled, and preserving it is what lets an interrupted handling turn re-drain the same work. The decision fold uses the whole-file scan rather than its incremental sibling, which writes a cursor the unread-status scan falls back to and would otherwise let this check blind the drain it protects. Three existing cases asserted the recovery wake on an empty fleet as proof that lock or marker evidence survived. They now hold a real open decision, so they still prove evidence retention while exercising resurfacing rather than the noise.
Confidence Score: 2/5The PR is not yet safe to merge because workers can still exceed the captain's authorized scope, local origins can still be misclassified, and damaged remote-backed pools can launch from stale code. The current guidelines still authorize unrelated changes, Files Needing Attention: AGENTS.md, bin/fm-brief.sh, bin/fm-project-origin-lib.sh, bin/fm-spawn.sh Reviews (5): Last reviewed commit: "no-mistakes(document): document plaineng..." | Re-trigger Greptile |
5b241e7 to
631a596
Compare
|
Speaking as Kun's firstmate: The intended change — a prompt-submit hook that injects the compressed reply-shape reminder, plus VISION:
Security: no on the hook itself (drains stdin, never nonzero, scoped to a genuine primary home). This branch is not the isolated hook. HEAD is 24 ahead of origin/main and bundles local-only landing, Fork CI approved (runs 32555452864 CI, 32555452863 Require no-mistakes). no-mistakes/CI have not finished. Greptile is green on the current head; that does not make the bundle landable. This is waiting on the author (strip the unrelated commits, rebase onto origin/main) and, separately, on a captain decision for default-on vs opt-in. It is not otherwise ready, so this is not a captain-flag-now. |
|
Speaking as Kun's firstmate: This injects a plain-English reply rule before every captain-facing turn. That is default-behavior: it changes what every session writes without an enable flag. The captain's gitignored preference is not an explicit grant on the shared surface. VISION: a buried ask is a real honesty problem, but a structural inject on every turn is a new default, not an option. Do not land without a captain decision, and only after CI including no-mistakes is green. Require no-mistakes FAILED on this HEAD. Also touches Waiting on the author for no-mistakes — not waiting on the captain yet. Will flag only if this later goes fully green and stays default-on. |
|
Speaking as Kun's firstmate: Follow-up after fork CI started: no-mistakes is red (blocking). Structured This is waiting on the author, not the captain. |
The plain-English discipline lived only in AGENTS.md prose and a gitignored preference file, so it faded as a session filled up and questions got buried under the reasoning that led to them. A prompt-submission hook now puts one compressed line of the rule in front of the model on the turn that will use it, and the new captain-invocable /plainenglish skill owns the full contract, the reasoning, the standing escalation exception, and the off switch. Mechanism notes: - No harness fires a hook between composing a reply and the captain reading it, so nothing can inspect or rewrite a reply. Prompt submission is the last event upstream of composition whose stdout reaches that turn's context. - Claude blocks and erases the prompt when such a hook exits 2, and bash itself exits 2 on a syntax error, so the registration pins its own exit to 0. A broken, half-installed, or missing hook loses a reminder and nothing else. - config/plainenglish=off switches the reminder off per home; absence means on. Live evidence against Claude 2.1.238 is recorded in docs/verification/supervision.md and refreshed by tests/fm-plainenglish-live-e2e.test.sh; tests/fm-plainenglish-hook.test.sh pins the scoping, the off switch, and the exit contract with no harness. Also stabilizes the Pi calm follow-up case, which counted rendered captain answers from a pane captured mid-redraw and failed about half its runs at base.
…k-guard inventories
7a2ddf9 to
cb4996a
Compare
| If something clearly looks off, even if it is not directly related to what you are doing, try to get it fixed along the way. | ||
| Apply that same high standard to engineering excellence: lint, test failures, and test flakiness. | ||
| If you see one, even if it is not caused by what you are working on right now, still get it fixed. |
There was a problem hiding this comment.
Unrelated fixes exceed task authority
When a worker encounters an unrelated UI defect, lint failure, test failure, or flaky test, these guidelines direct it to fix the issue without the captain's explicit opt-in, causing unauthorized project changes outside the original request.
Context Used: If there is a VISION.md file at the root of the re... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| *:*) | ||
| prefix=${url%%:*} | ||
| case $prefix in | ||
| */*) return 0 ;; | ||
| *) return 1 ;; | ||
| esac |
There was a problem hiding this comment.
| else | ||
| default=$(default_branch "$worktree") || { | ||
| echo "error: could not determine the default branch of pooled worktree '$worktree', which has no origin to consult; refusing to launch from an arbitrary base" >&2 | ||
| return 1 | ||
| } | ||
| target="refs/heads/$default" |
There was a problem hiding this comment.
Missing origin bypasses freshness
If a remote-backed pooled checkout loses or corrupts its origin configuration while retaining a stale local default branch, this fallback treats that branch as authoritative without checking the registered project mode, causing a worker to launch from stale or locally diverged code instead of refusing the damaged pool.
Intent
Make firstmate's plain-English discipline structural rather than remembered, by injecting it before every response.
The captain asked for: a new skill called /plainenglish that fires through a hook before any of firstmate's responses. The intent is exact; the mechanism needed adjusting and that adjustment was requested and verified: a skill is loaded on demand and cannot gate what firstmate writes, and no hook event fires between firstmate deciding on prose and the captain reading it, so nothing can intercept or rewrite a response. What works instead is injecting the rule into every turn BEFORE firstmate composes anything. This was verified against the installed harness (Claude 2.1.238): UserPromptSubmit fires when the captain sends a message and its stdout is added to that turn's context. That reading was confirmed live rather than assumed.
The rule being enforced, recorded in the captain's (gitignored) preferences after he raised it twice: 'One paragraph, two sentences. Not two sentences per paragraph, and not a summary followed by detail.' The failure it prevents is a buried ask, not verbosity: leading with reasoning means he must read everything to find what he is being asked. So the rule is lead with the ask or the answer, cut supporting clauses, and put evidence, options and tradeoffs in a file or task note he can open rather than in the message. Because data/captain.md is gitignored and cannot be read by anything shipped, the rule's shared statement lives in the skill.
Accepted design decisions a reviewer reading only the diff would not know:
Acceptance criteria, all required: the reminder demonstrably arrives on a captain turn shown by evidence rather than asserted; a deliberately broken hook does not block the session, proven; /plainenglish loads and states the full contract including the escalation exception; the trigger is declared in AGENTS.md; the off switch works and is documented; one full sentence per line in Markdown, plain dashes, no em dash anywhere; bin/*.sh passes shellcheck and bin/fm-lint.sh.
Evidence produced: tests/fm-plainenglish-live-e2e.test.sh is an opt-in live guard (FM_PLAINENGLISH_LIVE_E2E=1) that ran green against Claude 2.1.238 - the model quoted the delivered line back on a real turn, returned NONE with the switch off, and still answered normally when the hook was replaced by an unparseable script. tests/fm-plainenglish-hook.test.sh pins the same properties portably with no harness, including running the REAL tracked settings command string against a sabotaged script. Per this repo's harness-dependent-check rule both a portable regression and a live opt-in guard are required, and the dated result is recorded in docs/verification/supervision.md.
One in-scope extra, under this repo's standing instruction to fix test flakiness encountered along the way: tests/fm-calm-pi-extension.test.sh failed about half its runs at the base commit, unrelated to this work. It counted rendered captain answers from a tmux pane captured while Pi was redrawing, so the pane showed zero. It now waits for the answer, settles, then asserts exactly one, which also keeps a late duplicate render inside the assertion. Verified 4/4 green at base with the fix and 3/3 on the branch, against 3/3 red before.
Constraint honored: bin/fm-watch.sh and its tests were not touched, because another worker is concurrently changing them.
What Changed
bin/fm-plainenglish-hook.sh, registered as a grok-guardedUserPromptSubmitentry in.claude/settings.jsonthat pins its own exit to 0, so a one-line reply-shape reminder (one paragraph, two sentences, escalations still carry their evidence) lands in context before Firstmate composes anything. The hook is scoped to a genuine primary home throughbin/fm-primary-scope-lib.sh, stays quiet in away mode, andconfig/plainenglish=offswitches it off. The captain-invocable/plainenglishskill owns the full contract and the off switch, its load trigger is declared inline in AGENTS.md section 9, and both a portable regression suite and an opt-in live guard (FM_PLAINENGLISH_LIVE_E2E=1) cover delivery, the off switch, and a deliberately sabotaged hook not blocking a turn./updatethecaptainworker report loop with its/updatethecaptain-stopcompanion andbin/fm-captain-report-timer.sh, plus fleet-wide crew guidelines in AGENTS.md, split scout brief guidelines with an AGENTS.md drift guard inbin/fm-brief.sh, and matching README,docs/scripts.md, anddocs/configuration.mdinventory entries.bin/fm-project-origin-lib.shfor origin classification, reworkedbin/fm-merge-local.sh,bin/fm-spawn.sh, andbin/fm-home-seed.shwith guards for ignored-path collisions and relative origins, and an origin-less clone tolerated when freshening a spawn base. Also fixed a pre-existing tmux render race intests/fm-calm-pi-extension.test.sh, which now waits for the answer and settles before asserting exactly one.Risk Assessment
✅ Low: All three round-1 findings are fully and correctly applied, the away-mode gate provably matches the daemon's own injection predicate so no unmarked line can ride along with a marked injection, and the two remaining items are one-line documentation-accuracy corrections with no behavioural effect.
Testing
I ran no baseline commands beyond my own selection pass, then exercised the intent the way the captain would experience it. The strongest evidence is a real Claude 2.1.238 transcript where the identical captain question is answered twice through the real hook and the real tracked registration: with the reminder injected the reply is a single two-sentence paragraph leading with the ask, and with config/plainenglish=off the same question produces a multi-paragraph answer whose ask lands at the very bottom. The opt-in live guard confirmed all three vendor-dependent properties on real turns (line delivered into context and quoted back verbatim, NONE returned with the switch off, and a syntax-broken hook leaving the turn answering), /plainenglish loaded in a real turn and stated the contract with the escalation and /updatethecaptain exceptions intact, and the hook stayed silent and exit-0 in this actual gate worktree. The portable regression passed 8/8 including the sabotaged-script cases against the real settings command string, and the documentation, AGENTS.md and gate-refuse suites passed. The in-scope Pi flake fix was 3/3 green; I could not reproduce the pre-fix failure in 3 runs of the base-commit copy on this machine, so I verified the fixed test is stable rather than independently confirming the old failure rate. No screenshot applies: the change is a shell hook plus Markdown, and its only end-user surface is the terminal turn, so CLI transcripts are the rendered surface and are attached.
Evidence: Real captain turn: same question with the reminder on vs off
### Captain turn WITH the reminder injected (config/plainenglish absent = on) captain> Two of the workers finished and one is blocked waiting on a decision about whether to use the shared cache or a per-project one. Tell me where things stand. firstmate> I have no workers running or recorded in this session -state/is empty and the only peer is an unrelated interactive session, so I can't confirm the two completions or the cache decision you're describing. Point me at where those workers are tracked (or re-launch them here) and I'll give you the real status. ### Same captain turn with the reminder switched OFF (config/plainenglish=off) captain> Two of the workers finished and one is blocked waiting on a decision about whether to use the shared cache or a per-project one. Tell me where things stand. firstmate> There are no workers to report on - the premise doesn't match this session. What I actually see: -ListAgents: no subagents at all. The only entry is one peer interactive session started ~4 min ago, which is this session itself. Nothing was spawned, so nothing finished and nothing is blocked. - Repo state: a fresh repo, single commit771c46d init, with untracked.claude/,bin/,config/. Thestate/directory is empty - no run records, no checkpoints, no worker output. - Session age: this session started minutes ago and my first tool calls in it were the ones just now. So I have no shared-cache-vs-per-project decision pending in front of me, and no two finished results to summarize. Most likely one of these: 1. The work is in a different session ... Point me at it and I'll pick it up. 2. You want me to start that work now - in which case tell me the task and I'll spawn the workers. I'd rather ask than invent a status report. Which is it?Evidence: Live e2e against Claude 2.1.238 (delivery, off switch, broken hook)
ok - claude 2.1.238 (Claude Code): the tracked registration delivers the reminder into a real turn's context # delivered line: [plain-english] Answer the captain in one paragraph of at most two sentences that leads with the ask or the answer, keeping evidence, options, and detail in a file or task note rather than in the message. An escalation keeps that shape while still leading with the evidence and consequence that let it stand alone, an /updatethecaptain worker report keeps its own per-worker format, and the plainenglish skill owns the full contract. ok - claude 2.1.238 (Claude Code): config/plainenglish=off leaves the turn with no reminder in context ok - claude 2.1.238 (Claude Code): a syntax-broken reminder hook leaves the turn runningEvidence: /plainenglish invoked on a real Claude turn
$ claude -p "/plainenglish" Every message to you is one paragraph of at most two sentences, leading with the ask or the answer, with evidence, options, and reasoning pushed into a file or review surface that the message points at. The only exceptions are an escalation, which may spend one extra sentence when it genuinely cannot be acted on without that fact, and a/updatethecaptainworker report, which keeps its own per-worker format.Evidence: Hook stays inert in this real no-mistakes gate worktree
$ # (1) this no-mistakes gate worktree, NO_MISTAKES_GATE=1 in the real env exit=0 output-bytes=0 $ # (2) same worktree with the gate flag cleared - still inert, it is a linked worktree, not a primary home exit=0 output-bytes=0Evidence: Pi flake-fix stability: 3 consecutive branch runs
run1 EXIT=0 oks=12 run2 EXIT=0 oks=12 run3 EXIT=0 oks=12 (full logs: calm-pi-run1.log, calm-pi-run2.log, calm-pi-run3.log; base-commit probe runs in calm-pi-BASE-run{1,2,3}.log)Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
.agents/skills/afk/SKILL.md- branch carries 23 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (121 file(s)) into the PR:Push main to origin, or rebase your branch onto origin/main, before gating.
.agents/skills/plainenglish/SKILL.md:33- Two skills landed on this branch now claim conflicting ownership of captain-message shape, with no reconciliation between them. plainenglish declares itself the owner of "the shape of a captain-facing message" (SKILL.md:13) with exactly one standing exception, the escalation (SKILL.md:31-35), and the hook injects that rule on every primary turn: "Answer the captain in one paragraph of at most two sentences ... an escalation keeps that shape" (bin/fm-plainenglish-hook.sh:68). But .agents/skills/updatethecaptain/SKILL.md:73-92 requires the opposite output: "Report every worker on the list. Never summarise the fleet in aggregate, never drop a worker for having nothing new, and never merge two workers into one entry," each with four labelled parts, plus a "Question from the<name>worker:" block above every status line. updatethecaptain:32-35 explicitly carves itself out of section 9's short-acknowledgement rule but cannot carve itself out of plainenglish, which did not exist when it was written; and the AGENTS.md section 9 trigger line reconciles plainenglish only with the escalation requirement. Concrete path: the captain invokes /updatethecaptain with three workers under way. That invocation is a captain prompt, so UserPromptSubmit fires and the two-sentence reminder is in context for the very turn that must emit the immediate first report (updatethecaptain step 3), and again on each timer-wake turn delivered into the pane. The two contracts point at different outputs and nothing in the tree says which wins. Recommend adding the four-part worker report to plainenglish's exception section (the one place that already owns exceptions), or having updatethecaptain state the override explicitly, rather than leaving it to per-turn judgment.README.md:180- README's "Firstmate ships these user-invocable built-in skills" table (README.md:173-180) lists 7 rows, but the repo has 8 skills withuser-invocable: true: afk, ahoy, bearings, plainenglish, stow, updatefirstmate, updatethecaptain, updatethecaptain-stop./plainenglishis the sole omission. It is not excluded bymetadata.internal: trueeither, since all 8 carry that identically. This same branch established the convention by adding the/updatethecaptainand/updatethecaptain-stoprows to that exact table. Nothing enforces this automatically (bin/fm-doc-audience-check.sh only checks classification and README setup-target routing), so the omission is silent: a captain reading the shipped README has no way to learn/plainenglishexists or that it is where the off switch is explained. Add a row for/plainenglishalongside the others.bin/fm-plainenglish-hook.sh:20- The header reasons about the operational-input marker in one direction only. It argues that marking the reminder "could strand a home in away mode" because away mode exits on the first unmarked captain message. The symmetric hazard is unaddressed: while away mode is active, the sub-supervisor daemon's markedaway-supervisorinjections are delivered into the primary's pane, which is a prompt submission, so UserPromptSubmit fires and this unmarked line is added to that same turn's context alongside the marked daemon message. AGENTS.md section 8's away-mode stub says "Any other unmarked message means the captain returned" and "Bias ambiguous input toward exit because a present captain takes precedence" - a rule that pushes toward exiting on ambiguity. The ahoy skill's exclusion list ("System, developer, tool, watcher, guard, away-mode, and other injected operational messages are not captain messages") covers this shape, but that rule lives in ahoy, not in the section 8 stub the model is following while away. Consequence if it fires is bounded and in the chosen-safe direction (a premature return to full responsiveness plus the catch-up gate, not a stranded home), and the live E2E does not cover an away-mode turn. Flagging for the author's judgment since the marking decision was deliberate; no code change recommended without their call.🔧 Fix: name updatethecaptain exception, silence reminder while away
2 infos still open:
docs/configuration.md:38- docs/configuration.md is the schema owner forconfig/plainenglishand still claims the reminder goes "into every primary turn" unconditionally. That became inaccurate this round: bin/fm-plainenglish-hook.sh:67 now exits silently whenever state/.afk exists, and .agents/skills/plainenglish/SKILL.md:50 records that suppression, but this section does not. Concrete path: a captain in away mode notices the reminder is gone and opens this section to check the switch. It tells him an absent config/plainenglish "means on" (line 41) and that the line goes into every primary turn (line 38), so the documented behaviour and the actual behaviour disagree with nothing here pointing at the away-mode gate. One qualifying clause plus a pointer at the skill is enough - the hook header should stay the single owner of the reasoning, per the one-owner rule.bin/fm-plainenglish-hook.sh:31- The new header sentence says "bin/fm-afk-return.sh is what clears the flag", but that script contains no clear: it delegates at bin/fm-afk-return.sh:151 tofm-afk-launch.sh stop, and the actualrm -f "$FM_AFK_LAUNCH_STATE/.afk"is at bin/fm-afk-launch.sh:615, deliberately ordered last after the daemon's shutdown flush. The daemon has its ownafk_exitclear path as well (bin/fm-supervise-daemon.sh:255-257). .agents/skills/afk/SKILL.md:222 already names the correct owner: "Always exit through bin/fm-afk-launch.sh stop, which keeps state/.afk present through the daemon's shutdown flush and clears it last." The conclusion the header draws from it is correct and unaffected - the flag is still present on the captain's returning turn either way - so only the attribution is wrong, and a maintainer following the pointer would land in a file with no clearing code. Namefm-afk-launch.sh stop(reached through the return owner) instead.✅ **Test** - passed
✅ No issues found.
bash tests/fm-plainenglish-hook.test.sh- 8/8, including running the real.claude/settings.jsonUserPromptSubmit command string against a syntax-broken script, an exit-2 script, and a missing scriptFM_PLAINENGLISH_LIVE_E2E=1 bash tests/fm-plainenglish-live-e2e.test.sh- 3 real Claude 2.1.238 turns: reminder delivered into model context,config/plainenglish=offreturned NONE, unparseable hook left the turn runningManual product check: same captain-shaped question asked twice throughclaude -pin a throwaway Firstmate-shaped lab carrying the real hook and real settings, once with the reminder on and once withconfig/plainenglish=off, to show the reply-shape difference an end user actually seesManual product check:claude -p "/plainenglish"in a lab with.claude/skills -> ../.agents/skills, confirming the skill loads and states the contract plus the escalation and/updatethecaptainexceptionsManual scope check: piped a UserPromptSubmit payload intobin/fm-plainenglish-hook.shinside this real no-mistakes gate worktree, with and withoutNO_MISTAKES_GATE, confirming silent exit 0 in bothbash tests/fm-calm-pi-extension.test.sh- 3 consecutive runs, 12/12 each, covering the in-scope flake fix3 runs of the base-commit copy oftests/fm-calm-pi-extension.test.shto probe the pre-fix racebash tests/fm-documentation-audiences.test.sh,bash tests/fm-ensure-agents-md.test.sh,bash tests/fm-supervision-instructions.test.sh,bash tests/fm-gate-refuse.test.sh- registration and gate-scoping coverage for the changed docs, AGENTS.md and shared libbash bin/fm-test-run.sh --list --changed --base a0cec26to pick the smallest relevant set rather than guessingContent check: scanned every added Markdown line in the diff for em dashes and for multi-sentence linesdocs/scripts.md:8- docs/scripts.md, the bin/ toolbelt inventory, is missing roughly 33 tracked bin scripts unrelated to this change (for example fm-lint.sh, fm-cd-pretool-check.sh, fm-sessionstart-cursor.sh, fm-turnend-guard-cursor.sh, the fm-remote-* family, and the fm-procevent-* family). This change's own script was added, but closing the wider gap is a separate consolidation pass and was deliberately left out of scope here.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.