sync fork - #2432
Closed
Patrik-karlsson wants to merge 8 commits into
Closed
Conversation
* fix: pin firstmate's no-mistakes gate agent to neutralizing agents no-mistakes refuses to launch a gate agent that cannot be stripped of the target repository's AGENTS.md/CLAUDE.md, and only codex, claude, and pi carry a verified neutralization knob. This repo's `disable_project_settings: true` therefore only works with one of those three: any other machine-wide agent does not weaken validation here, it stops it from starting at all. Pin `agent` in the tracked .no-mistakes.yaml so the repo's own requirement travels with the repo instead of depending on each contributor's machine config. An ordered list is used rather than a single value: agent resolution filters the list to the installed agents, and the fallback wrapper reports neutralizing only if EVERY member does, so any resolvable subset of these three is safe while a single pinned value would hard-fail for a contributor who does not have that one agent. Add the invariant as an executable contract in the suite that already owns this boundary, replacing that file's stale header claim of an assertion it never made, and select that suite when .no-mistakes.yaml changes so a later edit cannot reintroduce a non-neutralizing agent unguarded. * docs: state the gate-agent pin's trust boundary precisely The pin is a code-executing selection field: no-mistakes reads it from the default-branch copy only while allow_repo_commands stays false, whereas disable_project_settings is taken from the trusted copy unconditionally. Saying the two are trusted-only "on the same terms" claims a stronger guarantee than EffectiveRepoConfig gives.
* feat: capture non-blocking friction across tasks Firstmate's status vocabulary had no category for "I hit resistance and kept going", so an impediment that did not block a worker survived only if firstmate happened to read the report and relay it. Add a `friction: [sig=<slug>] <observation>` verb over the keyed-status machinery fm-classify-lib.sh already implements, accumulate it per signature in data/friction/, and surface only signatures that recurred across two or more distinct tasks. One task hitting the same thing repeatedly is usually one worker looping; two tasks independently hitting it is a system problem. The verb is transparent rather than inert-by-omission. last_status_line now skips friction lines, so a friction append can neither mask a `done:` from the fleet scan nor turn a provably-working crew into an unknown one; fm-crew-state and the fleet snapshot read through that one owner instead of their own tails. Records outlive the task that reported them, which is the point, so teardown folds them into the durable store before removing a status log. That call is best-effort and prints a notice on failure: friction never gates teardown. Every rendering states surfaced, suppressed, and unclassified, including when all three are zero, because a quiet section that cannot be told apart from a blind one is the failure this exists to catch. Bearings carries them as flat scalars, since its TOON encoder renders a nested object as an opaque blob. Signatures naming a containment guard are never batched and offer only `keep` or `narrow`. This mechanism ranks by how often something impedes work, which is the right signal for a broken helper and the wrong one for a guard: without the carve-out its natural output is a prioritised list of security controls to remove. Triage files nothing. Both `clear` and `keep` produce a drafted issue for the captain, and a cancelled draft returns the signature to `surfaced` - rejecting a draft rejects the wording, not the finding. * fix: keep a friction append from waking supervision like a status Friction is non-blocking by contract, but it still grows the status file, and the watcher's change detector is a size:mtime signature - so every friction append woke firstmate exactly like a real status change. A mechanism built to remove noise that costs a turn each time a worker uses it is worse than not having it. Add status_delta_is_friction_only, which reads only the bytes appended since the size already recorded in that file's .seen-* signature and reports whether they are friction and nothing else. The watcher absorbs such a wake before the captain-verb test, advancing the suppressor so it cannot re-fire. Narrow on purpose, and fail-closed everywhere it is unsure: a turn-end marker, a first sighting with no recorded size, a shrunk or unreadable file, a torn write, or any non-friction content in the delta all surface as before. A `needs-decision` landing in the same poll window as a friction line is never swallowed. The absorb also deliberately skips mark_surfaced, so a captain-relevant line underneath stays eligible for the heartbeat backstop. Away mode is untouched: the check sits after afk_present, so the daemon's every-wake contract still holds. last_status_line is on that same per-poll path, so its per-line work is now two glob screens instead of a parameter substitution plus a function call on every line; an ordinary working:/done: line matches neither. Also render guard signatures in their own section rather than ranked beside the ordinary patterns, so "never batched" is enforced by the output instead of by whoever reads it, and keep the durable record free of the project's absolute path by storing the project name. * fix(bin): read the friction-only signal set without an unused field Take the seen-file and status path directly off the record with parameter expansion instead of a three-variable read whose middle field is never used, and quote the absorb note so it is not read as arithmetic. * test: pin the friction-only delta test's fail-closed cases The watcher absorbs a wake on this predicate's word, so its safety property is that every uncertain case reports "not friction-only" and the wake surfaces. Cover a mixed delta, a first sighting with no recorded size, no growth, a shrunk file, and a missing file - the cases the end-to-end watcher test cannot reach. * docs(skills): stop restating what the friction script header already owns The one-owner rule allows a cross-reference, not a second copy: the threshold mechanic, the count definitions, and the settled-record semantics belong to bin/fm-friction.sh --help. Keep only what firstmate needs to decide with, plus the one-line reinforcement at the point where routing around a guard refusal is the tempting mistake. * docs(architecture): record the friction absorb as a wake-triage boundary The maintainer-architecture owner describes how the watcher and daemon classify wakes; friction adds a signal class with its own absorb rule and fail-closed edges, which belongs there rather than only in the script. * fix(bin): name the batched friction list so an empty one is not misread With only a guard signature above threshold the list read "surfaced: none" while the counts line said surfaced=1, because guards render in their own section. Call the batched list what it is. * no-mistakes(review): harden friction guard carve-out, cancel precondition, and record reads * no-mistakes(review): bound friction records, stream reads, gate settled redraft * no-mistakes(review): spread friction eviction, harden record shape, bound status scan * no-mistakes(review): stream draft update, tail-first status scan, drop redundant ingest * no-mistakes(review): exempt guards from the record cap, drop quadratic scans * no-mistakes(review): unify friction signature grammar, gate settled dismiss * no-mistakes(review): exempt unclassified aggregate from cap, flag unreadable store * no-mistakes(review): anchor friction signature regex absolutely in jq * no-mistakes(review): retain friction observations by recency, report missing flag values * no-mistakes(document): document friction records in snapshot schema, watcher, and config * fix(bin): refuse dismiss as a draftable friction outcome `.outcomes` served double duty as what the captain may decide and what `draft` may compose. The two differ by exactly `dismiss`, which is its own command and files nothing, so validating `--outcome` against `.outcomes` let `draft <sig> --outcome dismiss` through on an ordinary signature. `compose_draft` then fell into its remaining branch - the guard-narrowing wording - and titled a non-guard draft "narrow <sig> to cut false positives" with a body reading "This guard stays ... must never remove or disable it". `approve` mapped it through its own catch-all to `state=cleared` with a filed issue URL. Derive `draftable` from `.outcomes` so the two sets cannot drift, validate `draft` against it, and name every branch in `compose_draft` and `approve` so an unrecognised outcome fails instead of borrowing another one's wording. This restores the contract the usage line and the friction-triage skill already stated. * fix(bin): pin the unattributable friction row inside the bearings bound The aggregate is never `surfaced`, so it ranks last and is the first row a prefix slice would cut. It survives today only because cap_model rebuilds the list as $exempt + $ordinary[:$cap] and the bearings surface happens to slice without re-sorting - the correct behaviour depends on a concatenation order established in a different file, with nothing at the surface doing the cutting to declare it. Guards carry their carve-out at both surfaces; the aggregate carried one at only the first. Re-derive the position where the cut happens. No row is added or removed: the aggregate is pinned to the front of the ranked list and still counts against FM_BEARINGS_FRICTION, so the rendered row count is still exactly the bound and every rendering is byte-identical. The regression pins both halves, because survival alone would also pass for an exemption that renders bound+1 rows. * no-mistakes(document): document friction store placement and bearings section boundary
) * fix(bin): route fleet-snapshot projections through stdin, not argv bin/fm-fleet-snapshot.sh handed each projected model to jq as a single --argjson string. Linux caps one argument at MAX_ARG_STRLEN (128 KiB) independently of ARG_MAX, so a backlog projection past that cap made execve fail with E2BIG and took the whole snapshot down, and with it bin/fm-bearings-snapshot.sh and every other consumer. The friction model already avoided this by arriving on stdin. Extend the same route to every projection that grows with the data: the main inventory summary, the secondmate home summary, the secondmate registry union, the per-home record accumulator and its assembly, the landed projection, and the final assembly. printf is a shell builtin, so the data never crosses an execve boundary at all. The jq programs bind these with `input as $x` above any def that uses them, since that binding is lexical where --argjson was global. Output is unchanged: both output modes stay byte-identical on a fixture the previous code could already handle. The colocated regression asserts the fixture's projections exceed the cap before asserting the snapshot succeeds, so a later shrink of either projection cannot leave the case quietly vacuous. * no-mistakes(review): route secondmate summary, decisions, reconciliation off argv
* fix(test-run): pin comm to LC_ALL=C in the coverage guard run_coverage_guard sorted its file lists with LC_ALL=C but invoked comm without it. A dictionary-collation locale orders punctuation differently from byte order, so comm rejected those C-sorted lists with "input is not in sorted order" and the guard exited 1 for a purely environmental reason. `LC_ALL=C bin/fm-test-run.sh --check-coverage` passed while the bare invocation under en_US.UTF-8 did not. Prefix every comm in the guard with LC_ALL=C so comparison uses the same collation as the sorts feeding it, and apply the same fix to the three comm calls in the colocated test that compare C-sorted lists. Add a regression test that probes for a locale whose collation actually diverges from C, asserts that divergence so the case cannot go vacuous, and then requires --check-coverage to exit 0 with the success marker and an empty stderr. C.UTF-8 collates by codepoint and does not reproduce the bug, so selecting a UTF-8 locale by name would have tested nothing. * test(test-run): keep the locale regression honest on non-glibc collation Collation is a platform property, not only a locale-name property. glibc ignores punctuation at the primary level, so en_US.UTF-8 reorders the C-sorted lists and reproduces the defect; stock BSD/macOS sort does not, so no listed locale may diverge there at all. Requiring a diverging locale unconditionally would turn the test red on macOS for an environmental reason - the exact failure mode this change exists to remove. Require instead that the guard hold under whatever UTF-8 locale is available, prefer a diverging one when it exists, and keep asserting the divergence itself in that case so the check cannot go vacuous. The pass message states which of the two was exercised. * no-mistakes(review): skip locale regression when no diverging collation exists
…orded (#3) * docs: require answered decision surfaces to drop their options An answered captain decision left beside a live options table reads as a fresh question and invites a contradictory second answer. State the rule once in the decision-hold-lifecycle policy owner, with a one-line reinforcement in AGENTS.md section 9 where captain-facing presentation is governed and no skill is loaded. * no-mistakes(review): scope answered-decision rationale to the recording surface
…iefs (#8) * feat: route findings that contradict a project's committed material back to the repo A scout report lives in the home's gitignored data/, so it is unreachable from any clone of the project it is about. When an investigation establishes something the project's own committed documents contradict, the repo keeps asserting the opposite and every later repo-only check re-derives the wrong answer from sound reasoning. Three surfaces, one predicate: - AGENTS.md section 6 gains one routing line: such findings belong in the project through its selected delivery path, carrying the findings themselves rather than a pointer to the private report. - fm-brief.sh's scout contract requires a named "Repo contradictions" section inventorying each affected committed file with file:line, written even when empty, since an absent section cannot be told apart from an unperformed inventory. The ship and secondmate scaffolds are unchanged. - decision-hold-lifecycle takes a second inventory class into its existing completion gate rather than adding a parallel one; a second gate would be one more thing to remember at exactly the moment things get forgotten. The class files follow-up ship tasks, never holds, which stay reserved for captain decisions. The inventory is agent-attested: no script observes it, unlike the hold inventory that fm-decision-hold.sh complete enforces. That limitation is recorded in both the skill and the mechanism doc so it cannot be mistaken for an enforced gate. * no-mistakes(review): widen charter contradiction inventory; anchor brief test assertions * no-mistakes(review): document contradiction inventory for both investigation scaffolds * no-mistakes(document): record repo-contradiction inventory regression evidence
* feat(bin): read the PR's checks before merging instead of assuming them "Never merge a red PR" had no mechanical backing. bin/fm-pr-merge.sh called gh-axi pr merge with no preflight of any kind, and neither fm-pr-check.sh nor fm-pr-lib.sh referenced check conclusions, status rollups, or check suites anywhere. The rule rested entirely on firstmate having looked at the PR by eye plus whatever branch protection the forge happened to enforce - and forge-side enforcement is not a fallback worth trusting, since this fleet's own fork has never had Actions enabled. fm-pr-merge.sh now reads statusCheckRollup from the forge and distinguishes the states that must not be collapsed: failing and unfinished both refuse and name the concrete check, an unreadable answer refuses rather than merging blind, and a repository with no checks configured merges normally because an empty rollup is not red. The filter always emits a state= line, so empty output can only mean failure and a broken gh cannot silently turn the guard into a no-op. Nothing classified why a check failed, so a billing block, an unavailable runner, a token-permission fault, and a genuine regression all arrived looking identical. --checks-override is the one way past a non-clean state and carries that classification as CHANGE, FLAKE, or INFRASTRUCTURE, recorded as checks_override= in the task's meta. It is a flag with no environment or configuration equivalent, so no standing posture such as yolo can reach it, and it is refused outright when the check state is already clean. The three classifications and the flag syntax live only in the script's header, per the knowledge-placement tree's tier for exact flags and commands. AGENTS.md section 7 gains a one-line cross-reference and does not restate them, and bin/fm-brief.sh's generated "checks green" wording is deliberately unchanged - it reports the pipeline's own reading of CI, which the merge guard now verifies independently rather than contradicts. Tests cover failing, blocked or never-started, pending, no-checks-configured, passing, unreadable, the override and its recording, override replacement, override refusal on a clean state and on an unknown or missing classification, and that neither a yolo posture nor an environment variable reaches the override. They drive the real filter through jq, standing in for the one gh runs, and skip rather than pass vacuously where jq is absent. * fix(bin): clear a superseded check override and keep the states to one owner Two follow-ups on the merge preflight. The strip-and-append only ran when an override was passed, so a classification could outlive the merge it described: override FLAKE on a failing PR, let the merge itself fail, fix CI, then merge again with no flag, and meta still claimed an authorization that second merge never used. The rewrite now also runs when a checks_override= line is already present, so a merge that needed no override clears it. The common clean merge still touches nothing. AGENTS.md restated the same four check states as the script header and docs/architecture.md. Section 7 keeps only what firstmate needs at merge time - that the merge can be refused on check state, that the concrete check is named, and that the script owns both the refusing states and the classification its override requires - and the mechanism detail stays with its owners. * no-mistakes(review): keep the override meta parseable and reject empty classifications * no-mistakes(review): read gh's empty check fields, bind the meta swap * no-mistakes(document): correct the unreadable check-state refusal claim
* feat(skills): own the Lavish board workflow in a loadable skill The board procedure lived in startup prose that counted against the startup memory budget and was read hours before the moment of use. Firstmate broke the captain's boards twice within one hour on 2026-08-14 while holding a written rule that would have prevented both. A skill costs nothing at startup and fires at the moment of use, so the six-step procedure and its supporting evidence move to .agents/skills/lavish-boards/, with a single trigger line in AGENTS.md section 13 and the matching agent-runtime classification entry. Card format stays owned by the private local data/captain.md, which the skill references rather than restates, and the collapse-on-answer rule stays owned by decision-hold-lifecycle. * no-mistakes(review): correct arm/poll mechanism, collapse rule, and diagnostic invocation * no-mistakes(review): qualify terminal retirement, fix arming check and diagnostic output * no-mistakes(review): point step 3 at process-event-sources, drop mechanism restatement * no-mistakes(review): bind pending counts to their commands, repoint owner * no-mistakes(review): restore the arming-check purpose in step 3 * no-mistakes(review): qualify the pending_prompts zero on an armed board * no-mistakes(test): delete the copied option-presentation definition in step 5
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.