Skip to content

feat(bin): merge upstream/main into fork, adding workflow linting and decision-hold bindings - #8

Merged
digbycampbell merged 13 commits into
mainfrom
fm/fm-upstream-sync
Aug 18, 2026
Merged

feat(bin): merge upstream/main into fork, adding workflow linting and decision-hold bindings#8
digbycampbell merged 13 commits into
mainfrom
fm/fm-upstream-sync

Conversation

@digbycampbell

Copy link
Copy Markdown
Owner

Intent

Sync this fork (digbycampbell/firstmate) with upstream (kunchenguid/firstmate) so pending charter fixes land on top of current upstream rather than under it. origin/main was 6 ahead and 11 behind upstream/main, with genuinely diverged histories.

MECHANIC: use a MERGE, not a literal rebase. The captain said 'rebase our fork' meaning 'bring it up to date'. Rebasing published history was explicitly ruled out: our main is already published and tracked by six worktrees and two secondmate homes, so rewriting it would strand all of them, and it contains a merge commit that rebase handles badly. The established precedent is our own f18ba40 'Merge remote-tracking branch upstream/main' landed as PR #6; this follows that shape: branch, merge upstream/main into it, resolve conflicts, PR.

SCOPE: merge only. Deliberately do NOT improve, reword, re-slim, or refactor anything while merging. Do not touch projects/, data/, state/, or config/. This is why the diff contains no independent improvements - that is intended, not an oversight.

CONFLICT RULES the captain set, in force:

  1. Our functional work is preserved and non-negotiable: bin/fm-procevent-slack-captain.sh and the whole Slack adapter, the decision-key parsing fix, and the real-application verification clause in the ship-brief scaffold. The Slack adapter currently carries every conversation between the captain and the user; losing it would cut that link mid-session. These files were verified byte-identical to origin/main after the merge.
  2. Where upstream and we edited the SAME charter prose and genuinely conflict, PREFER UPSTREAM WORDING - the captain trusts upstream more than our own edits.
  3. But never resolve by dropping a rule. AGENTS.md is a safety document; a resolution that quietly loses a 'never', an 'all', or a safety boundary is the exact failure being guarded against. Preferring upstream is about wording, never about losing a boundary.
  4. Count entries before and after on every touched file, and record every resolution in the PR body.

THREE CONFLICTS, all charter prose, no functional conflicts:
(a) .agents/skills/stow/SKILL.md description - genuine competing wording; took upstream's sentence verbatim (including its 'e.g.'), reflowed into our YAML block scalar only so the file keeps the repo's one-sentence-per-line Markdown style. A follow-up commit corrects an earlier resolution that had kept our dropped 'e.g.', to comply with rule 2.
(b) .agents/skills/process-event-sources/SKILL.md - NOT competing wording: both sides added different paragraphs at the same spot. Kept both, our Slack adapter paragraph plus upstream's new decision-hold binding rule verbatim.
(c) AGENTS.md section 2 layout block - the one place upstream was not taken wholesale, deliberately. Upstream made only two changes inside that block while our side had restructured the whole block in PR #1; taking upstream's block wholesale would not be a wording choice, it would revert our landed restructure. So upstream's two facts were applied into our structure instead: the 'CLAUDE.md is a real @AGENTS.md pointer' correction, and the new state/decision-bindings/ entry carrying its full semantics (written only by fm-decision-hold.sh bind, dropped by unbind or source retirement, plus the docs/decision-hold-lifecycle.md pointer). This is why AGENTS.md is 451 lines rather than the mechanical 449: upstream's one-line decision-bindings entry needed three lines in that block's column layout.

CLAUDE.md is intentionally no longer a symlink. Upstream commit 4913723 deliberately replaced it with a real two-line @AGENTS.md pointer because a Write through the symlink destroyed AGENTS.md, and CI (bdae21e) now enforces that shape. Our side never changed CLAUDE.md, so the merge correctly took upstream's file. .claude/skills remains a symlink to .agents/skills - that is a different, still-valid symlink.

VERIFICATION DONE: every touched file's line count is exactly ours + (upstream - base), and an automated sweep confirmed every line upstream added since the merge base is present in the result. Full suite showed 21 failing scripts; a control run of the same scripts on pre-merge origin/main in a throwaway clone reproduced 18 identically. The three that differ are all missing-tool environmental, not regressions: tests/fm-lint.test.sh and the new tests/fm-lint-workflows.test.sh both pass once upstream's pinned actionlint is installed (verified by installing it to a temp dir and re-running), and tests/fm-watch-checkpoint.test.sh passed 3/3 in isolation after being polluted in the full run by a watcher killed in the preceding script. bin/fm-lint.sh and bin/fm-doc-audience-check.sh are clean, and bin/fm-session-start.sh produces a full digest on the merged tree.

KNOWN PRE-EXISTING DEFECT, deliberately NOT fixed here because this is a merge-only change: tests/fm-procevent-slack-captain.test.sh already fails on origin/main - the poll leaves its /tmp/fm-slack-captain.* staging directory behind, containing the captured message payload. That is our own adapter's bug, present before this merge, and it is reported to the captain as a separate finding rather than folded into a merge.

What Changed

  • Merged upstream/main into the fork's main (merge, not rebase, since our published history is tracked by multiple worktrees and secondmate homes and contains a merge commit). This lands upstream's tooling and charter delta: new bin/fm-install-actionlint.sh and bin/fm-lint-workflows.sh with tests/fm-lint-workflows.test.sh, workflow linting wired into bin/fm-lint.sh and .github/workflows/ci.yml, decision-hold binding support in bin/fm-decision-hold.sh plus docs/decision-hold-lifecycle.md, and updates across bin/fm-send.sh, bin/fm-procevent*.sh, bin/fm-ensure-agents-md.sh, bin/fm-backlog-handoff.sh, bin/fm-x-link.sh, .pi/extensions/fm-calm.ts, and the .agents/skills/ charter docs.
  • CLAUDE.md changes from a symlink to a real two-line @AGENTS.md pointer, taking upstream's file (our side never modified it); CI enforces that shape. .claude/skills remains a symlink to .agents/skills.
  • Resolved three conflicts, all charter prose, no functional conflicts:
    • .agents/skills/stow/SKILL.md description — took upstream's sentence verbatim (including its e.g.), reflowed only to fit the YAML block scalar and the repo's one-sentence-per-line style; a follow-up commit corrects an earlier resolution that had dropped the e.g..
    • .agents/skills/process-event-sources/SKILL.md — both sides added different paragraphs at the same spot, so both were kept: our Slack adapter paragraph plus upstream's new decision-hold binding rule verbatim.
    • AGENTS.md section 2 layout block — our restructured block was kept and upstream's two facts applied into it (the "CLAUDE.md is a real @AGENTS.md pointer" correction and the new state/decision-bindings/ entry with its full semantics), rather than taking upstream's block wholesale, which would have reverted our landed restructure.
  • Merge-only by design: no independent improvements, rewording, or refactoring, and nothing under projects/, data/, state/, or config/ was touched. Our Slack adapter, the decision-key parsing fix, and the ship-brief real-application verification clause are byte-identical to origin/main after the merge.

Findings not fixed

  • tests/fm-procevent-slack-captain.test.sh fails on origin/main before this merge: the poll leaves its /tmp/fm-slack-captain.* staging directory behind, containing the captured message payload. Pre-existing defect in our own adapter, left out of a merge-only change and reported separately.

Risk Assessment

✅ Low: A pure merge with three prose-only conflict resolutions, each verified line-by-line against both parents: no upstream content lost, no our-side functional file altered, no safety rule dropped, and the only cross-side semantic couplings (fm-send --resolve-key refusal text, the new adapter answers seam) still hold.

Testing

Validated the merge as a merge: blob-identity proof that the protected functional work is untouched, an automated completeness sweep showing every upstream-added line landed, per-file line-count arithmetic, and a safety-boundary census on AGENTS.md showing no 'never'/'all'/boundary was lost. The stow resolution now matches upstream's wording exactly under YAML parse. Targeted suites over the merged surfaces (decision-hold lifecycle, decision-key classification, procevent, ensure-agents-md) all pass, the two lint suites pass once upstream's pinned actionlint is installed - confirming they are missing-tool environmental rather than regressions - and CI's repo-invariants checks pass locally, including the new CLAUDE.md real-pointer shape. bin/fm-session-start.sh produces a complete digest on the merged tree. The only failure is tests/fm-procevent-slack-captain.test.sh, which the author disclosed and which is provably pre-existing since that adapter's blobs are unchanged. No visual artifact applies: this change is charter prose, shell scripts, and CI config with no rendered end-user surface; the CLI transcripts are the end-user surface. Temp tooling and the throwaway home were removed and the worktree is clean.

Evidence: Merge verification: preservation, line-count arithmetic, safety-boundary census

Source: Merge verification: preservation, line-count arithmetic, safety-boundary census

# Upstream sync merge - verification transcript

merge-base=f1a4af426d7199c1781bc91ccd143b8e1f732d10  ours(origin/main)=6ed30063d20753bfb2f0072a704452d904037978  upstream=64d61aed84373e02b1a28c4e6b262908ed8128d5  merged=caa142503d26dd6ad01fb44096ef4092b0e0a49b

## Rule 1 - our functional work preserved byte-identical to origin/main
IDENTICAL  bin/fm-procevent-slack-captain.sh  (cfecb00c42fab04a678137b9d1f9a3aad4d66f65)
IDENTICAL  tests/fm-procevent-slack-captain.test.sh  (3598f8143d5bf5c8ff4beb5a1ba8e602f7f90143)
IDENTICAL  bin/fm-brief.sh  (2e8fafb47808fbdc2198b1cb18c252270365ad2d)
IDENTICAL  bin/fm-classify-lib.sh  (30f0fd027c30f66963d4dd85515997a69a3b4760)

## Rule 4 - line-count arithmetic: merged == ours + (upstream - base)
FILE                                                   base   ours  upstr   merged   expect verdict
.agents/skills/decision-hold-lifecycle/SKILL.md          42     42     49       49       49 OK
.agents/skills/firstmate-coding-guidelines/SKILL.md     126    125    127      126      126 OK
.agents/skills/fmx-respond/SKILL.md                     256    252    269      265      265 OK
.agents/skills/process-event-sources/SKILL.md           107    103    117      113      113 OK
.agents/skills/secondmate-provisioning/SKILL.md         249    248    252      251      251 OK
.agents/skills/stow/SKILL.md                            269    271    283      285      285 OK
.agents/skills/stuck-crewmate-recovery/SKILL.md          51     50     54       53       53 OK
.agents/skills/updatefirstmate/SKILL.md                  64     63     64       63       63 OK
.github/workflows/ci.yml                                389    389    417      417      417 OK
.no-mistakes.yaml                                        42     42     44       44       44 OK
.pi/extensions/fm-calm.ts                               468    468    495      495      495 OK
AGENTS.md                                               568    448    569      451      449 DELTA 2 (documented: AGENTS.md column layout needs 3 lines for upstream's 1-line entry)
CLAUDE.md                                                 0      0      2        2        2 OK
CONTRIBUTING.md                                         105    105    111      111      111 OK
README.md                                               228    228    228      228      228 OK
bin/fm-backlog-handoff.sh                               578    578    606      606      606 OK
bin/fm-decision-hold.sh                                 662    662    850      850      850 OK
bin/fm-ensure-agents-md.sh                              198    198    253      253      253 OK
bin/fm-ff-lib.sh                                        420    420    420      420      420 OK
bin/fm-install-actionlint.sh                              0      0     84       84       84 OK
bin/fm-install-shellcheck.sh                             36     36     84       84       84 OK
bin/fm-lint-workflows.sh                                  0      0    137      137      137 OK
bin/fm-lint.sh                                          541    541    561      561      561 OK
bin/fm-procevent-lavish.sh                              156    156    241      241      241 OK
bin/fm-procevent.sh                                     811    811    856      856      856 OK
bin/fm-send.sh                                          552    552    609      609      609 OK
bin/fm-test-run.sh                                     1717   1717   1719     1719     1719 OK
bin/fm-x-link.sh                                        178    178    234      234      234 OK
docs/architecture.md                                    339    339    342      342      342 OK
docs/calm-mode-feasibility.md                           439    439    502      502      502 OK
docs/configuration.md                                   637    652    646      661      661 OK
docs/decision-hold-lifecycle.md                         112    112    159      159      159 OK
docs/scripts.md                                         121    121    121      121      121 OK
docs/verification/process-event-sources.md              153    153    155      155      155 OK
tests/fm-backlog-handoff.test.sh                        554    554    649      649      649 OK
tests/fm-calm-pi-extension.test.sh                     3934   3934   3967     3967     3967 OK
tests/fm-decision-hold-lifecycle.test.sh                785    785   1133     1133     1133 OK
tests/fm-ensure-agents-md.test.sh                       211    211    371      371      371 OK
tests/fm-lint-workflows.test.sh                           0      0    530      530      530 OK
tests/fm-lint.test.sh                                   639    639    881      881      881 OK
tests/fm-procevent.test.sh                             1144   1144   1153     1153     1153 OK
tests/fm-x-mode.test.sh                                2899   2899   2971     2971     2971 OK

## Rule 3 - AGENTS.md safety-boundary census, origin/main vs merged
6ed3006: 'never'=60 'always'=3 ' must '=20 sections=16 layout-entries=10
caa1425: 'never'=61 'always'=3 ' must '=20 sections=16 layout-entries=10
(no boundary lost; 'never' count rises by 1 from upstream's /stow sentence)
Evidence: Upstream-line completeness sweep and the three conflict resolutions

Source: Upstream-line completeness sweep and the three conflict resolutions

# Upstream-line completeness sweep + conflict resolutions

## Every line upstream added since the merge base, present in the merge result
REFLOWED (not verbatim) in .agents/skills/stow/SKILL.md:
  description: Sweep the current session for uncaptured durable knowledge, file it to disk, persist the open work records this session knows are unfiled or now wrong, and curate the home's tiered, decaying startup memory before a context reset. Use when the captain invokes /stow (e.g. "/stow", "stow what you've learned"), before a session reset or context compaction, or periodically to keep operational memory current.
REFLOWED (not verbatim) in AGENTS.md:
    decision-bindings/ private bindings from a captured-answer source id to the captain-hold origin its keyed answers close; written only by bin/fm-decision-hold.sh bind, dropped by unbind and by source retirement (section 13; docs/decision-hold-lifecycle.md)
upstream-touched files: 42 ; non-verbatim lines: 2 (both are the two documented reflow resolutions)

## Resolution (a) stow SKILL.md description - YAML-parsed comparison of the real consumer contract
upstream : Sweep the current session for uncaptured durable knowledge, file it to disk, persist the open work records this session knows are unfiled or now wrong, and curate the home's tiered, decaying startup memory before a context reset. Use when the captain invokes /stow (e.g. "/stow", "stow what you've learned"), before a session reset or context compaction, or periodically to keep operational memory current.
merged   : Sweep the current session for uncaptured durable knowledge, file it to disk, persist the open work records this session knows are unfiled or now wrong, and curate the home's tiered, decaying startup memory before a context reset. Use when the captain invokes /stow (e.g. "/stow", "stow what you've learned"), before a session reset or context compaction, or periodically to keep operational memory current.
VERDICT  : IDENTICAL after YAML parse - only line wrapping differs (repo one-sentence-per-line style)

## Resolution (b) process-event-sources SKILL.md - both paragraphs kept
diff --git a/.agents/skills/process-event-sources/SKILL.md b/.agents/skills/process-event-sources/SKILL.md
index 3602c77..e5d6410 100644
--- a/.agents/skills/process-event-sources/SKILL.md
+++ b/.agents/skills/process-event-sources/SKILL.md
@@ -26,6 +26,16 @@ bin/fm-procevent-lavish.sh arm <artifact.html>
 
 A configured Slack captain channel is armed and handled through `bin/fm-procevent-slack-captain.sh`, whose header owns its configuration, token handling, and read-position rules; its `handle <source-id> <sequence> <result-file>` is the required handling command, because only it advances the channel read position along with the acknowledgement.
 
+When a source carries captain answers to decisions that already have durable holds, bind it to their origin BEFORE arming it, so it can never produce an answer that has nowhere to go:
+
+`` `sh
+bin/fm-decision-hold.sh bind <source-id> <origin-id>
+`` `
+
+The runner then passes each captured result to that source's own adapter `answers` command and pipes the keyed answers it prints into the one keyed-answer intake, which owns every rule about what they mean.
+This is generic: any adapter with an `answers` command works, and the runner still wakes you to act on the result.
+`decision-hold-lifecycle` owns when a binding is required and what the keys must be.
+
 A configured remote secondmate reply source is armed and handled through `bin/fm-procevent-remote-reply.sh`.
 Its header owns exact commands, while the adapter owns cursor continuity, validated deduplicated status ingest, path-confined document fetch, acknowledgement, and re-arming after a good delta.
 A continuity break is escalated once and stays unarmed until an operator deliberately rebases it.

## Resolution (c) AGENTS.md section 2 - upstream's two facts applied into our restructured block
diff --git a/AGENTS.md b/AGENTS.md
index 7da1bdd..2e33d3d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -47,7 +47,7 @@ Each secondmate has a persistent isolated `FM_HOME`, including its own state, ba
 `bin/fm-send.sh` fails closed unless `FM_HOME` is explicit, so a steer cannot silently resolve against another home.
 
 `` `
-AGENTS.md            this file (CLAUDE.md is a symlink to it)
+AGENTS.md            this file (CLAUDE.md is a real @AGENTS.md pointer to it)
 README.md CONTRIBUTING.md .github/workflows/ .tasks.toml bin/   committed shared surfaces;
                      .tasks.toml configures the default backlog backend (section 10), and each bin/
                      script's header is authoritative - read it before first use
@@ -69,6 +69,9 @@ state/               volatile runtime signals: <id>.status wake-event lines ("<s
                      <id>.herdr-presentation, a Herdr projection journal that is never task or endpoint
                      authority; procevent/, procevent-inbox/, and when/ condition->action watch specs,
                      whose registered sources alone keep supervision required (section 13);
+                     decision-bindings/, written only by bin/fm-decision-hold.sh bind and dropped by
+                     unbind or source retirement, binding a captured-answer source id to the
+                     captain-hold origin its keyed answers close (docs/decision-hold-lifecycle.md);
                      pending-replies/; generated Relay artifacts (section 14); PR merge-poll and
                      check-migration records; and dot-prefixed watcher, startup, guard,
                      presentation-cursor, and sub-supervisor internals - never touch those
@@ -167,7 +170,7 @@ Route durable knowledge to its most specific owner:
 Firstmate never writes a project's `AGENTS.md` directly.
 A crewmate creates or updates it lazily through the project's selected delivery path, using `bin/fm-ensure-agents-md.sh` and preferring pointers to authoritative sources over copied detail.
 Keep fleet delivery posture and captain-private strategy out of project memory.
-When the captain invokes `/stow`, load the `stow` skill for the complete knowledge-routing and unfinished-work sweep.
+When the captain invokes `/stow`, load the `stow` skill for its memory curation, knowledge routing, and persistence of the open work records this session is holding; it files and corrects only the open work that session is holding, and never reconciles the backlog against repository or PR reality.
 
 ## 7. Task lifecycle
 
Evidence: Targeted test run + CI repo-invariants checks on the merged tree

Source: Targeted test run + CI repo-invariants checks on the merged tree

# Targeted test + CI-invariant transcript on the merged tree (caa1425)

## CI 'Repo invariants' job, run locally
OK  CLAUDE.md is a real file, not a symlink
OK  CLAUDE.md is the canonical @AGENTS.md pointer
OK  .claude/skills -> ../.agents/skills (still a valid symlink)
OK  no personal fleet paths tracked

## tests/fm-decision-hold-lifecycle.test.sh
ok - resolved findings and decision-like prose do not create false holds
ok - terminal single-owner stale status decisions do not block empty inventory
ok - main-home and secondmate-home captain holds remain correctly routed
ok - resolve matches first/middle/last in quoted blocked_by and rejects a genuinely absent id
ok - a bound channel's captured answers close their captain holds at answer time
ok - a channel source with no decision binding closes nothing
ok - the answer path keeps every guard the unrouted close path already had
ok - the chat channel feeds the same keyed-answer intake a captured review does
  -> PASS

## tests/fm-classify-decision-key.test.sh
ok - a [key=x] mentioned mid-note is prose, never an opened or closed key
ok - a malformed stated key is rejected in both positions, never folded as default
ok - status_line_verb strips every bracket tag before the colon, in any order, and recovers the bare verb
ok - a [corr=...] tag ahead of [key=...] no longer swallows the verb: opens and closes under the stated key
ok - a [corr=...] tag with no stated key opens under 'default', exactly like a bare needs-decision line
ok - a [key=x] tag alone (no corr tag) still opens x - no regression from the tag-stripping fix
ok - blocked/resolved parse their bare verb with any bracket-tag order preceding the colon
ok - the incremental fold matches the full fold across appends in both key positions
  -> PASS

## tests/fm-procevent.test.sh
ok - oversized output is bounded rather than published whole or dropped
ok - live output stays bounded and retirement reaps the whole source group
ok - invalid output bounds fail closed
ok - the adapter derives physical identity without newline path corruption
ok - source-only homes trigger the general supervision guard
ok - the adapter classifies published poll output safely
ok - the adapter owns which Lavish results end a source, and payload text cannot forge one
ok - the published interfaces state the loss limitation and claim no lossless delivery
  -> PASS

## tests/fm-ensure-agents-md.test.sh
ok - fm-ensure-agents-md.sh: CRLF AGENTS.md with the section stays unchanged
ok - fm-ensure-agents-md.sh: CRLF injection preserves line endings idempotently
ok - fm-ensure-agents-md.sh: canonical real CLAUDE.md pointer is not a conflict
ok - fm-ensure-agents-md.sh: refuses distinct real AGENTS.md and CLAUDE.md
ok - fm-ensure-agents-md.sh: refuses AGENTS.md when it is a symlink
ok - fm-ensure-agents-md.sh: refuses a CLAUDE.md symlink that does not point to AGENTS.md
ok - fm-ensure-agents-md.sh: refuses a non-regular CLAUDE.md
ok - fm-ensure-agents-md.sh: refuses a case-variant lowercase agents.md (issue #389)
  -> PASS

## tests/fm-lint-workflows.test.sh
ok - fm-lint-workflows.sh refuses to lint under a non-pinned actionlint version
ok - actionlint installer retries a transient download failure
ok - actionlint installer selects the official archive, URL, and checksum per OS/arch
ok - actionlint installer rejects a wrong checksum
ok - actionlint installer falls back to shasum -a 256 when sha256sum is absent
ok - actionlint installer prefers sha256sum when both hashers are present
ok - actionlint installer rejects an unsupported OS or architecture
ok - fm-lint.sh default path catches a self-broken ci.yml
  -> PASS

## tests/fm-lint.test.sh
ok - jobs=1 and jobs=2 stop complete worker trees with and without telemetry
ok - seeded dispatcher, adapter, production-owner, and test-local diagnostics preserve parity
ok - fm-lint.sh changed mode lints only the changed canonical file
ok - fm-lint.sh forces a full lint in CI even when the local diff would be empty
ok - fm-lint.sh forces a full lint when HEAD is on main
ok - fm-lint.sh explicit paths bypass changed-file mode selection
ok - fm-lint.sh exits 0 with a note when the local branch has no changed lint targets
ok - fm-lint.sh --list-files reports the would-be changed set in changed mode
  -> PASS
Evidence: fm-session-start.sh digest rendered on the merged tree (end-user surface)

Source: fm-session-start.sh digest rendered on the merged tree (end-user surface)


================================================================================
SESSION START - <throwaway FM_HOME>
================================================================================

LOCK
--------------------------------------------------------------------------------
lock acquired: harness pid 3838223

BOOTSTRAP
--------------------------------------------------------------------------------
(silent - all good)

WAKE QUEUE
--------------------------------------------------------------------------------
1787024966	1	check	startup-network	check: startup-network: deferred startup network checks finished (done); read them with /home/digby/.no-mistakes/worktrees/2c4342823de3/01M09F1VFRDKT2HYB7CSAF97XX/bin/fm-startup-network.sh report
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation 4093818.1787024966.YTgrEh
================================================================================
SUPERVISION OPERATING INSTRUCTIONS - primary harness: claude
================================================================================
Current state:
- Lock: held by this session; this session owns normal supervision unless away mode says otherwise.
- Away mode: inactive.
- X mode: inactive; use the default watcher cadence.
- Ordinary wake: the Stop-owned auto-arm (bin/fm-claude-stop-autoarm.sh) already owns watcher continuity; drain and handle the wake, and do not arm another cycle yourself.

Mode: Claude Stop-hook-owned supervision.

When this session owns supervision and away mode is not active:
1. Drain first with `bin/fm-wake-drain.sh`.
   After handling all emitted wakes and reconciling open decisions and unread status lines, run the exact `--ack-through` command printed as `WAKE_ACK_REQUIRED`; until then the work remains durable for idempotent re-handling after interruption.
2. Routine watcher arm and re-arm are owned by the Stop `asyncRewake` hook (`bin/fm-claude-stop-autoarm.sh`), never by you.
   Every turn end while supervision is needed launches or attaches one home-scoped watcher cycle with no model command and no model tokens.
   An actionable close wakes you through the hook's exit-2 rewake, delivered as a `Stop hook feedback` message.
3. On a `Stop hook feedback` wake (`signal:`, `stale:`, `check:`, or `heartbeat`), run `bin/fm-wake-drain.sh` first and handle the wake.
   Do not run `bin/fm-watch-arm.sh` after an ordinary wake; the next turn end re-arms automatically when supervision is still needed.
   Do not invent a wake from an attach-status line alone; drain and act only on real wake records, the drain's `OPEN DECISIONS` and `UNREAD STATUS` entries, or a real watcher reason line.
4. On the one `Stop hook feedback` automatic-mechanism failure notice (`firstmate watcher auto-arm FAILED ...`), drain, inspect the automatic mechanism failure, and do not turn the notice into a repeating manual-arm loop.
5. If the Stop hook does not claim the home or reports an exhausted failure, inspect its registration and watcher startup path before ending blind.
   Keep the Stop-owned automatic mechanism as the only Claude arm owner.
6. Treat `watcher: started ...` and `watcher: attached ...` inside automatic arm output as proof that one live cycle exists.
   On attach, the arm follows verified identity-matched successors instead of exiting when the first cycle ends.
7. The durable wake queue preserves actionable events between a rewake and the next Stop-launched arm, while the bounded turn-end guard prevents a blind Stop when recovery did not start.
   No PreToolUse hook denies fleet commands based on watcher status.
   [`watcher-continuity.md`](../watcher-continuity.md) owns the exact session-lock recovery boundary.
8. The turn-end guard (`bin/fm-turnend-guard.sh --claude`) remains the final backstop.
   It requires the PID-strict live-watcher and fresh-beacon predicate at the Stop boundary, while the mid-turn pull guard accepts a fresh beacon without a live process under Claude's between-turns auto-arm model.
   It allows the stop when a watcher is healthy or the role-verified auto-arm owns recovery, while fresh failure epochs advance the bounded one-time attended fail-open progression described in [`turnend-guard.md`](../turnend-guard.md).
9. Waiting on the hook-owned cycle is silent: do not send idle progress while the watcher is parked.

The watcher itself remains `bin/fm-watch.sh`, and `bin/fm-watch-arm.sh` remains the verified arm wrapper that the Stop hook foregrounds.
Re-arm attaches to an existing healthy cycle when one is already present and follows its verified successor chain.
See [`watcher-continuity.md`](../watcher-continuity.md) for the arm-layer successor and clean-close failure contract and the Claude ownership model.


================================================================================
READ-ONCE CONTRACT
================================================================================
Everything below is printed in full for this session start: every state/*.meta,
a compact data/backlog.md listing, a bounded tail of every state/*.status,
data/projects.md, data/secondmates.md, data/captain.md, data/captain-shared.md,
and data/learnings.md.
Do NOT re-read any of them after reading this digest, and do NOT bulk-read
data/backlog.md or state/*.status: re-reading everything defeats the entire
point of this command.

Go to a source directly only when:
  - this digest flagged it ABSENT (then rebuild or create it per AGENTS.md),
  - its contents looked unparseable or corrupt,
  - an individual full status log is needed for older wake-event history, or a
    status line was capped and its tail matters (each task's full log path is
    printed with its tail),
  - a full task body is needed (tasks-axi show <id> --full, or data/backlog.md),
  - the backlog listing disclosed omitted queued items and this turn needs them,
  - the NETWORK CHECKS section reported its checks still IN PROGRESS and this
    turn needs their verdict (bin/fm-startup-network.sh report),
  - or a STARTUP TRUNCATED banner named the stage that would have printed it, in
    which case that stage's sources were never emitted and must be reconciled.

================================================================================
FLEET STATE
================================================================================

data/backlog.md
--------------------------------------------------------------------------------
ABSENT

Work under way (state/*.meta)
--------------------------------------------------------------------------------
(none)

Orphan status logs (state/*.status without matching .meta)
--------------------------------------------------------------------------------
(none)

AFK
--------------------------------------------------------------------------------
absent

================================================================================
NETWORK CHECKS
================================================================================
IN PROGRESS - the deferred network checks have not finished yet.
NOT yet confirmed: GitHub authentication, dead-secondmate relaunch, secondmate convergence, pending handoff delivery, and project clone refresh with its drift reporting.
Started 2s ago, bounded at 120s.
The result is durable in state/.startup-network.report and arrives as a `check: startup-network` wake.
Read it now with /home/digby/.no-mistakes/worktrees/2c4342823de3/01M09F1VFRDKT2HYB7CSAF97XX/bin/fm-startup-network.sh report; until it lands, treat none of it as confirmed.

================================================================================
CONTEXT
================================================================================

data/projects.md
--------------------------------------------------------------------------------
ABSENT

data/secondmates.md
--------------------------------------------------------------------------------
ABSENT

data/captain.md
--------------------------------------------------------------------------------
ABSENT

data/captain-shared.md (shared, main-authoritative, read-only in secondmate homes)
--------------------------------------------------------------------------------
ABSENT

data/learnings.md
--------------------------------------------------------------------------------
ABSENT

================================================================================
NEXT STEP
================================================================================
Follow the supervision operating instructions block above for harness 'claude'.
This script never starts supervision itself.

The digest above is complete for this session start. The READ-ONCE CONTRACT
section near the top of it governs what may still be read from disk.
Evidence: AGENTS.md boundary census, origin/main vs merged
6ed3006: never=60 always=3 must=20 sections=16 layout-entries=10
HEAD : never=61 always=3 must=20 sections=16 layout-entries=10
(no boundary lost; +1 'never' comes from upstream's own new /stow clause)
- Outcome: ⚠️ 1 info across 1 run (12m5s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ℹ️ bin/fm-procevent-slack-captain.sh:457 - Post-merge, upstream's generic keyed-answer seam (bin/fm-procevent.sh feed_keyed_answers) calls fm-procevent-&lt;adapter&gt;.sh answers &lt;result-file&gt; for any source bound via fm-decision-hold.sh bind. Our Slack captain adapter has no answers subcommand, so binding a Slack source would silently feed nothing (the call is guarded by an existing binding and fully silenced, so nothing breaks today). Noting the capability gap only; adding it is out of this merge-only scope.
  • ℹ️ AGENTS.md:72 - The reflowed decision-bindings/ entry carries upstream's full semantics (bind-only writer, dropped by unbind or source retirement, docs/decision-hold-lifecycle.md pointer) but omits upstream's incidental "section 13" cross-reference. No safety boundary or rule is lost; recorded for completeness against conflict rule 3.
⚠️ **Test** - 1 info
  • ℹ️ tests/fm-procevent-slack-captain.test.sh - tests/fm-procevent-slack-captain.test.sh fails on the merged tree ("the poll left its staging directory behind: /tmp/fm-slack-captain.*"). Confirmed pre-existing and not a merge regression: bin/fm-procevent-slack-captain.sh and its test are byte-identical blobs to origin/main (6ed3006), so this merge cannot have introduced it. Deliberately not fixed here per the merge-only scope.
  • git rev-parse &lt;rev&gt;:&lt;path&gt; blob-identity check of bin/fm-procevent-slack-captain.sh, tests/fm-procevent-slack-captain.test.sh, bin/fm-brief.sh, bin/fm-classify-lib.sh against origin/main (6ed3006)
  • Automated sweep: every line upstream added between merge-base f1a4af4 and upstream tip 64d61ae checked for presence in the merged tree, across all 42 upstream-touched files
  • Line-count arithmetic merged == ours + (upstream - base) for all 42 upstream-touched files
  • AGENTS.md safety-boundary census (never / always / must / '## ' sections / layout-block entries) compared origin/main vs merged
  • YAML-parsed comparison of .agents/skills/stow/SKILL.md description between upstream 64d61ae and merged HEAD
  • bash tests/fm-decision-hold-lifecycle.test.sh
  • bash tests/fm-classify-decision-key.test.sh
  • bash tests/fm-procevent.test.sh
  • bash tests/fm-ensure-agents-md.test.sh
  • bash bin/fm-install-actionlint.sh &lt;tmpdir&gt; then bash tests/fm-lint-workflows.test.sh
  • bash tests/fm-lint.test.sh with pinned actionlint on PATH
  • CI invariants job steps run locally: CLAUDE.md non-symlink + canonical @AGENTS.md pointer byte compare, readlink .claude/skills, git ls-files -- data state config projects .no-mistakes
  • FM_HOME=&lt;throwaway&gt; bash bin/fm-session-start.sh on the merged tree (full digest captured)
  • bash tests/fm-procevent-slack-captain.test.sh (reproduces the disclosed pre-existing staging-directory leak)
⚠️ **Document** - 1 info
  • ℹ️ docs/scripts.md - docs/scripts.md has no rows for upstream's new bin/fm-lint-workflows.sh and bin/fm-install-actionlint.sh. This gap arrived from upstream (CONTRIBUTING.md documents both), was not created by this merge-only change, and adding rows here would exceed merge scope — worth a follow-up on the script inventory table.
⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

kunchenguid and others added 13 commits August 15, 2026 22:10
…d mate (kunchenguid#2457)

The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.

- fmx-respond now states the routing rule crisply: a task in this home takes the
  lightweight link, and second-mate-routed work takes a promised-final
  commitment bound to that home, registered up front with the brief command
  carried into the routed worker's instructions.
- fm-x-link.sh refuses a task with no local record by naming the registered
  second mate whose home actually holds it and printing the promised-final
  registration command, with the exact --work-home when the match is
  unambiguous. A home with no registered second mates keeps the plain error.
- fm-backlog-handoff.sh reports, after a successful move, any moved key that
  still owes a public reply bound to main/<key>, since that binding no longer
  names the home owning the work. The move itself is never blocked.

Docs and the secondmate handoff prose follow the same rule. Tests cover the
refusal, its scoping, the unchanged local-link path, and both handoff outcomes
at the script boundary.
…verdicts (kunchenguid#2456)

* fix(skills): hint that remote secondmate liveness verdicts false-negative

fm-crew-state and fm-send routinely misreport a live remote secondmate
as dead; confirm against the pane before relaunching, and relaunch only
through fm-spawn.sh, never raw herdr pane surgery.

* no-mistakes: apply CI fixes
Pi 0.83.0 added a status line to every tool-expansion change, and Pi
updates the previous status line in place when two status messages
arrive back to back. Calm's post-export redraw cycled tool expansion on
the macrotask right after Pi printed "Session exported to: <path>", so
both expansion status lines coalesced over that confirmation and the
captain was left with no record of where their export landed.

Calm now repaints only the tool rows it presents, by invalidating each
row through the render context Pi hands its render slots, and requests
the surrounding redraw through setStatus. Neither appends to the
transcript. The repaint is still needed because Pi can re-render a row
asynchronously - the built-in edit row invalidates itself once its diff
is ready - and that re-render can land inside the window where /export
forces stock rendering.

The real-terminal /export case now asserts the confirmation is still on
screen after the redraw has settled, and that the redraw restored every
Calm-hidden row, instead of only racing the moment the confirmation
first appeared.
…nguid#2488)

* feat(stow): persist the open records a session is holding

/stow curated memory and captured session knowledge, but never touched
record state, while AGENTS.md called it an "unfinished-work sweep" and the
receipt declared the session "safe to reset" - wording that implied a
record-correctness guarantee stow does not make. A shipped PR with no
backlog item, a queued umbrella whose phases had merged, and four decision
holds left open after their answers shipped all survived repeated stows.

Add a bounded pass that files record state from the same volatile input the
rest of stow already uses: the open threads in context, minutes before the
reset destroys them. It creates a record for an unfiled thread and corrects
one the session knows is wrong, through the owning path, and states its
boundary as part of the contract - it never enumerates the backlog, lists
holds, or queries a forge, because it cannot be a reconciliation and must
not be read as one.

Correct the wording in AGENTS.md and the completion receipt so reset-safe
means what it actually guarantees: nothing this session knew was lost.

* no-mistakes(review): correct stow decision-hold inspection to read hold via tasks-axi

* no-mistakes(document): note /stow open-record persistence in README command catalog

* refactor(stow): state open-record persistence as principle, not procedure

The first version enumerated triggers, named commands, and prescribed an
ordered procedure. That is too rigid for an agent skill: it invites literal
execution of a checklist instead of judgment, and every enumerated example
is a way for the guidance to go stale.

Reduce it to the intent - before a reset, the important open work you are
holding in context must end up durably recorded rather than dying with the
session, filing what is unfiled and correcting what is stale - and let the
agent judge importance, the record, and the owning write path.

Keep the scope bound, since it is a decided contract and not a mechanic:
this covers the open work the session is holding, never a reconciliation of
durable records against repository or forge reality. The wording
corrections in AGENTS.md and the completion receipt are unchanged.
…eyed-answer path (kunchenguid#2490)

* fix(decisions): close captain holds at answer time

Firstmate had two "a decision is open" ledgers with asymmetric closing
mechanics. The live status-log ledger closes atomically at answer time,
because bin/fm-send.sh --resolve-key makes answering a decision be the
act that closes it. The durable backlog hold ledger had no such coupling:
answering and recording were two separate acts, and only the first was
forced by the workflow.

That asymmetry lost four real captain decisions. Their answers were
captured durably to disk, keyed character for character by the hold
decision keys, acknowledged, and even implemented and shipped, yet the
holds stayed open for two days and the captain was asked to re-answer
decisions already on his own disk.

Give the hold ledger the same answer-time-closure property:

- bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's
  counterpart to --resolve-key. It shares one unrouted close
  implementation with `decline`, so it carries every existing guard - the
  captain decision file, the active-hold requirement, retry identity, and
  the refusal to release still-routed work - and differs only in the
  resolution mode it records. `decline` keeps its stronger meaning that
  the answer routes no follow-up work at all.
- bin/fm-procevent-lavish.sh wires the channel that actually carried the
  lost answers. `arm --decisions-origin` binds a deck to the origin whose
  holds it carries, `answers` reads the structured choices out of a
  captured poll result, `close-decisions` maps each key to its hold and
  closes it through the command above, and `autohandle` lets the runner
  apply that at capture time.

Safety is preserved rather than traded away. Only rows tagged `choice`
are read, so freeform captain prose cannot forge a decision key. Closure
is confined to the one bound origin. The decision text is a pure function
of the captured result, so a replayed capture is idempotent. A hold that
is absent, already closed, or still blocking routed work is skipped and
left for `resolve`, never forced. A deck armed without the binding
touches no hold at all. And autohandle deliberately never reports full
handling, because recording an answer is transcription while acting on it
is firstmate's judgement - so the check wake still reaches the handler.

fm-send --resolve-key is untouched.

* no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory

* refactor(decisions): make keyed-answer closure one general capability

The previous pass gave holds answer-time closure but built it as bespoke
Lavish wiring: the review adapter carried the source-to-origin binding,
mapped keys to hold identities, wrote decision records, decided what to
skip, and closed holds itself. That treated a review deck as a special
decision source. It is not - it is an ephemeral discussion format that
happens to carry answers.

Collapse it into ONE general capability with one owner.

bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its
matching hold":
- `answers <origin> --source <provenance>` is the channel-agnostic
  intake. It reads key/answer/label lines on stdin, maps each key to its
  hold, and closes it through the same `answer` path, so every guard
  applies identically whatever channel the answer came from. --source is
  provenance recorded in the decision, never a behavior switch; there is
  no per-channel branch and no knowledge of chat, decks, or transports.
- `bind`/`unbind`/`binding` own the source-to-origin binding for any
  channel whose answers arrive detached from their origin.

Every channel is now an ordinary caller that only turns what it received
into keyed lines:
- bin/fm-send.sh (chat) feeds the intake for a key that names an active
  hold. This also fixes a real gap: once `complete` transfers a decision
  to its hold it closes the live status copy, so --resolve-key alone
  could never answer a transferred decision.
- bin/fm-procevent.sh feeds it generically. A bound source's captured
  result goes to `<adapter> answers <result-file>` and whatever that
  prints is piped into the intake. The runner names no adapter, parses
  no result, and carries no decision rule, so any future adapter with an
  `answers` command works with no change here.
- bin/fm-procevent-lavish.sh keeps only `answers`, which reports the
  structured choices a review captured and stops. It maps nothing to a
  hold and closes nothing; it lost ~160 lines of decision logic.

Feeding is independent of handling, so it never acknowledges a result
and never suppresses a wake - recording an answer is transcription,
acting on it stays firstmate's judgement.

The regression that proves closure now drives a FIXTURE adapter that is
not the review adapter, so what is proven is that any bound channel
reaches the intake rather than that one channel is wired specially. A
new regression drives the real fm-send over a stubbed transport for the
chat side. Every prior guarantee still holds, and fm-send's status-log
behavior is unchanged.

* no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression
…mlink (kunchenguid#2512)

A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md.
The installer now creates and migrates to a recoverable two-line pointer file.
* fix(lint): catch malformed GitHub workflows before merge

A self-broken ci.yml cannot report its own breakage, so parse every
workflow in the local lint path that no-mistakes already runs.

* fix(lint): pin actionlint instead of Ruby for workflow lint

A self-broken ci.yml still has to fail in the local lint path, and the
named tool for that gate is actionlint, not a new Ruby runtime.

* no-mistakes(document): Clarify pinned workflow lint documentation
…d#2546)

* fix: install pinned shellcheck and actionlint on macOS and linux arm64

The installers were hardcoded to linux amd64 and sha256sum, so a Mac
dev could not satisfy the refuse-on-mismatch lint gate. Select the
official per-platform archive and checksum, and fall back to shasum -a 256.

* no-mistakes(document): Document cross-platform pinned lint installers
…uid#2548)

.no-mistakes.yaml has set test.evidence.store_in_repo: true since kunchenguid#2355, but
CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described
the old policy of keeping evidence out of the repo in a temp directory.

The current no-mistakes behavior for store_in_repo: true is to publish each run's
test evidence to the orphan no-mistakes/evidence branch and link it from the PR
body. That branch shares no history with code branches, so evidence never enters
a pushed feature branch or the default branch, and CI's tracked personal fleet
paths rule stays accurate.

Docs only. No change to .no-mistakes.yaml or any workflow.
* docs: correct test evidence storage comment in .no-mistakes.yaml

* no-mistakes: apply CI fixes
Brings in 11 upstream commits. Three conflicts resolved by keeping both
sides' meaning: AGENTS.md section 2 layout block, the stow skill
description, and the process-event-sources arming section.
…resolution

The conflict resolution had kept our dropped 'e.g.'; the captain's standing
direction is to prefer upstream wording where the two genuinely conflict.
Only the block-scalar reflow remains ours, so the file keeps one sentence
per line as the repo style requires.
@digbycampbell

Copy link
Copy Markdown
Owner Author

Merge resolution record

Merge base: f1a4af4. Merged upstream/main (11 commits) into a branch off origin/main (6 ahead), following the precedent of f18ba40 / PR #6. No rebase of published history: main is already published and tracked by six worktrees and two secondmate homes, and it contains a merge commit.

Upstream commits brought in

commit change
64d61ae docs: clarify test evidence branch storage (kunchenguid#2549)
312871d docs: reconcile test-evidence docs with store_in_repo: true (kunchenguid#2548)
ac55d39 fix: install pinned lint tools across supported platforms (kunchenguid#2546)
0ae14af ci: gate GitHub workflows with pinned actionlint (kunchenguid#2517)
bdae21e fix(ci): keep CLAUDE.md pointer check valid (kunchenguid#2515)
4913723 fix(memory): emit a real @AGENTS.md pointer instead of a CLAUDE.md symlink (kunchenguid#2512)
362c508 fix(decisions): close decision holds at answer time via one general keyed-answer path (kunchenguid#2490)
e518906 feat(stow): add open-record persistence to /stow before reset (kunchenguid#2488)
ef35d79 fix(calm): keep Pi's export confirmation visible (kunchenguid#2461)
196fb65 docs(skills): add remote-secondmate recovery hint for false-negative verdicts (kunchenguid#2456)
7a3259e fix: keep the public promise reachable when work is routed to a second mate (kunchenguid#2457)

Behaviour changes we rely on, flagged:

  1. CLAUDE.md is no longer a symlink (4913723). A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md, so upstream replaced it with a real two-line @AGENTS.md pointer, and bdae21e made CI enforce that shape. Our side never changed CLAUDE.md, so the merge took upstream's file. .claude/skills remains a symlink to .agents/skills — a different, still-valid symlink.
  2. Linting now requires a pinned actionlint (0ae14af, ac55d39). bin/fm-lint.sh invokes the new bin/fm-lint-workflows.sh, which needs actionlint 1.7.12. CI installs it into $RUNNER_TEMP/bin in every lane; local homes need bin/fm-install-actionlint.sh <dir> first or bin/fm-lint.sh exits 127.
  3. Decision holds now close at answer time (362c508), adding state/decision-bindings/ and bin/fm-decision-hold.sh bind.

Conflicts and how each was resolved

Three conflicts, all charter prose. No functional file conflicted.

1. .agents/skills/stow/SKILL.md — frontmatter description

  • Ours: reformatted the description into a YAML block scalar (>-), one sentence per line per the repo's Markdown style, and dropped e.g..
  • Upstream: added a new clause — "persist the open work records this session knows are unfiled or now wrong".
  • Resolved: took upstream's sentence verbatim, including its e.g., and kept only our block-scalar reflow so the file still obeys one-sentence-per-line. Genuinely competing wording, so upstream wins on wording; our change was formatting and is preserved as formatting.

2. .agents/skills/process-event-sources/SKILL.md — "Arming a source"

  • Ours: added a paragraph for the bin/fm-procevent-slack-captain.sh adapter.
  • Upstream: added a paragraph requiring a fm-decision-hold.sh bind before arming a source that carries captain answers.
  • Resolved: kept both, verbatim. This was not competing wording — both sides added different content at the same location. Dropping either would have lost a rule.

3. AGENTS.md — section 2 layout block

  • Ours: restructured the whole block (landed in PR docs(agents): slim the startup context load #1, 566d737), 568 → 448 lines.
  • Upstream: two changes inside that block — the CLAUDE.md is a real @AGENTS.md pointer correction, and a new state/decision-bindings/ entry.
  • Resolved: applied upstream's two facts into our structure, rather than taking upstream's block wholesale. Taking it wholesale would not have been a wording choice; it would have reverted a landed restructure. The decision-bindings/ entry carries its full upstream semantics — written only by bin/fm-decision-hold.sh bind, dropped by unbind or source retirement, plus the docs/decision-hold-lifecycle.md pointer. Upstream's third AGENTS.md change (the /stow sentence in section 6) merged cleanly and took upstream's stronger wording.

No conflict resolution dropped a rule, and no case arose where preferring upstream wording would have cost one of our constraints.

Before/after counts

Every touched file is exactly ours + (upstream − base):

file base ours upstream merged expected
AGENTS.md 568 448 569 451 449 + 2 *
.agents/skills/process-event-sources/SKILL.md 107 103 117 113 113
.agents/skills/stow/SKILL.md 269 271 283 285 285
.agents/skills/fmx-respond/SKILL.md 256 252 269 265 265
.agents/skills/secondmate-provisioning/SKILL.md 249 248 252 251 251
.agents/skills/decision-hold-lifecycle/SKILL.md 42 42 49 49 49
.agents/skills/stuck-crewmate-recovery/SKILL.md 51 50 54 53 53
.agents/skills/updatefirstmate/SKILL.md 64 63 64 63 63
.agents/skills/firstmate-coding-guidelines/SKILL.md 126 125 127 126 126
CLAUDE.md 0 (symlink) 0 (symlink) 2 2 2

* AGENTS.md is 451 rather than the mechanical 449 because upstream's one-line decision-bindings/ entry needs three lines in that block's column layout.

An automated sweep additionally confirmed that every line upstream added since the merge base is present in the result; the only two it flagged were the two resolutions above that were reflowed to fit, both verified semantically complete.

Our work, confirmed intact

Verified byte-identical to origin/main after the merge:

  • bin/fm-procevent-slack-captain.sh and tests/fm-procevent-slack-captain.test.sh
  • bin/fm-brief.sh (including the real-application verification clause) and tests/fm-brief.test.sh
  • tests/fm-send-resolve-key.test.sh

Note on the decision-key parsing fix (29d056b): bin/fm-classify-lib.sh is identical across base, origin/main, upstream/main and this merge — our implementation was superseded by upstream's at the previous sync (PR #6), not by this one. The behaviour is intact (the after-the-colon needs-decision: [key=...] form is handled and documented in that file) and our regression test still guards it.

Test evidence

The full suite reports 21 failing scripts on this branch. A control run of the same scripts on pre-merge origin/main, in a throwaway clone, reproduced 18 of them identically. The three that differ are missing-tool environmental, not regressions:

  • tests/fm-lint.test.sh and the new tests/fm-lint-workflows.test.sh both pass once upstream's pinned actionlint is installed (verified by installing it to a temp dir and re-running).
  • tests/fm-watch-checkpoint.test.sh passed 3/3 in isolation; in the full run it was polluted by a watcher process killed during the preceding script.

bin/fm-lint.sh (with actionlint present) and bin/fm-doc-audience-check.sh are clean, and bin/fm-session-start.sh produces a full digest on the merged tree.

Findings not fixed

  • bin/fm-procevent-slack-captain.sh leaks its staging directory. tests/fm-procevent-slack-captain.test.sh already fails on origin/main: the poll leaves /tmp/fm-slack-captain.* behind, containing the captured message payload. Pre-existing and unrelated to this merge, so out of scope for a merge-only change.
  • docs/scripts.md has no rows for upstream's new bin/fm-lint-workflows.sh and bin/fm-install-actionlint.sh. This gap arrived from upstream (CONTRIBUTING.md documents both); adding rows would exceed merge scope.

@digbycampbell
digbycampbell merged commit 1e0557c into main Aug 18, 2026
13 checks passed
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.

2 participants