Skip to content

sync fork - #2432

Closed
Patrik-karlsson wants to merge 8 commits into
kunchenguid:mainfrom
Patrik-karlsson:main
Closed

sync fork #2432
Patrik-karlsson wants to merge 8 commits into
kunchenguid:mainfrom
Patrik-karlsson:main

Conversation

@Patrik-karlsson

Copy link
Copy Markdown

No description provided.

* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant