diff --git a/.agents/skills/decision-hold-lifecycle/SKILL.md b/.agents/skills/decision-hold-lifecycle/SKILL.md index 9eecc57811a..07fb7e48876 100644 --- a/.agents/skills/decision-hold-lifecycle/SKILL.md +++ b/.agents/skills/decision-hold-lifecycle/SKILL.md @@ -23,6 +23,12 @@ Run the command in the originating work's authoritative `FM_HOME`; main-home wor Do not close a hold merely because the originating investigation completed, its report was archived, its visual review ended, or its task was torn down. When the captain's answer authorizes follow-up work, the hold remains the authoritative Captain's Call item until that answer is durably recorded, dependent work is created in the same backlog and blocked by the hold, and `bin/fm-decision-hold.sh resolve` routes the answer by clearing those dependency edges before closing the hold. When the captain's answer routes no follow-up work at all, such as a declined proposal, `bin/fm-decision-hold.sh decline` records that answer and closes the hold; it never substitutes for routing work the captain did authorize. +When the captain simply answers a hold that has no follow-up work routed behind it yet, `bin/fm-decision-hold.sh answer` records that answer and closes the hold, so answering is closing rather than a separate later act that can be forgotten. +"A keyed answer closes its matching hold" is one capability with one owner, `bin/fm-decision-hold.sh answers`, and every channel that carries a captain answer feeds it the same `` and answer. +A channel never maps a key to a hold, records a decision, or closes anything itself, so no channel is special and a new one needs no new closing logic. +Chat already feeds it: `bin/fm-send.sh --resolve-key` answers a decision in whichever ledger still holds it open, including a decision already transferred to its durable hold. +A captured-answer source feeds it too once bound with `bin/fm-decision-hold.sh bind `; bind before arming the source, and key each structured question by the hold's own decision key. +An unbound source and a question slug that is not a decision key both simply feed nothing: the answer is still captured and firstmate is still woken, and closing falls back to the commands above. A hold closed outside this owner leaves no durable answer, so the completion gate keeps failing until `bin/fm-decision-hold.sh repair` records the decision the captain actually gave; neither unrouted path may stand in for an answer the captain has not given. Resolved findings, recommendations that need no captain choice, and prose that merely sounds decision-like do not create holds. Bearings reads the resulting structured state and must never compensate by scraping historical reports, visual-review artifacts, terminal output, chat, or other prose. @@ -35,8 +41,9 @@ Bearings reads the resulting structured state and must never compensate by scrap 4. Run the script's `complete` command with the full unresolved-key inventory for that review pass. 5. Relay the choices to the captain as decisions from Bearings' Captain's Call section under `AGENTS.md` section 9; do not use the word hold in captain chat. 6. If the captain authorizes dependent work, record it with normal tasks-axi commands and block it by the hold identity. -7. Put the captain's exact durable decision in a file and close the hold with the script's `resolve` command and every routed task, its `decline` command when the answer routes no work, or its `repair` command when the hold was already closed outside the script. -8. Steps 8 through 11 apply to `resolve` alone, because the unrouted `decline` and `repair` paths release no work: immediately after resolving, examine what each routed task is actually waiting for now that the decision itself is no longer a blocker. +7. Put the captain's exact durable decision in a file and close the hold with the script's `resolve` command and every routed task, its `answer` command when the captain answered a hold with no routed work behind it, its `decline` command when the answer routes no work at all, or its `repair` command when the hold was already closed outside the script. + A hold that a channel already closed by feeding its keyed answer needs none of these; confirm it in step 12 instead. +8. Steps 8 through 11 apply to `resolve` alone, because the unrouted `answer`, `decline`, and `repair` paths release no work: immediately after resolving, examine what each routed task is actually waiting for now that the decision itself is no longer a blocker. 9. When a routed task still waits for an implementation landing or any other precondition besides the decision, re-establish that precondition explicitly with normal backlog dependency or hold mechanics. 10. Write each re-block note to identify what running the task early would measure or produce wrongly, rather than merely saying that the task is blocked on the precondition. 11. Confirm each routed task's structured backlog state matches its real remaining preconditions. diff --git a/.agents/skills/firstmate-coding-guidelines/SKILL.md b/.agents/skills/firstmate-coding-guidelines/SKILL.md index 2d434932997..a9e21543077 100644 --- a/.agents/skills/firstmate-coding-guidelines/SKILL.md +++ b/.agents/skills/firstmate-coding-guidelines/SKILL.md @@ -118,7 +118,8 @@ Run `bin/fm-doc-audience-check.sh`; it enforces classification, README setup rou - Plain dash `-`, never an em dash. - Never add an agent name as a commit co-author. - `bin/*.sh` and `bin/backends/*.sh` must pass `shellcheck`. -- Run `bin/fm-lint.sh` before treating a script change as done; it is the single owner of the lint definition (file set, config, and pinned shellcheck version) that CI and the no-mistakes pre-push gate both invoke, and it refuses to run under any other shellcheck version. +- Run `bin/fm-lint.sh` before treating a script change as done; it is the single owner of the lint definition (file set, config, pinned shellcheck version, and pinned actionlint workflow lint) that CI and the no-mistakes pre-push gate both invoke, and it refuses to run under any other version of either linter. +- When a task names a specific tool, implement the work with that tool, or explicitly flag the substitution and its new dependency footprint for review before shipping. - Colocate tests with the existing pattern in `tests/`, name them `.test.sh`, and extend an existing script rather than inventing a new runner. - Tests must exercise behavior through an executable or public interface and must never assert implementation-source bytes, including through parsers, regexes, snapshots, or indirect wrappers. - A maintainer-verification record under `docs/verification/` records active empirical facts, not assumptions or task chronology. diff --git a/.agents/skills/process-event-sources/SKILL.md b/.agents/skills/process-event-sources/SKILL.md index 093272c41a2..793ac546126 100644 --- a/.agents/skills/process-event-sources/SKILL.md +++ b/.agents/skills/process-event-sources/SKILL.md @@ -25,12 +25,22 @@ Firstmate registers a source, keeps working, and is woken when that process comp ## Arming a source Use the adapter, not the generic runner, for a real source. -For a Lavish review artifact: +For a Lavish review artifact firstmate owns (a live investigating scout should host its own loop): ```sh bin/fm-procevent-lavish.sh arm ``` +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 +``` + +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. diff --git a/.agents/skills/stuck-crewmate-recovery/SKILL.md b/.agents/skills/stuck-crewmate-recovery/SKILL.md index cf741b9d95f..b9b94b27d43 100644 --- a/.agents/skills/stuck-crewmate-recovery/SKILL.md +++ b/.agents/skills/stuck-crewmate-recovery/SKILL.md @@ -23,7 +23,7 @@ The target window's harness is recorded as `harness=` in `state/.meta`. This procedure covers ordinary `kind=ship` and `kind=scout` direct reports. Load `secondmate-provisioning` instead for `kind=secondmate` recovery. -For a REMOTE secondmate, `fm-crew-state`'s `unknown`/`worktree gone` and `fm-send`'s `remote send failed`/`delivery unconfirmed` verdicts are unreliable and routinely false-negative; do not conclude the mate is dead or the send failed from those alone, confirm against the actual remote pane first. +For a REMOTE secondmate, `fm-crew-state` and `fm-peek` read the actual remote endpoint over `fm-on.sh`, and `fm-send` reports a delivered-with-pending-confirmation steer as delivered (their headers own the contracts); an `unknown-remote` read or unreachable-host failure means the remote state could not be read, never that the mate is dead or the send failed. Recover a genuinely stuck remote mate only through `bin/fm-spawn.sh --secondmate`, never raw herdr pane close/kill surgery, which strands the endpoint binding. Treat the digest's endpoint result as a presence signal, not proof that the task's work or validation run is gone. diff --git a/.agents/skills/updatefirstmate/SKILL.md b/.agents/skills/updatefirstmate/SKILL.md index 0230b31f073..36e9a80b937 100644 --- a/.agents/skills/updatefirstmate/SKILL.md +++ b/.agents/skills/updatefirstmate/SKILL.md @@ -35,7 +35,7 @@ This touches only the firstmate repo and its own worktrees, never anything under 2. **Re-read AGENTS.md if your own instructions changed.** When the updater printed `reread-firstmate: yes`, the tracked instruction surface (`AGENTS.md`, `bin/`, or `.agents/skills/`) just advanced under you. - **Read `AGENTS.md` now** (CLAUDE.md is a symlink to it) to refresh your operating instructions before doing anything else, so you are acting on the new instructions rather than the stale ones you were started with. + **Read `AGENTS.md` now** (CLAUDE.md is a real `@AGENTS.md` pointer to it) to refresh your operating instructions before doing anything else, so you are acting on the new instructions rather than the stale ones you were started with. When it printed `reread-firstmate: no`, nothing changed for you - skip the re-read. 3. **Nudge each updated live secondmate.** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5495ec44947..fcfc4cb2dfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ permissions: jobs: lint: - name: Lint shell scripts + name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -20,8 +20,15 @@ jobs: set -eu bin/fm-install-shellcheck.sh "$RUNNER_TEMP/bin" echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" - # Single owner of the lint definition (file set + config + version). Do not - # re-spell the shellcheck command here; keep CI and the pre-push gate on it. + - name: Install pinned actionlint + run: | + set -eu + bin/fm-install-actionlint.sh "$RUNNER_TEMP/bin" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + # Single owner of the lint definition (shell file set, config, version, + # and GitHub workflow lint). Do not re-spell the checks here; keep CI + # and the pre-push gate on this script so a self-broken ci.yml still + # fails locally before merge. - run: bin/fm-lint.sh # Deterministic proof that portable parallel shards + portable serial + Herdr @@ -52,6 +59,11 @@ jobs: set -eu bin/fm-install-shellcheck.sh "$RUNNER_TEMP/bin" echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + - name: Install pinned actionlint + run: | + set -eu + bin/fm-install-actionlint.sh "$RUNNER_TEMP/bin" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" - name: Install tasks-axi run: | set -eu @@ -84,6 +96,11 @@ jobs: set -eu bin/fm-install-shellcheck.sh "$RUNNER_TEMP/bin" echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + - name: Install pinned actionlint + run: | + set -eu + bin/fm-install-actionlint.sh "$RUNNER_TEMP/bin" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" - name: Install tasks-axi run: | set -eu @@ -130,6 +147,11 @@ jobs: set -eu bin/fm-install-shellcheck.sh "$RUNNER_TEMP/bin" echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + - name: Install pinned actionlint + run: | + set -eu + bin/fm-install-actionlint.sh "$RUNNER_TEMP/bin" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" - name: Require tmux for e2e tests run: | set -eu @@ -373,10 +395,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - name: Symlinks must stay intact + - name: Compatibility pointers must stay intact run: | set -eu - [ "$(readlink CLAUDE.md)" = "AGENTS.md" ] || { echo "::error::CLAUDE.md must be a symlink to AGENTS.md"; exit 1; } + [ ! -L CLAUDE.md ] || { echo "::error::CLAUDE.md must be a real @AGENTS.md pointer file, not a symlink"; exit 1; } + tmp=$(mktemp) + trap 'rm -f "$tmp"' EXIT + printf '%s\n' \ + '' \ + '@AGENTS.md' >"$tmp" + cmp -s CLAUDE.md "$tmp" || { echo "::error::CLAUDE.md must be the canonical @AGENTS.md pointer"; exit 1; } [ "$(readlink .claude/skills)" = "../.agents/skills" ] || { echo "::error::.claude/skills must be a symlink to ../.agents/skills"; exit 1; } - name: Personal fleet paths must not be tracked run: | diff --git a/.no-mistakes.yaml b/.no-mistakes.yaml index 62bb9e72849..f825543372d 100644 --- a/.no-mistakes.yaml +++ b/.no-mistakes.yaml @@ -24,9 +24,10 @@ document: # Pin lint to the same owner CI runs instead of leaving it to no-mistakes' # default handling, which does not invoke the repository's canonical lint gate. -# `bin/fm-lint.sh` owns the complete lint definition and +# `bin/fm-lint.sh` owns the complete lint definition, including GitHub workflow +# lint via pinned actionlint in `bin/fm-lint-workflows.sh`, and # `.github/workflows/ci.yml` invokes it directly, with parity asserted by -# `tests/fm-lint.test.sh`. +# `tests/fm-lint.test.sh` and `tests/fm-lint-workflows.test.sh`. # # Do not set commands.test to a complete tests/*.test.sh walk. Local no-mistakes # Test is intent-targeted validation of whether the change meets its brief; @@ -36,7 +37,8 @@ document: commands: lint: 'bin/fm-lint.sh' -# Store test evidence in this repo so it is committed alongside the change instead of kept in a temp dir. +# Publish each run's test evidence to the orphan no-mistakes/evidence branch linked from the PR. +# The evidence is not committed to the feature or default branch. test: evidence: store_in_repo: true diff --git a/AGENTS.md b/AGENTS.md index 19a8b69a14e..27c01817d68 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,7 @@ Each secondmate has a persistent isolated `FM_HOME`, including its own state, ba Tracked files hold shared instructions and tooling; `data/` holds durable private fleet records; `state/` holds runtime records and append-only status events; `config/` holds local operating choices; and `projects/` contains clones that are read-only to firstmate except under hard rule 1's concrete captain-approved project operation exception. ``` -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) CONTRIBUTING.md contributor workflow and repo conventions README.md public overview and development notes .github/workflows/ shared CI and PR enforcement, committed @@ -110,6 +110,7 @@ state/ runtime records and signals; gitignored pending-replies/ parent-owned secondmate pending-reply records (correlation id, delivery vs reply, recovery, escalation); fm-pending-reply-lib.sh procevent/ registered process-to-event sources, one private record per canonical source id; written only by bin/fm-procevent.sh, and their presence alone keeps supervision required (section 13) procevent-inbox/ private captured results and their durable handled-acknowledgement markers; source output lives here and never in an event line + 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) when/ private condition->action watch specs, their trust bindings, and single-fire markers; written only by bin/fm-procevent-when.sh (section 13's process-event-sources trigger) x-inbox/ generated Relay pending mention payloads; fmx-respond drains it (section 14) x-context/ generated Relay durable per-request reply context and one-wake offer markers, keyed by request_id; survives inbox cleanup and expires within seven days (section 14; bin/fm-x-lib.sh) @@ -377,6 +378,7 @@ Retire one only on an explicit captain or main-firstmate decision, after loading A completed scout must leave a self-contained report before its scratch worktree can be discarded; read and relay its findings, record the report as the Done artifact, and re-evaluate the queue. A report may recommend implementation but does not authorize it. Before treating the investigation or any visual review as complete, load `decision-hold-lifecycle`; teardown enforces that shared completion gate. +When a scout's deliverable is a visual artifact the captain will iterate on, prefer keeping that scout alive to host its own Lavish loop rather than tearing it down and mediating from firstmate, so the scout keeps its investigation context and the captain iterates in one continuous session. When implementation is separately authorized, promote the existing scout through `bin/fm-promote.sh` rather than creating a duplicate task. The promoted worker must inventory scratch state, return to a clean default-branch base, carry over only intended fix changes, create the ship branch, and follow the project's selected delivery path while leaving scratch commits and debug edits behind and turning a reproduced bug into the regression test. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 47dc3e3d863..00000000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..a9d4d2694af --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,2 @@ + +@AGENTS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fa1f30c561..cef1f1180f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star ## Repo conventions - This repo is a template for running a firstmate orchestrator agent. - `AGENTS.md` is the agent's main job description and names when to load bundled firstmate skills; `CLAUDE.md` is a symlink to it, and `.claude/skills` is a symlink to `.agents/skills`. + `AGENTS.md` is the agent's main job description and names when to load bundled firstmate skills; `CLAUDE.md` is a real `@AGENTS.md` pointer to it, and `.claude/skills` is a symlink to `.agents/skills`. - Only shared material is tracked: `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and `skills/`. `.agents/skills/` holds agent-loaded skills that assume a live firstmate home and carry `metadata.internal: true` so installers such as [skills.sh](https://skills.sh) hide them from discovery; `skills/` holds standalone, installer-facing public skills with no firstmate dependency (see the README's "Two-tier skill layout"). Everything personal to one captain's fleet (`.env`, `data/`, `state/`, `config/`, `projects/`, `.no-mistakes/`) is gitignored; never commit it. @@ -45,8 +45,11 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star - Helper scripts in `bin/` are plain bash. Each starts with a usage header comment; keep it accurate when you change behavior. Test scripts and helpers in `tests/` are plain bash too. - `bin/fm-lint.sh` must pass: it is the single owner of the lint definition (the shellcheck file set, config, and pinned shellcheck version), and both CI and the no-mistakes pre-push gate run it, so local and CI can never diverge. - It pins one exact shellcheck version and refuses to run under any other; print it with `bin/fm-lint.sh --required-version` and install that build locally. + `bin/fm-lint.sh` must pass: it is the single owner of the lint definition (the shellcheck file set, config, pinned shellcheck version, and pinned actionlint workflow lint), and both CI and the no-mistakes pre-push gate run it, so local and CI can never diverge. + A malformed `.github/workflows/*.yml`, including a self-broken `ci.yml`, fails that local lint path before merge because a broken workflow cannot report its own breakage. + It pins one exact shellcheck version and one exact actionlint version and refuses to run under any other. + Print the shellcheck pin with `bin/fm-lint.sh --required-version` and the actionlint pin with `bin/fm-lint-workflows.sh --required-version`. + Use `bin/fm-install-shellcheck.sh` and `bin/fm-install-actionlint.sh` to install those exact builds locally; each installer's header owns its destination usage and supported platforms. - Harness-adapter ownership spans detection in `bin/fm-harness.sh`, launch and hook mechanics in `bin/fm-spawn.sh`, semantic busy sources and trust gates in `bin/fm-busy-lib.sh`, delivery-only rendered guards in `bin/fm-composer-lib.sh`, cleanup in `bin/fm-teardown.sh`, and facts in `.agents/skills/harness-adapters/SKILL.md`; the `firstmate-coding-guidelines` skill owns the validation policy for checks that depend on those harnesses. - Changes to runtime session backends (`bin/fm-backend.sh`, `bin/backends/`, and the scripts that dispatch through them) keep current setup and limits in the relevant backend guide and active empirical evidence in [`docs/verification/runtime-backends.md`](docs/verification/runtime-backends.md). - [`docs/documentation-audiences.md`](docs/documentation-audiences.md) and its machine-consumed inventory own prose classification; run `bin/fm-doc-audience-check.sh` after documentation changes. @@ -64,15 +67,15 @@ A crewmate picking up such a brief should load the skill even if the brief preda When supervising live crewmates, keep firstmate's own long validation or build commands in the background so watcher wakes can still be handled. Crewmate validation follows the installed no-mistakes version's SKILL.md and live `axi` help instead of duplicating gate mechanics in firstmate docs. Firstmate's wrapper still matters: crewmates route every `ask-user` finding to firstmate, which applies the authority contract in `AGENTS.md`, and crewmates avoid `--yes` because it would bypass that check and any required captain escalation. -Local `.no-mistakes/` state and test evidence stay out of this repo; `.no-mistakes.yaml` keeps evidence in a temp directory and pins the gate's lint command to `bin/fm-lint.sh`, matching the Linux CI lint job. +`.no-mistakes.yaml` publishes test evidence to the orphan `no-mistakes/evidence` branch, which shares no history with code branches, and pins the gate's lint command to `bin/fm-lint.sh`, matching the Linux CI lint job. Local no-mistakes Test is intent-targeted and must not re-run every `tests/*.test.sh`; `.github/workflows/ci.yml` owns the broad behavior suite plus platform-specific compatibility lanes. -That is firstmate-specific; do not commit `.no-mistakes/evidence/` here even when another no-mistakes-managed target project keeps committed PR evidence. +The pipeline publishes that evidence itself, so never hand-commit `.no-mistakes/` paths onto a feature branch; CI rejects them as tracked personal fleet paths. Check and test the toolbelt before pushing: ```sh while IFS= read -r script; do /bin/bash -n "$script" || exit; done < <(bin/fm-lint.sh --list-files) # syntax-check the shell surface fm-lint.sh will cover (changed files locally, full set in CI/on main) -bin/fm-lint.sh # lint that same surface; the single owner CI and the no-mistakes gate both run, full set in CI +bin/fm-lint.sh # lint that shell surface plus GitHub workflows via pinned actionlint; the single owner CI and the no-mistakes gate both run bin/fm-test-run.sh tests/.test.sh # one script (primary local focus path, timed) bin/fm-test-run.sh --family pure-contract-unit # ordinary family-scoped local path (serial, timed) bin/fm-test-run.sh --changed # conservative changed-file-informed set (never silent full suite) @@ -83,7 +86,10 @@ bin/fm-test-run.sh --check-coverage # prove portable shards + serial + serial bin/fm-test-run.sh --all # deliberate complete regression (optional local full walk; not no-mistakes Test) bin/fm-test-isolation-proof.sh --list # proven parallel candidate set (Phase 2 owner) bin/fm-test-isolation-proof.sh --jobs 4 --json /tmp/fm-isolation-proof.json # re-run concurrent isolation proof only -[ "$(readlink CLAUDE.md)" = "AGENTS.md" ] +[ ! -L CLAUDE.md ] && cmp -s CLAUDE.md - <<'EOF' + +@AGENTS.md +EOF [ "$(readlink .claude/skills)" = "../.agents/skills" ] tmp=$(mktemp -d) && printf 'done: smoke\n' > "$tmp/smoke.status" && FM_STATE_OVERRIDE="$tmp" FM_SIGNAL_GRACE=1 FM_POLL=1 FM_HEARTBEAT=999999 bin/fm-watch-arm.sh # watcher re-arm smoke test (prints arm status, then an actionable signal) ``` diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index 45cbc1272f6..6c2344313ab 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -373,6 +373,7 @@ Write your findings to \`$DATA/$ID/report.md\`. The report must stand alone: 4. VERIFICATION - exactly what you ran to confirm your findings and its result; if you cannot verify something, say so explicitly - never claim unverified work as confirmed. 5. UNVERIFIED CLAIMS - claims in this report you could not confirm (or "none"). 6. RECOMMENDATION - what you recommend; this does not authorize implementation. +If your deliverable is a visual artifact the captain will review and iterate on, you may host the Lavish review loop yourself (poll, revise, re-serve, staying alive) instead of handing it back to firstmate. Before reporting done, read and follow \`$FM_ROOT/.agents/skills/decision-hold-lifecycle/SKILL.md\` and pass its shared completion gate for the report and any visual review. When the report is complete, append \`done: {one-line conclusion}\` to the status file and stop. If your findings reveal work that should ship (e.g. you reproduced a bug and the fix is clear), say so in the report; firstmate may promote this task in place, and you would then receive mode-specific ship instructions as a follow-up message. diff --git a/bin/fm-crew-state.sh b/bin/fm-crew-state.sh index 09ba4ae2bea..1bde7c87d55 100755 --- a/bin/fm-crew-state.sh +++ b/bin/fm-crew-state.sh @@ -16,10 +16,17 @@ # fixed mapping logic, no heuristics and no LLM. Output is one stable, parseable, # token-tight line firstmate can read every heartbeat: # -# state: · source: · +# state: · source: · # # Logic, in order: -# 1. Resolve worktree + backend target + kind from state/.meta. +# 1. Resolve worktree + backend target + kind from state/.meta. A meta +# recording remote_host= is a remote secondmate: its worktree and endpoint +# live on that host, so the local worktree and pane reads are skipped and +# the remote host is asked for the endpoint's recovery-grade state +# (fm-on.sh + fm-remote-secondmate-control.sh state). alive falls through +# to the routed status log; dead/missing report the remote verdict; an +# unreachable or unreadable remote reports unknown-remote, never a false +# gone/dead. # 2. Matching no-mistakes run for this crew's branch AND current code identity, # active or terminal (from `axi status`, or the coarse `no-mistakes runs` # fallback)? Branch name alone is not enough: a historical run on a reused @@ -118,10 +125,13 @@ meta_value() { # WT=$(meta_value worktree) KIND=$(meta_value kind) HARNESS=$(meta_value harness) +REMOTE_HOST=$(meta_value remote_host) [ -n "$KIND" ] || KIND=ship -# A torn-down (or never-created) worktree has no current state to read. -if [ -z "$WT" ] || [ ! -d "$WT" ]; then +# A torn-down (or never-created) worktree has no current state to read. A +# remote secondmate's recorded worktree is a path on ITS host, so the local +# probe proves nothing for it - the remote arm below reads the true source. +if [ -z "$REMOTE_HOST" ] && { [ -z "$WT" ] || [ ! -d "$WT" ]; }; then emit unknown none "worktree gone (torn down?)" fi @@ -155,6 +165,45 @@ map_log_state() { # LOG_LINE=$(log_last_line || true) LOG_VERB=$(status_line_verb "$LOG_LINE") +# --- remote secondmate: the true source is the remote endpoint --------------- +# A remote mate's recorded worktree and backend target live on its own host, so +# the local worktree probe above and the local pane reads below would misreport +# a healthy remote mate as gone or dead. Ask the remote host for the endpoint's +# recovery-grade state over the same fm-on.sh transport fm-send uses, then read +# current activity from the routed status log exactly as for a local +# secondmate (an idle endpoint is healthy for a secondmate either way). An +# unreachable host or unreadable endpoint is reported as unknown-remote - +# explicitly NOT proof of death - so a transport blip never reads as a torn +# down or dead mate; only the remote host's own dead/missing verdict may say +# the endpoint is actually gone. +if [ -n "$REMOTE_HOST" ]; then + if ! REMOTE_STATE=$(FM_HOME="$FM_HOME" "$SCRIPT_DIR/fm-on.sh" "$ID" \ + fm-remote-secondmate-control.sh state "$ID" < /dev/null 2>/dev/null); then + REMOTE_STATE= + fi + REMOTE_STATE=$(printf '%s\n' "$REMOTE_STATE" | tail -1) + case "$REMOTE_STATE" in + alive) + if [ -n "$LOG_VERB" ]; then + LOG_STATE=$(map_log_state "$LOG_LINE") + if [ "$LOG_STATE" != unknown ]; then + emit "$LOG_STATE" status-log "$(status_line_note "$LOG_LINE")${SEP}remote endpoint alive on $REMOTE_HOST" + fi + fi + emit unknown remote-endpoint "alive on $REMOTE_HOST (an idle secondmate is healthy)" + ;; + dead|missing) + emit unknown remote-endpoint "remote endpoint $REMOTE_STATE on $REMOTE_HOST" + ;; + '') + emit unknown remote-endpoint "unknown-remote: $REMOTE_HOST unreachable or endpoint unreadable (not proof of death)" + ;; + *) + emit unknown remote-endpoint "unknown-remote: endpoint state '$REMOTE_STATE' on $REMOTE_HOST (not proof of death)" + ;; + esac +fi + # pane_readable is consulted ONLY in the no-run fallback below. The run-step path # stays authoritative regardless of pane liveness - judge by the run-step, not the # shell - so a finished crew whose endpoint has closed still reports its run-step diff --git a/bin/fm-decision-hold.sh b/bin/fm-decision-hold.sh index 886d99e65dc..28c6d8ddb1b 100755 --- a/bin/fm-decision-hold.sh +++ b/bin/fm-decision-hold.sh @@ -24,6 +24,11 @@ # fm-decision-hold.sh verify # fm-decision-hold.sh resolve \ # --decision-file --routed-to [--routed-to ...] +# fm-decision-hold.sh answer --decision-file +# fm-decision-hold.sh answers --source (keyed answers on stdin) +# fm-decision-hold.sh bind +# fm-decision-hold.sh unbind +# fm-decision-hold.sh binding # fm-decision-hold.sh decline --decision-file # fm-decision-hold.sh repair --decision-file # @@ -35,12 +40,13 @@ # `verify` is read-only and is called by scout teardown so teardown cannot erase a # source before this gate has succeeded. # -# `resolve` and `decline` close active holds; `repair` attests a hold already closed -# outside this script. All three paths require a non-empty captain decision file of -# at most 8192 bytes, record the same durable resolution block in the hold body, and -# store the decision digest plus routed identities so an exact retry is idempotent -# while a changed decision or, for `resolve`, routed set is rejected. New records -# include a `Resolution mode:` naming their path; older routed records remain valid. +# `resolve`, `answer`, and `decline` close active holds; `repair` attests a hold +# already closed outside this script. All four paths require a non-empty captain +# decision file of at most 8192 bytes, record the same durable resolution block in +# the hold body, and store the decision digest plus routed identities so an exact +# retry is idempotent while a changed decision or, for `resolve`, routed set is +# rejected. New records include a `Resolution mode:` naming their path; older +# routed records remain valid. # # `resolve` is the routed path. It requires every --routed-to task to exist and to # be blocked by the hold. It writes the captain decision and routed identities into @@ -49,6 +55,49 @@ # Successful resolve output also lists the routed identities with a pointer to the # policy owner's post-resolution review; that reminder is advisory, not a guard. # +# `answer` is the answer-time closure path, the hold ledger's counterpart to +# `fm-send.sh --resolve-key`: it exists so the act that carries the captain's +# answer is the act that closes the hold, instead of leaving closure to a +# separate later call nobody is forced to make. It records the captain's answer +# on an actively held hold, records `(none)` as the routed identities because no +# follow-up work has been routed behind the hold yet, and closes it. It shares +# every guard `decline` has, including the refusal while any task is still +# blocked by the hold, so a decision whose follow-up work is already routed still +# goes through `resolve` and the routed-vs-unrouted distinction survives. It says +# only that the captain answered; `decline` still says the captain answered with +# no follow-up work at all. +# +# ONE KEYED-ANSWER INTAKE, FED BY EVERY CHANNEL. +# "A keyed answer closes its matching hold" is a single capability, owned here +# and nowhere else. `answers` is its channel-agnostic entry point: it reads +# `\t\t