diff --git a/.agents/skills/lavish-decision-boards/SKILL.md b/.agents/skills/lavish-decision-boards/SKILL.md index 45bdb5542e..c7e68513ad 100644 --- a/.agents/skills/lavish-decision-boards/SKILL.md +++ b/.agents/skills/lavish-decision-boards/SKILL.md @@ -40,7 +40,13 @@ A Lavish board is a live surface, so reconcile it against live fleet state befor ## Serve and verify -1. Serve the board without auto-opening by passing `--no-open` or setting `LAVISH_AXI_NO_OPEN=1`. +1. Serve the board with `lavish-axi --no-open --no-gate`. + `--no-open` suppresses the auto-open, and `LAVISH_AXI_NO_OPEN=1` does the same. + `--no-gate` skips the open-time layout curtain, which is the difference between a working board and a blank one. + The curtain is CSS, not a hang: Lavish renders the shell with `body.layout-gate-active`, and `body.layout-gate-active iframe#artifact { opacity: 0 }` keeps the artifact invisible while the chrome paints, until the layout audit clears or a safety timer expires. + The gate is decided per request from the query string, so `?no-gate=1` on any already-served session URL uncurtains it with no re-serve; `?gate=0` is equivalent. + `--no-gate` sets no server-side state - it only stamps `?no-gate=1` onto the URL it prints, so the curtain returns the moment that parameter is dropped. + Carry the full printed URL, query string included, through every later step. 2. Extract the printed session URL without its surrounding double quotes. A safe extraction pattern is `grep -oE 'https?://[^ "]+'`; verify that no quote or punctuation trails the URL. 3. Open every board in its own dedicated Chrome window with `open -na "Google Chrome" --args --new-window ""`. @@ -49,6 +55,10 @@ A Lavish board is a live surface, so reconcile it against live fleet state befor Treat the board as not ready until the tool-authoritative connection signal confirms that it is genuinely connected. The transition to connected can take several minutes and is expected; during that lag, do not act on a poll return or prematurely re-serve, re-open, abandon, or otherwise thrash the session. Create and select a `chrome-devtools-axi` page for the exact served URL, then use a bounded retry to inspect that page's snapshots. + The artifact iframe is sandboxed `allow-scripts allow-forms allow-popups allow-downloads`, without `allow-same-origin`, so parent-frame JS evaluation can never reach into it. + Verify board content with a snapshot that crosses frames, never with an expression evaluated against the parent page. + Diagnose a board that looks blank with `curl -s | grep -o ' ยท Lavish` when the artifact HTML carries a `` and bare `Lavish Editor` when it does not, identically with and without the curtain. Pass only after the layout-audit-in-progress indicator has cleared and no layout-issue indicator is present, confirming zero error-severity `layout_warnings` for that board. A returned snapshot alone is not success; if the bound expires while the audit remains in progress, treat the board as unverified and do not surface it. Consult `chrome-devtools-axi --help` and the relevant command help for current commands and flags. @@ -59,8 +69,13 @@ A Lavish board is a live surface, so reconcile it against live fleet state befor ## Protect answers -- Answer preservation takes precedence over the serve-fresh rule while the captain has unsubmitted input. +- A board in front of the captain is theirs, and that ownership is absolute while it is open. +- Never navigate, reload, re-serve, or end it, never edit the file behind it, and never run browser automation against that window, including a read-only snapshot. +- Verification with `chrome-devtools-axi` belongs before the board is surfaced; once the captain has it, the window is off limits too. +- The test before any action is whether it could change what is on the captain's screen right now; if it could and their input may be unsubmitted, do not do it. +- Answer preservation takes precedence over the serve-fresh rule while the captain has unsubmitted input; a board showing slightly stale state costs one correction, while a cleared board costs the captain's answers outright. - Never edit, refresh, or reload a served board while the captain is answering because doing so clears in-progress input. +- Fix a problem with a live board server-side or not at all; a per-request option such as `?no-gate=1` is safe because it changes only what a fresh request renders, whereas re-serving or editing the file is not. - After submission, reconcile and refresh before continuing; if freshness must be preserved sooner, use only a strategy proven to retain the captain's current input without editing, refreshing, or reloading the served board. - When poll feedback arrives, write every annotation to the chosen durable file immediately, before interpreting it, acting on it, or doing anything else. - Never rely on poll output or conversation memory as the only copy because ephemeral poll output can be reaped. diff --git a/.agents/skills/memory-hygiene/SKILL.md b/.agents/skills/memory-hygiene/SKILL.md index ce0f3f2612..8485a254d6 100644 --- a/.agents/skills/memory-hygiene/SKILL.md +++ b/.agents/skills/memory-hygiene/SKILL.md @@ -15,9 +15,24 @@ This skill owns entry shape for `data/captain.md` and `data/learnings.md` only. `AGENTS.md` section 6 owns knowledge routing and the files' inspect-then-update, rewrite-in-place contract. The `/stow` skill owns session sweeps, and `firstmate-coding-guidelines` owns repository-wide knowledge placement and one-owner discipline. +## Promote or delete + +Learnings must live where they are read, so this is the first decision on every candidate entry, before any question of wording. + +- A rule that should change behavior belongs in the instruction surface that loads at the moment it applies: `AGENTS.md` when every session needs it, or the skill that loads at the triggering moment. + Promote it there and do not also keep it here. +- During `/stow`, `stow/SKILL.md` owns the exception to this routing: file the surfaced rule through that skill's graduation path, and scope any natural skill-owner change separately as deliberate firstmate repository work. +- Private memory holds only what cannot live in the shared repo: machine-specific and fleet-local facts such as service regions, credential locations, environment drift, and tool quirks specific to this box. +- Promote or delete is the default outcome for a candidate entry; keeping it is the exception that has to earn itself against those two. +- A general rule parked in a private file is the failure mode, not a safe backup. + Length is not diligence: a file nobody reads at the moment it matters lets its lessons be re-learned the expensive way. +- Convert an incident narrative to the durable fact plus a one-line `[[pointer]]`, or delete it. +- Delete stale and superseded entries outright rather than preserving them for safety. + ## Entry standard -- Record only the actionable rule or durable fact. +- Record only an actionable rule or evidence-backed durable fact, never an inference framed as capability. +- Require any asserted limit - "cannot", "impossible", "structural" - to cite the exact command and output that proved it and remain re-checkable as configs and permissions change; never write "do not retry" or an equivalent that suppresses falsification. - Strip emotion, emphasis, interpersonal interpretation, and incident drama. - Do not preserve that anyone was furious, shocked, disappointed, emphatic, or otherwise emotionally affected. - Preserve the operational instruction or evidence-backed fact that remains after the mood is removed. @@ -29,7 +44,7 @@ The `/stow` skill owns session sweeps, and `firstmate-coding-guidelines` owns re ## Update practice -1. Use `AGENTS.md` section 6 to confirm that the knowledge belongs in one of these private-memory files. +1. Apply "Promote or delete" first, then use `AGENTS.md` section 6 to confirm that what remains belongs in one of these private-memory files. 2. Inspect the current file and the candidate owning entry before writing. 3. Search nearby entries for overlap, stale wording, and superseded variants. 4. Reduce the new information to one actionable rule or durable fact. @@ -58,6 +73,8 @@ It drops mood, drama, chronology, and commentary that do not change the rule. Reject the update if any answer is yes: +- Is it a general rule that belongs in `AGENTS.md` or a skill that loads at the triggering moment? +- Does it assert a limit without citing the command and output that proved it, or tell a future session not to re-check? - Does it describe emotion or intensity instead of changing future action? - Does it retell an incident beyond one short example or pointer? - Does another entry already own the same rule or fact? diff --git a/.agents/skills/stuck-crewmate-recovery/SKILL.md b/.agents/skills/stuck-crewmate-recovery/SKILL.md index d8b1d1e20e..dce918c635 100644 --- a/.agents/skills/stuck-crewmate-recovery/SKILL.md +++ b/.agents/skills/stuck-crewmate-recovery/SKILL.md @@ -34,6 +34,13 @@ Handle permission evidence before the ordinary recovery ladder because approving 6. After the captain clears the dialog, verify that the pane changes or `bin/fm-crew-state.sh <id>` reports resumed work before returning to supervision. If the permission was denied and the task still cannot proceed, use the ordinary ladder below with that denial as evidence. +## Diagnose the layer before fixing + +Name the mechanism before applying any remedy, and treat "it is not working" as a symptom rather than a cause. +State the specific line, flag, call, or process producing the behavior; if you cannot state it, you are guessing. +Reading the tool's own code, header, or `--help` is cheaper than a round of speculative fixes, and each speculative fix destroys the evidence the next diagnosis needs and commonly breaks something that was working. +Prove the layer before acting on it: confirm a dead agent from the process tree rather than from elapsed quiet, and confirm a wedge from two samples rather than one. + For a non-permission stall, escalate in order: 1. Peek the pane. diff --git a/AGENTS.md b/AGENTS.md index ae43821cb5..0a52cc1a8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ Hard rules, in priority order: All are fast-forward operations, guarded gitignored-config propagation, or guarded local merges that never force, stash, or discard unlanded work. Project `AGENTS.md` maintenance is not another exception: firstmate records not-yet-committed project knowledge in `data/`, and crewmates update project `AGENTS.md` through normal delivery (section 6). 2. **Never merge a PR without the captain's explicit word.** - The one standing, captain-authorized relaxation is a project's `yolo` flag (section 7): with `yolo` on, firstmate makes routine approval decisions itself, but anything destructive, irreversible, or security-sensitive still escalates to the captain. + The one standing, captain-authorized relaxation is a project's `yolo` flag (section 7): with `yolo` on, firstmate also makes merge decisions itself, but anything destructive, irreversible, or security-sensitive still escalates to the captain. 3. **Never tear down a worktree that holds unlanded work.** `bin/fm-teardown.sh` enforces this, and `--force` only requests recursive cleanup after every ordinary safety proof succeeds. Three ways work counts as "landed": `HEAD` reachable from any remote-tracking branch (a fork counts, so an upstream-contribution PR pushed to a fork satisfies this in any mode); for a normal ship task, its PR merged with a head that contains the local work, or its content already present in the up-to-date default branch; for `local-only` ship tasks with no remote, merged into the local default branch. @@ -124,7 +124,7 @@ state/ volatile runtime signals; gitignored .afk durable away-mode flag; present = sub-supervisor may deliver escalations (set by /afk, cleared on user return) .lock per-home session lock written by fm-lock.sh: harness PID on line 1, holder process start time on line 2; direct readers parse only the first-line PID, while acquire/status treat it as held only when that PID is live with a matching start time (skipped for a legacy one-line lock, which is held while its PID is a live harness) and is not a Codex app-server .watch.lock .wake-queue.lock watcher singleton and queue serialization locks - .hash-* .count-* .stale-* .stale-since-* .paused-* .wedge-escalations-* .brief-started-* .seen-* .hb-surfaced-* .last-* .heartbeat-streak watcher internals; never touch + .hash-* .count-* .stale-* .stale-since-* .paused-* .wedge-escalations-* .brief-started-* .seen-* .hb-surfaced-* .last-* .heartbeat-streak .backlog-ready-ids watcher internals; never touch .watch-triage.log watcher's absorbed-wake debug log (size-capped); never relied on, safe to delete .last-watcher-beat watcher liveness beacon, touched every poll (including while absorbing benign wakes); guard scripts read it .subsuper-* .supervise-daemon.* sub-supervisor internals; never touch @@ -228,7 +228,7 @@ Exact best-score Codex ties and an all-unavailable quota result rotate across el The now-unreachable Agent Fleet pool-summary implementation and inactive new-lease fixtures are legacy code deferred to follow-up task `remove-fleet-routing-deadcode`. Quota trouble must never block dispatch. -Precedence, highest first: +Precedence is the captain's harness, model, and reasoning-tier dial, never firstmate's preference; apply it highest-first without substituting an unrequested profile that seems better suited: 1. An explicit per-task captain override, such as "run this one on codex" or "use haiku for this". 2. firstmate's best-fit rule from `config/crew-dispatch.json`. @@ -375,7 +375,7 @@ During a long Claude primary run, periodically load `stow` before compaction pre - `direct-PR` - push + open a PR via `gh-axi`, no pipeline -> captain merge. - `local-only` - local branch, no remote, no PR; firstmate reviews the diff, the captain approves, firstmate merges to local `main` (section 7). -Orthogonal to mode is an optional `+yolo` flag (`[direct-PR +yolo]`), default off and **not recommended**: with `yolo` on, firstmate makes the approval decisions itself instead of asking the captain (section 7). When the captain adds a project without saying, default to `no-mistakes` with yolo off; only set a faster mode or `+yolo` on the captain's explicit say-so. +Orthogonal to mode is an optional `+yolo` flag (`[direct-PR +yolo]`), default off and **not recommended**: with `yolo` on, firstmate also makes PR and local-only merge decisions itself instead of asking the captain (section 7). When the captain adds a project without saying, default to `no-mistakes` with yolo off; only set a faster mode or `+yolo` on the captain's explicit say-so. **Clone existing:** `git clone <url> projects/<name>`, add its registry line with the chosen mode, then initialize only if the mode is `no-mistakes`. @@ -439,6 +439,8 @@ Then classify readiness: - **Dispatchable:** no overlap with in-flight tasks. Dispatch immediately. There is no concurrency cap. - **Blocked:** touches the same files or subsystem as an in-flight task, or explicitly depends on an unmerged PR. Record it in `data/backlog.md` with `blocked-by: <id>` and tell the captain what work is waiting and why. Scout tasks are read-mostly and almost never block on anything. +A captain instruction whose second half waits on something finishing ("once X lands, do Y") is two work items: file the dependent half with `--blocked-by` the moment it is spoken, before doing the first half, because a conversation-only promise does not survive its blocker. + Keep dependency judgment coarse: same repo plus overlapping area means serialize; everything else runs parallel. For `no-mistakes` projects, the pipeline rebase step absorbs mild overlaps; for other modes, have the crewmate rebase before review or merge if needed. @@ -500,8 +502,8 @@ Evidence-hosting end-state (gists, an orphan evidence branch, or similar) is a d Firstmate's own repo is the exception: its `.no-mistakes/` stays gitignored, untracked local state, and CI rejects tracked `.no-mistakes` paths. This do-not-fight rule does not license evidence commits in firstmate's own repo. -**yolo (orthogonal).** With `yolo=off` (default) every approval is the captain's: ask-user findings, PR merges, the local-only merge. -With `yolo=on`, firstmate makes those calls itself without asking - resolve ask-user findings on your judgment, and run `bin/fm-pr-merge.sh <id> <full GitHub PR URL>` / `bin/fm-merge-local.sh` once the work is green/approved - EXCEPT anything destructive, irreversible, or security-sensitive, which still escalates to the captain. +**yolo (orthogonal).** With `yolo=off` (default), PR and local-only merge approval is the captain's; firstmate still resolves ordinary ask-user findings under section 9's blocker triage. +With `yolo=on`, firstmate also makes those merge calls itself without asking - run `bin/fm-pr-merge.sh <id> <full GitHub PR URL>` / `bin/fm-merge-local.sh` once the work is green/approved - EXCEPT anything destructive, irreversible, or security-sensitive, which still escalates to the captain. Never merge a red PR even under yolo. `bin/fm-pr-merge.sh` always records `pr=` and records `pr_head=` when available before merging, parses the full `https://github.com/<owner>/<repo>/pull/<n>` URL into `gh-axi pr merge <n> --repo <owner>/<repo>`, and defaults to `--squash` unless an explicit merge method is forwarded after `--`; this holds even on a repo with no PR CI where the "checks green" signal that normally triggers `bin/fm-pr-check.sh` never fires - do not call `gh-axi pr merge` directly for a task's PR, or the recording step can be silently skipped and a later `fm-teardown.sh` has nothing to verify a squash merge against. After any merge you perform without asking the captain, post a one-line "merged <full PR URL or local main> after checks passed" FYI so the captain keeps a trail. @@ -513,7 +515,7 @@ Load `harness-adapters` for the target harness's skill invocation form; natural The crewmate drives the no-mistakes pipeline (review, test, document, lint, push, PR, CI) itself. The ship brief intentionally does not restate no-mistakes gate mechanics; it points the crewmate to the version-matched SKILL.md loaded by `/no-mistakes`, `no-mistakes axi run --help`, and per-response `help` lines. -Firstmate's wrapper stays narrow: `ask-user` findings return through `needs-decision`, captain-owned decisions go back through `no-mistakes axi respond`, crewmate validation avoids `--yes`, and CI-green completion is reported as `done: PR {url} checks green`. +Firstmate's wrapper stays narrow: `ask-user` findings return through `needs-decision`, firstmate applies section 9's blocker triage and sends the answer back through `no-mistakes axi respond`, crewmate validation avoids `--yes`, and CI-green completion is reported as `done: PR {url} checks green`. That checks-green status is owed at the CI-ready return point, when `/no-mistakes` first reports CI green, not after the monitor-until-merge loop observes the PR merged or closed. Use chat for yes/no decisions; use lavish-axi when there are multiple findings or options to triage. @@ -557,8 +559,7 @@ A safety refusal after quiescence leaves the endpoint stopped while preserving a `bin/fm-teardown.sh`'s header owns the full landed-work definition (remote-reachable, merged-PR-head containment for the squash-merge-then-delete-branch flow, content already in the default branch, local-only merges) and the `pr=` discovery fallback for merges that skipped `bin/fm-pr-check.sh`. Known benign case: after an external-PR task, a squash merge leaves the branch commits reachable only on the contributor's fork; add the fork as a remote and fetch (`git remote add fork <fork url> && git fetch fork`), then retry - never reach for `--force`. A successful PR-based teardown also refreshes that project's clone through `bin/fm-fleet-sync.sh`, best-effort. -Then update the backlog using the teardown reminder: run `tasks-axi done` when the default tasks-axi backend is active and compatible, otherwise move the task to Done in `data/backlog.md` manually with the full `https://...` PR URL or local merge note and date and keep Done to the 10 most recent. -Re-evaluate the queue and dispatch only queued work whose blockers are gone and whose time/date gate, if any, has arrived. +Then record completion with the command teardown printed, and dispatch the due dependency-cleared IDs its read-only completion probe surfaced; under the manual backend, move the task to Done with the full `https://...` PR URL or local merge note and date and keep Done to the 10 most recent. ### Secondmate teardown (explicit only) @@ -632,16 +633,23 @@ On wake, in order of cheapness: 1. Read the reason line and drain queued wake records with `bin/fm-wake-drain.sh`. 2. `signal:` read the listed status files first; a wake lists every signal that landed within the coalescing grace window (e.g. a status write plus the same turn's turn-end marker), and each is ~30 tokens and usually sufficient. - A status line is the wake *event*, not the crewmate's current state; when you need the live state - especially to confirm a `needs-decision`/`blocked`/`paused` status is still real and not already resolved-and-resumed - read it with `bin/fm-crew-state.sh <id>`, which reconciles the authoritative run-step over the possibly-stale log line, and never `tail` the status log as the current-state source. + A status line is the wake *event*, not the crewmate's current state; when you need the live state - especially to confirm a `needs-decision`/`blocked`/`paused` status is still real and not already resolved-and-resumed - read it with `bin/fm-crew-state.sh <id>`, which reconciles the authoritative run-step over the possibly-stale log line. + Conversely, a terminal `failed` or `cancelled` run-step label describes the pipeline run, not the deliverable; before concluding the work failed, read the task's own last status line and check for an unmerged PR, because a cancelled run can leave finished, mergeable work behind. + That is one instance of a general rule: when a subsystem ships a status command, that command is the only acceptable source for its state, and hand-rolling a read of its status logs, dotfiles, lock paths, or PID files to infer state is a defect in the reader, not a diagnostic technique. + `bin/fm-lock.sh status` reports the session-lock holder, while `head -1 state/.watch.lock` fails because the watcher lock is a symlink to a transient owner directory; an error or empty read is not evidence of an unheld lock. 3. `stale:` the crewmate stopped without reporting, a recognized mid-run permission prompt is waiting, or a busy pane exceeded the possible system-dialog no-progress threshold. If the reason includes `permission-prompt detected` or `permission/system-dialog suspected`, load `stuck-crewmate-recovery` before taking any ordinary recovery action and follow its permission-blocked branch. Otherwise peek the pane (`bin/fm-peek.sh <window>`) to diagnose. If the stale reason includes `demand-deep-inspection`, inspect the pane, `bin/fm-crew-state.sh <id>`, and the validation logs before resuming supervision. If the pane is waiting, looping, confused, or unresponsive, load `stuck-crewmate-recovery`. 4. `check:` a per-task poll fired (usually a merge, or X mode when enabled); act on it. -5. `heartbeat:` a heartbeat wake now reaches you only when the watcher's bash fleet-scan caught a captain-relevant status the per-wake path missed (no-change heartbeats are absorbed in bash, never surfaced), so treat it as "something turned up" and review the whole fleet: start with `bin/fm-fleet-view.sh` for the structured overview, use `bin/fm-crew-state.sh <id>` only for targeted follow-up, peek panes that look off, check PR-ready tasks for merge, reconcile data/backlog.md, then resume the emitted supervision protocol. +5. `heartbeat:` a heartbeat wake now reaches you only when the watcher's bash fleet-scan caught newly ready backlog IDs or a captain-relevant status the per-wake path missed (no-change heartbeats are absorbed in bash, never surfaced), so treat it as "something turned up" and review the whole fleet: dispatch due IDs named in the wake, start with `bin/fm-fleet-view.sh`, read every task's own last status line and check for unmerged PRs before interpreting terminal run labels, use `bin/fm-crew-state.sh <id>` for targeted follow-up, peek panes that look off, reconcile data/backlog.md, then resume the emitted supervision protocol. Do not report that the fleet is unchanged. +On any of these, diagnose the layer before applying a fix: name the producing line, flag, call, or process and ask whether your own action causes the symptom, because a stop-clear-start reflex can kill what it just started and misread the duplicate as confirmation. +Before treating another observation as confirmation, state and rule out the innocent explanation and make a prediction that could fail; a theory that grows to absorb every observation is confirmation bias, while real diagnosis narrows. +If you cannot name the producing layer or a falsifiable prediction, you are guessing, and reading the tool's own code or `--help` is cheaper than a round of guesses. + When a task reaches a terminal state on any of these wakes (a `done`/merge `check:`, a `failed` signal, a scout report, a local-only merge), and X mode is enabled, load `fmx-respond` (section 13) and post the X-mode mention's **final** completion follow-up if that task is X-mode-linked: `bin/fm-x-followup.sh --check <id>` then `bin/fm-x-followup.sh <id> --final --text-file <path>`, so the link always clears here regardless of how many of the up-to-three follow-ups were already spent on earlier milestones. When any wake's status reports a merged PR naming a project this home also has cloned under `projects/`, run `bin/fm-fleet-sync.sh <project-name>` for that project as the low-latency fast path. The installed home-scoped `fm-checkout-refresh.sh` owner independently detects every tracked upstream-tip change, surfaces new or growing non-ignored untracked skill-draft inventories, and supplies the periodic backstop across projects, Treehouse backing checkouts, configured paths, and matching-origin top-level clones. @@ -670,9 +678,11 @@ On every verified primary harness, "no turn ends blind" has a structural backsto Watcher liveness is harness-aware. Do not assume one primary harness can use another harness's foreground or background shape. For example, Claude uses a background-notify cycle, while Codex intentionally uses bounded foreground checkpoints. -A crewmate driving its own `no-mistakes` validation still drives that gate loop synchronously and processes every return, never idle-waiting for its own validation run to advance on its own. +`no-mistakes axi run` is the blocking wait for a crewmate's own validation: re-enter it and process every return synchronously, because repeated `axi status` calls only poll, do not advance the run, and burn tokens. +A shared-socket timeout under known concurrent AXI clients is queue contention, not a daemon outage: leave the daemon untouched, re-enter `axi run` after 60, 120, then 240 seconds, and escalate only after about 10 minutes with the exact error and elapsed time. +Never park a crewmate on "firstmate is driving it" unless firstmate currently owns that live blocking loop; a check script only observes, so blocking firstmate on that observer deadlocks both sides. -Token discipline: for a crewmate's current state prefer `bin/fm-crew-state.sh <id>`, which looks for a branch-matched run-step before checking pane liveness, then falls back to the pane and log in that cheap-first order and treats the status log's last line as a wake event rather than the current state; default peeks to 40 lines; never stream a pane repeatedly through yourself; batch what you tell the captain. +Token discipline: default peeks to 40 lines; never stream a pane repeatedly through yourself; batch what you tell the captain. The context-% shown in a peek is not actionable as crewmate health; ignore it and intervene only on real signals (`signal`, `stale`, `needs-decision`, `blocked`), looping or confusion in the pane, or a question the brief already answers. ### Away-mode stub @@ -688,7 +698,7 @@ Inline facts that must survive without a loaded skill: - If firstmate receives a legacy marked message while afk is active, it is an internal escalation: stay afk and process it. - If the message starts with `/afk`, stay afk and refresh the flag. - Any other real user message means the captain is back: stop the daemon through `bin/fm-afk-launch.sh stop`, which clears `state/.afk` last, flush catch-up from `state/.wake-queue` and `state/.subsuper-escalations` plus any legacy `state/.subsuper-inject-wedged`, then resume the emitted primary-harness supervision protocol. -- Afk never changes approval authority; PR merges, ask-user findings, destructive actions, irreversible actions, and security-sensitive choices still require the same approval they required before. +- Afk never changes approval authority; PR and local-only merges, destructive actions, irreversible actions, and security-sensitive choices still require the same approval they required before, while ordinary ask-user findings remain firstmate's to resolve under section 9. - Bias ambiguous cases toward exit because a present captain beats token savings and a false exit is self-correcting. ### Stuck-crewmate recovery @@ -702,22 +712,39 @@ That playbook escalates from peek, to one-line steer, to harness-specific interr Report progress and completion against the captain's actual goal, which stays fixed unless the captain changes it. Never rewrite that goal into a weaker version and report the weaker one as met; when the actual goal is unmet, say "unmet" plainly. Firstmate earns nothing for claimed wins, so never optimize for claimable success - that instinct drives goal-stretching. +A success-shaped signal is not the outcome: verify the outcome's own signal before claiming it, never a thing that merely correlates with it. +Verified instances, each with the check it requires: + +- `gh-axi pr merge` reports ok on merge-queue *enqueue*; verify `merged: true`. +- Elastic Beanstalk shows the old version label while `Status=Updating`; read the label only after `Status=Ready`. +- A harness knob such as `codex -c model_reasoning_effort=` can exit 0 and silently run at `default`; verify the tier the running agent reports, not the launch exit code. +- `chrome-devtools-axi` drops the MCP `isError` flag, so failures print as success; check the operation's own result. +- A quiet step log is buffered output, not a wedge; prove liveness from the process tree across two samples before calling anything stuck. + Every captain-facing message describes the captain's work in plain language: what is being looked into, built, ready for review, blocked, or needing their decision. Never name firstmate internals in captain-facing messages: bootstrap, recovery, the session lock, the watcher, heartbeats, polling, "going quiet", crewmate, scout, ship, task ids, briefs, worktrees, status files, meta files, teardown, promotion, harness names such as pi or codex, context budgets, delivery-mode labels, or yolo labels. Translate, don't expose: say the project is blocked, ready, or needs a decision instead of describing the machinery that found it. +Firstmate is a tool, not a person: describe the captain's work, never `we`, `us`, or `our`. Before serving or updating any captain-facing board, status, or summary, reconcile it against live fleet state, including current crewmate states and what is done versus pending. Never render from a remembered snapshot; the instant a decision is actioned or work changes state, each actionable portion must reflect it by removing resolved actionable or decision items and showing only what is genuinely pending or in flight. Completion-oriented surfaces whose purpose is completed work, including the Recently Landed section of `/bearings` and `/reports`, retain relevant completion history instead of applying this removal rule. +A surface already in front of the captain is theirs, and that ownership is absolute: once served, never navigate, reload, re-serve, edit the file behind, or run browser automation against it while their input may be unsubmitted. +Fix such a surface server-side or leave it alone; the serve-fresh rule above yields to this one, and the test is whether the change could alter what is on the captain's screen right now. +Write input arriving through any reapable channel - poll output, background task output, a returned tool payload - to a durable file before interpreting it, acting on it, or replying to it. Operating fundamentals #7 owns the check-before-acting bar. Reaches the captain immediately: - Work ready for review, with the full PR URL. - Finished investigation findings, relayed as findings and not just "it's done". -- Review findings that need the captain's decision, relayed verbatim unless routine approval is authorized on firstmate judgment. -- A genuine captain-owned decision only: a product or brand call; something destructive, irreversible, or security-sensitive; a true external blocker; or a needed credential or login. -- A blocker or failure reaches this bar only after directing the crewmate to root-cause and implement a fix, iterating until it is genuinely solved or the crewmate's capability is truly exhausted. -- `This is hard` or `the task is failing` is not an escalation trigger; get it working through the crewmate first. +- For a crewmate's `blocked:`, unblocking is the default; its escalation exception has exactly three members: an outgoing message to a person, spending money, or a significant UI/UX change. +- Standing approval gates for merges, production, destructive or irreversible actions, and security-sensitive work still bound what firstmate is authorized to do; within those bounds, engineering tradeoffs, review or gate findings, in-scope questions, missing config, and stale environments are firstmate's to resolve. +- Triage in order: trace a reported missing config or credential from its consumer to its source before believing it absent - env and secret names differ, and per-org credentials may live in ClickHouse `org_configs`; only a dead-end trace is a blocker, named with the exact secret and consumer. +- Next perform any ordinary lane action already authorized, such as a dev deploy, an authorized green merge, or a pipeline response, and state its limit in the same instruction - dev is not production. +- Only when one of the three exceptions remains, escalate with the root-cause work already done and one specific question; `this is hard`, a failed run, or filing a defect is not a substitute for unblocking the work. + +A qualified answer is not an approval: when the captain's comment describes something different from the option they selected, the comment wins and the decision is not made. +Go back and ask rather than acting on the selection alone, most sharply when the pending action is irreversible. Does not reach the captain: auto-fixes, retries, routine progress, or firstmate's internal vocabulary and machinery. Batch non-urgent updates into your next natural reply. @@ -747,7 +774,7 @@ Update the backlog on every dispatch, completion, and decision for a work item. - [x] <id> - <one line> - data/<id>/report.md (reported <date>) ``` -Re-evaluate Queued on every teardown and every heartbeat: anything whose blocker is gone and whose time/date gate, if any, has arrived gets dispatched. +Re-evaluate Queued on every teardown and every heartbeat: teardown surfaces IDs that completion would unblock, the watcher wakes on newly ready IDs, and anything whose time/date gate has arrived gets dispatched. A tracked `.tasks.toml` at this repo root pins the default `tasks-axi` markdown backend to `data/backlog.md`, with `done_keep = 10` and an archive at `data/done-archive.md`. The local, gitignored `config/backlog-backend` file is the explicit opt-out knob. diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index eb0ba38faf..130a248323 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -257,8 +257,8 @@ The report is the only thing that survives, so anything worth keeping must be in append \`needs-decision: {summary of options}\` and stop. Firstmate will reply with the decision. When firstmate replies or a blocker clears and you resume, append \`resolved: {how it was decided or unblocked}\` (add the same \`[key=<slug>]\` if you opened it with one) so the decision or blocker is durably closed and does not keep resurfacing. 7. Never stop, restart, or update the shared \`no-mistakes\` daemon - it is one instance serving - every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes - daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon. + every lane/home, so restarting it kills in-flight pipeline runs in other lanes; only firstmate manages it. + Under known concurrent AXI runs, a socket I/O timeout is contention, not an outage: leave the daemon alone, re-enter no-mistakes axi run after 60, 120, then 240 seconds, and report blocked only after about 10 minutes with the exact error and elapsed time. # Definition of done Write your findings to \`$DATA/$ID/report.md\`. @@ -347,7 +347,7 @@ Do not hand-edit, commit, or fix findings yourself while a run is active - the p Two firstmate-specific rules layer on top of that guidance: - ask-user findings are not yours to answer: escalate to firstmate (rule 6) and stop. When the decision comes back, feed it to the gate with \`no-mistakes axi respond\` and let the pipeline apply it - do not route the question to "the user" or implement the fix yourself. -- Avoid \`--yes\`: the captain, not you, owns the ask-user decisions it would silently auto-resolve. +- Avoid \`--yes\`: report \`needs-decision\` to firstmate, which applies the blocker triage in AGENTS.md section 9; all safety and approval boundaries still apply. After /no-mistakes reports CI green (the CI-ready return point - do not wait for it to keep monitoring in the background until merge), append \`done: PR {url} checks green\` and stop. You are finished. EOF @@ -390,12 +390,12 @@ $RULE1 a scheduled window): firstmate then leaves your idle pane alone and rechecks it on a long cadence instead of treating it as a possible wedge. Use \`blocked:\` when you are stuck and need help. 5. If you hit the same obstacle twice, append \`blocked: {why}\` and stop; firstmate will help. -6. If a decision belongs to a human (product choices, destructive actions, ask-user findings), - append \`needs-decision: {summary of options}\` and stop. Firstmate will reply with the decision. +6. If you encounter a product choice, destructive action, or ask-user finding, + append \`needs-decision: {summary of options}\` and stop. Firstmate applies the blocker triage in AGENTS.md section 9 and replies with the decision. When firstmate replies or a blocker clears and you resume, append \`resolved: {how it was decided or unblocked}\` (add the same \`[key=<slug>]\` if you opened it with one) so the decision or blocker is durably closed and does not keep resurfacing. 7. Never stop, restart, or update the shared \`no-mistakes\` daemon - it is one instance serving - every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes - daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon. + every lane/home, so restarting it kills in-flight pipeline runs in other lanes; only firstmate manages it. + Under known concurrent AXI runs, a socket I/O timeout is contention, not an outage: leave the daemon alone, re-enter no-mistakes axi run after 60, 120, then 240 seconds, and report blocked only after about 10 minutes with the exact error and elapsed time. $REPORT_CONTRACT diff --git a/bin/fm-project-mode.sh b/bin/fm-project-mode.sh index 666292afbb..fc2b79eea6 100755 --- a/bin/fm-project-mode.sh +++ b/bin/fm-project-mode.sh @@ -12,8 +12,8 @@ # no-mistakes full pipeline -> PR -> captain merge (default) # direct-PR push + PR via gh-axi, no pipeline -> captain merge # local-only local branch, no remote/PR -> firstmate review -> captain approve -> local merge -# yolo (orthogonal) = when on, firstmate makes approval decisions itself (PR merges, -# ask-user findings, local-only merge approval) without checking the captain - except +# yolo (orthogonal) = when on, firstmate also makes PR and local-only merge +# decisions without checking the captain - except # anything destructive/irreversible/security-sensitive, which still escalates. # # An unknown/missing project or unknown mode falls back to "no-mistakes off" and warns diff --git a/bin/fm-tasks-axi-lib.sh b/bin/fm-tasks-axi-lib.sh index 54d02fcc9e..0456c3ca3b 100644 --- a/bin/fm-tasks-axi-lib.sh +++ b/bin/fm-tasks-axi-lib.sh @@ -74,3 +74,34 @@ fm_tasks_axi_backend_available() { fm_backlog_backend_manual "$config_dir" && return 1 fm_tasks_axi_compatible } + +# Print the sorted IDs from tasks-axi's ready view for one explicit backlog. +fm_tasks_axi_ready_ids() { + local backlog_file=$1 output + [ -f "$backlog_file" ] && [ ! -L "$backlog_file" ] || return 0 + output=$(tasks-axi ready --file="$backlog_file" 2>/dev/null) || return 1 + printf '%s\n' "$output" \ + | sed -n 's/^ \([^,][^,]*\),.*/\1/p' \ + | LC_ALL=C sort -u +} + +# Report only IDs that would become ready if <id> were marked Done. +# The real backlog stays untouched: the mutation runs against a sibling copy. +fm_tasks_axi_ready_after_done() { + local backlog_file=$1 id=$2 before after probe_file probe_status=0 + [ -f "$backlog_file" ] && [ ! -L "$backlog_file" ] || return 0 + before=$(fm_tasks_axi_ready_ids "$backlog_file") || return 1 + probe_file=$(mktemp "$(dirname "$backlog_file")/.backlog-ready-probe.XXXXXX") || return 1 + cp "$backlog_file" "$probe_file" || probe_status=$? + if [ "$probe_status" -eq 0 ]; then + tasks-axi "done" "$id" --file="$probe_file" --note "completion readiness probe" --no-prune >/dev/null 2>&1 \ + || probe_status=$? + fi + if [ "$probe_status" -eq 0 ]; then + after=$(fm_tasks_axi_ready_ids "$probe_file") || probe_status=$? + fi + rm -f "$probe_file" + [ "$probe_status" -eq 0 ] || return "$probe_status" + awk 'NR == FNR { if (NF) before[$0] = 1; next } NF && !before[$0]' \ + <(printf '%s\n' "$before") <(printf '%s\n' "$after") +} diff --git a/bin/fm-teardown.sh b/bin/fm-teardown.sh index 00567b1823..0d0b728d66 100755 --- a/bin/fm-teardown.sh +++ b/bin/fm-teardown.sh @@ -2,7 +2,7 @@ # Tear down a finished task: return the treehouse worktree, release the Orca # worktree, or retire a secondmate home; kill the recorded runtime endpoint, # clear volatile state, refresh/prune the project's clone for PR-based ship -# tasks, then print a backlog-refresh reminder for ship and scout teardowns +# tasks, then surface backlog completion and newly-ready work for ship and scout teardowns # (a secondmate teardown prints none, since secondmates are not backlog items). # REFUSES if the worktree holds work that has not LANDED, because cleanup # hard-resets/removes the worktree and kills its processes. Work has landed when it is @@ -937,8 +937,8 @@ work_is_landed() { content_in_default } -backlog_refresh_reminder() { - local pr done_cmd report_path +backlog_completion_surface() { + local pr done_cmd report_path ready_ids ready_line [ "$KIND" = secondmate ] && return 0 if fm_tasks_axi_backend_available "$CONFIG"; then case "$KIND" in @@ -959,10 +959,24 @@ backlog_refresh_reminder() { fi ;; esac - printf '%s\n' "Backlog: $ID just finished. Run $done_cmd, then run tasks-axi ready for dependency-cleared candidates, check date gates, and dispatch only work whose blockers are gone and date is due." + printf '%s\n' "Backlog: $ID just finished. Run $done_cmd; dispatch only work whose blockers are gone and date is due." else printf '%s\n' "Backlog: $ID just finished. Update data/backlog.md - move $ID to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due." fi + if fm_tasks_axi_compatible; then + if ready_ids=$(fm_tasks_axi_ready_after_done "$DATA/backlog.md" "$ID"); then + if [ -n "$ready_ids" ]; then + ready_line=$(printf '%s\n' "$ready_ids" | paste -sd ' ' -) + printf '%s\n' "Dependency-cleared queued work when $ID is recorded Done: $ready_line. Check date gates, then dispatch every due item." + else + printf '%s\n' "Dependency-cleared queued work when $ID is recorded Done: none." + fi + else + printf '%s\n' "Dependency-cleared queued work: unavailable because the read-only completion probe failed; inspect tasks-axi before ending this teardown turn." + fi + else + printf '%s\n' "Dependency-cleared queued work: unavailable because compatible tasks-axi is not on PATH; inspect Queued before ending this teardown turn." + fi } registry_home_for_line() { @@ -4850,4 +4864,4 @@ if [ "$KIND" != scout ] && [ "$KIND" != secondmate ] && [ "$MODE" != local-only "$FM_ROOT/bin/fm-fleet-sync.sh" "$PROJ" || true fi echo "teardown $ID complete (window $T, worktree $WT)" -backlog_refresh_reminder +backlog_completion_surface diff --git a/bin/fm-watch.sh b/bin/fm-watch.sh index 3392ba7c1c..347a4015f3 100755 --- a/bin/fm-watch.sh +++ b/bin/fm-watch.sh @@ -48,6 +48,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}" FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}" STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}" +DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}" CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}" # shellcheck source=bin/fm-gate-refuse-lib.sh . "$SCRIPT_DIR/fm-gate-refuse-lib.sh" @@ -64,6 +65,8 @@ mkdir -p "$STATE" # shellcheck source=bin/fm-wake-lib.sh . "$SCRIPT_DIR/fm-wake-lib.sh" +# shellcheck source=bin/fm-tasks-axi-lib.sh +. "$SCRIPT_DIR/fm-tasks-axi-lib.sh" # Shared wake classifier (captain-relevant verbs + signal/stale/heartbeat # predicates), the SAME library the away-mode daemon uses, so the triage policy # has one definition. @@ -705,6 +708,36 @@ safe_touch_marker_or_log() { # <path> <label> return 1 } +write_backlog_ready_snapshot() { # <ready-ids> + local ready_ids=$1 marker="$STATE/.backlog-ready-ids" tmp + tmp=$(mktemp "$STATE/.backlog-ready-ids.XXXXXX") || return 1 + printf '%s' "$ready_ids" > "$tmp" || { rm -f "$tmp"; return 1; } + safe_marker_path "$marker" || { rm -f "$tmp"; return 1; } + mv "$tmp" "$marker" +} + +# Print only IDs newly present in tasks-axi's ready view since the last heartbeat. +# An absent snapshot establishes a baseline instead of waking for the whole queue. +backlog_ready_additions() { # <current-ready-ids> + local current=$1 marker="$STATE/.backlog-ready-ids" previous added + if ! safe_marker_path "$marker"; then + safe_touch_marker_or_log "$marker" "backlog-ready snapshot" || true + return 1 + fi + if [ ! -e "$marker" ]; then + write_backlog_ready_snapshot "$current" || true + return 1 + fi + previous=$(cat "$marker" 2>/dev/null || true) + added=$(awk 'NR == FNR { if (NF) before[$0] = 1; next } NF && !before[$0]' \ + <(printf '%s\n' "$previous") <(printf '%s\n' "$current")) + if [ -z "$added" ]; then + write_backlog_ready_snapshot "$current" || true + return 1 + fi + printf '%s\n' "$added" | paste -sd ' ' - +} + marker_due() { # <path> <interval-seconds> <label> local marker=$1 interval=$2 label=$3 if ! safe_marker_path "$marker"; then @@ -1237,15 +1270,34 @@ EOF hb=$(( HEARTBEAT * (1 << streak) )) [ "$hb" -gt "$HEARTBEAT_MAX" ] && hb=$HEARTBEAT_MAX if marker_due "$STATE/.last-heartbeat" "$hb" "watcher heartbeat"; then + current_ready_ids= + ready_ids= + ready_changed=0 + if fm_tasks_axi_backend_available "$CONFIG" \ + && current_ready_ids=$(fm_tasks_axi_ready_ids "$DATA/backlog.md") \ + && ready_ids=$(backlog_ready_additions "$current_ready_ids"); then + ready_changed=1 + fi # Triage: in always-on mode a heartbeat is benign unless the cheap fleet-scan - # turns up a captain-relevant status the per-wake path missed. Absorb the + # turns up newly-ready backlog work or a captain-relevant status the per-wake path missed. Absorb the # no-change case (advance the schedule and back off exactly as wake() would, # without exiting); the away-mode daemon, when present, owns triage and wants # every heartbeat. if afk_present; then - fm_wake_append heartbeat heartbeat heartbeat || exit 1 + reason=heartbeat + if [ "$ready_changed" -eq 1 ]; then + reason="heartbeat: dependency-cleared queued work: $ready_ids" + fi + fm_wake_append heartbeat heartbeat "$reason" || exit 1 safe_touch_marker_or_log "$STATE/.last-heartbeat" "watcher heartbeat" || true - wake "heartbeat" + [ "$ready_changed" -eq 0 ] || write_backlog_ready_snapshot "$current_ready_ids" || true + wake "$reason" + elif [ "$ready_changed" -eq 1 ]; then + reason="heartbeat: dependency-cleared queued work: $ready_ids" + fm_wake_append heartbeat backlog-ready "$reason" || exit 1 + safe_touch_marker_or_log "$STATE/.last-heartbeat" "watcher heartbeat" || true + write_backlog_ready_snapshot "$current_ready_ids" || true + wake "$reason" elif heartbeat_scan_finds_actionable; then # Backstop: a captain-relevant status the per-wake path absorbed by mistake. # Enqueue first, then mark every captain-relevant status surfaced so the next diff --git a/tests/fm-teardown.test.sh b/tests/fm-teardown.test.sh index c23750472b..c9105dab1d 100755 --- a/tests/fm-teardown.test.sh +++ b/tests/fm-teardown.test.sh @@ -295,6 +295,24 @@ if [ "${1:-}" = mv ] && [ "${2:-}" = --help ]; then printf '%s\n' 'usage: tasks-axi mv <id> [<id>...] --to <path-or-dir>' exit 0 fi +file= +for arg in "$@"; do + case "$arg" in --file=*) file=${arg#--file=} ;; esac +done +if [ "${1:-}" = ready ]; then + if [ -n "$file" ] && grep -F '# completion-readiness-probe' "$file" >/dev/null 2>&1; then + printf '%s\n' 'count: 1' 'ready[1]{id,state,kind,repo,title}:' \ + ' follow-up,queued,ship,firstmate,"Dependent follow-up"' + else + printf '%s\n' 'count: 0' 'ready[0]{id,state,kind,repo,title}:' + fi + exit 0 +fi +if [ "${1:-}" = done ]; then + [ -n "$file" ] || exit 2 + printf '%s\n' '# completion-readiness-probe' >> "$file" + exit 0 +fi exit 0 SH chmod +x "$case_dir/fakebin/tasks-axi" @@ -768,23 +786,28 @@ test_local_only_fork_remote_allows() { pass "local-only worktree with HEAD on a fork remote is torn down (fix holds)" } -test_teardown_prompts_tasks_axi_done_when_compatible() { +test_teardown_surfaces_dependency_cleared_work_when_compatible() { local case_dir out case_dir=$(make_case tasks-axi-reminder) write_meta "$case_dir" no-mistakes ship printf '%s\n' 'pr=https://github.com/example/repo/pull/7' >> "$case_dir/state/task-x1.meta" + mkdir -p "$case_dir/data" + printf '%s\n' '# backlog-v1' '## In flight' '- [ ] task-x1 - Current task' \ + '## Queued' '- [ ] follow-up - Dependent follow-up blocked-by: task-x1' > "$case_dir/data/backlog.md" add_compatible_tasks_axi "$case_dir" out=$(run_teardown "$case_dir") || fail "teardown failed with compatible tasks-axi" printf '%s\n' "$out" | grep -F 'tasks-axi done task-x1 --pr https://github.com/example/repo/pull/7' >/dev/null \ || fail "teardown did not prompt tasks-axi done: $out" - printf '%s\n' "$out" | grep -F 'tasks-axi ready' >/dev/null \ - || fail "teardown did not prompt tasks-axi ready: $out" - printf '%s\n' "$out" | grep -F 'check date gates' >/dev/null \ - || fail "teardown did not preserve date-gate check: $out" + printf '%s\n' "$out" | grep -F 'Dependency-cleared queued work when task-x1 is recorded Done: follow-up.' >/dev/null \ + || fail "teardown did not surface the dependency-cleared follow-up: $out" + printf '%s\n' "$out" | grep -F 'Check date gates' >/dev/null \ + || fail "teardown did not preserve the date-gate check: $out" printf '%s\n' "$out" | grep -F 'keep Done to the 10 most recent' >/dev/null \ && fail "teardown kept manual Done pruning in compatible tasks-axi prompt: $out" - pass "teardown prompts tasks-axi backlog refresh when compatible" + grep -F '# completion-readiness-probe' "$case_dir/data/backlog.md" >/dev/null \ + && fail "teardown mutated the real backlog while probing readiness" + pass "teardown surfaces dependency-cleared work without mutating the backlog" } test_teardown_manual_backend_prompts_hand_edit_even_when_tasks_axi_present() { @@ -5094,7 +5117,7 @@ if [ "${FM_TEST_FOCUSED:-}" = preserve-scratch ]; then fi test_local_only_fork_remote_allows -test_teardown_prompts_tasks_axi_done_when_compatible +test_teardown_surfaces_dependency_cleared_work_when_compatible test_teardown_manual_backend_prompts_hand_edit_even_when_tasks_axi_present test_local_only_truly_unpushed_refuses test_local_only_merged_to_local_main_allows diff --git a/tests/fm-watch-triage.test.sh b/tests/fm-watch-triage.test.sh index 308d19bf0c..2c144b3ef7 100755 --- a/tests/fm-watch-triage.test.sh +++ b/tests/fm-watch-triage.test.sh @@ -1504,6 +1504,64 @@ test_heartbeat_backstop_surfaces_unsurfaced_status() { pass "heartbeat backstop fail-safe surfaces a captain-relevant status the per-wake path missed" } +test_heartbeat_surfaces_newly_ready_backlog_work() { + local dir state fakebin config data out drain_out ready_ids pid + dir=$(make_case heartbeat-backlog-ready); state="$dir/state"; fakebin="$dir/fakebin" + config="$dir/config"; data="$dir/data"; out="$dir/watch.out"; drain_out="$dir/drain.out" + ready_ids="$dir/ready-ids" + mkdir -p "$config" "$data" + printf '%s\n' '# backlog-v1' '## Queued' > "$data/backlog.md" + printf '%s\n' baseline > "$ready_ids" + cat > "$fakebin/tasks-axi" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = --version ]; then printf '%s\n' '0.2.2'; exit 0; fi +if [ "${1:-}" = update ] && [ "${2:-}" = --help ]; then + printf '%s\n' 'usage: tasks-axi update <id> --archive-body' + exit 0 +fi +if [ "${1:-}" = mv ] && [ "${2:-}" = --help ]; then + printf '%s\n' 'usage: tasks-axi mv <id> [<id>...] --to <path-or-dir>' + exit 0 +fi +if [ "${1:-}" = ready ]; then + count=$(awk 'NF { n++ } END { print n + 0 }' "$FM_FAKE_READY_IDS_FILE") + printf 'count: %s\nready[%s]{id,state,kind,repo,title}:\n' "$count" "$count" + while IFS= read -r id; do + [ -n "$id" ] || continue + printf ' %s,queued,ship,firstmate,"Ready task"\n' "$id" + done < "$FM_FAKE_READY_IDS_FILE" + if [ "${FM_FAKE_MUTATE_READY_AFTER_READ:-0}" = 1 ] \ + && grep -Fx follow-up "$FM_FAKE_READY_IDS_FILE" >/dev/null; then + printf '%s\n' baseline later > "$FM_FAKE_READY_IDS_FILE" + fi + exit 0 +fi +exit 1 +SH + chmod +x "$fakebin/tasks-axi" + + PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" \ + FM_CONFIG_OVERRIDE="$config" FM_FAKE_READY_IDS_FILE="$ready_ids" \ + FM_FAKE_MUTATE_READY_AFTER_READ=1 \ + FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=1 FM_HEARTBEAT_MAX=1 \ + "$WATCH" > "$out" & + pid=$! + wait_file_value "$state/.backlog-ready-ids" baseline 40 \ + || { reap "$pid"; fail "heartbeat did not establish the ready-work baseline"; } + printf '%s\n' baseline follow-up > "$ready_ids" + wait_for_exit "$pid" 40 \ + || { reap "$pid"; fail "heartbeat did not surface newly-ready backlog work"; } + grep -Fx 'heartbeat: dependency-cleared queued work: follow-up' "$out" >/dev/null \ + || fail "heartbeat did not name the newly-ready backlog item: $(cat "$out")" + FM_STATE_OVERRIDE="$state" "$DRAIN" > "$drain_out" 2>/dev/null \ + || fail "drain after backlog-ready heartbeat failed" + grep -F 'dependency-cleared queued work: follow-up' "$drain_out" >/dev/null \ + || fail "backlog-ready heartbeat was not queued durably" + [ "$(cat "$state/.backlog-ready-ids")" = "$(printf '%s\n' baseline follow-up)" ] \ + || fail "heartbeat did not persist the ready snapshot used for detection" + pass "heartbeat surfaces and persists the observed newly-ready backlog snapshot" +} + # --- beacon stays fresh while absorbing ------------------------------------- test_beacon_stays_fresh_while_absorbing() { @@ -1739,6 +1797,11 @@ if [ "${FM_TEST_FOCUSED:-}" = failure-pause ]; then exit 0 fi +if [ "${FM_TEST_FOCUSED:-}" = backlog-ready-snapshot ]; then + test_heartbeat_surfaces_newly_ready_backlog_work + exit 0 +fi + test_signal_reason_is_actionable_classifier test_stale_is_terminal_classifier test_scan_captain_relevant_statuses_classifier @@ -1776,6 +1839,7 @@ test_nonterminal_stale_repairs_missing_or_corrupt_timer test_triage_log_size_cap_accepts_spaced_wc_counts test_heartbeat_no_change_absorbed test_heartbeat_backstop_surfaces_unsurfaced_status +test_heartbeat_surfaces_newly_ready_backlog_work test_beacon_stays_fresh_while_absorbing test_afk_present_reverts_watcher_to_one_shot test_afk_paused_changed_pane_hands_off_plain_stale