Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .agents/skills/decision-hold-lifecycle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<decision-key>` 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 <source-id> <origin-id>`; 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.
Expand All @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion .agents/skills/firstmate-coding-guidelines/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<subject>.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.
Expand Down
12 changes: 11 additions & 1 deletion .agents/skills/process-event-sources/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <artifact.html>
```

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.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/stuck-crewmate-recovery/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The target window's harness is recorded as `harness=` in `state/<id>.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 <id> --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.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/updatefirstmate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down
38 changes: 33 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:

jobs:
lint:
name: Lint shell scripts
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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' \
'<!-- Points Claude at AGENTS.md via import; edit AGENTS.md, not this file. -->' \
'@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: |
Expand Down
8 changes: 5 additions & 3 deletions .no-mistakes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.

Expand Down
1 change: 0 additions & 1 deletion CLAUDE.md

This file was deleted.

2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- Points Claude at AGENTS.md via import; edit AGENTS.md, not this file. -->
@AGENTS.md
Loading
Loading