Skip to content

feat(bin): split task identity into role/deliverable/stage axes and rename promote to reflag - #65

Merged
sbracewell64 merged 15 commits into
mainfrom
fm/cfvc-16-landing
Aug 9, 2026
Merged

sbracewell64 merged 15 commits into
mainfrom
fm/cfvc-16-landing

Conversation

@sbracewell64

Copy link
Copy Markdown
Owner

What this is

CFVC-16 fleet-side implementation, executing the captain's 2026-08-07 naming ruling. The captain framed it as cohesion work, not cosmetic terminology work.

  • D18 - open docs/vocabulary-collisions.md, the fleet's first vocabulary-collision registry, seeded with every ruled disposition (AXI not renamed, the five skill senses, the qualified watcher / Watch Register forms, promotion's senses, execution NO-CONTACT, the qualified lifecycle forms) and with each obsolete name's retirement condition recorded.
  • D10 - rename the scout-to-ship operation bin/fm-promote.sh -> bin/fm-reflag.sh. The old name remains only as a bounded shim that forwards, warns, and records each use to state/.reflag-shim-used, so its retirement is settled by evidence rather than memory.
  • D16 - split task metadata kind= into three orthogonal fields: role=crew|secondmate, deliverable=scout|ship, stage=commissioned|reflagged|delivered, owned by the new bin/fm-task-axis-lib.sh. Migration follows the ruled sequence: dual-write, deterministic idempotent backfill (a locked startup sweep), then readers migrated one axis at a time. kind= stays dual-written exactly as the ruling requires; no reader consults it any more.
  • D17 - the role axis becomes the home for role-typed facts, so a future requested_agent_role lands on it instead of adding a fourth conflated dimension.

Deliberate non-goals

  • Platform-side work (D3/D13/D19 and the platform registers) belongs to a separate task and is untouched here.
  • stage=delivered is part of the ruled value set but is declared and not yet written by any path. Stamping it belongs after a confirmed landing, inside bin/fm-pr-merge.sh's private metadata rewrite, whose ordering, device, and single-link invariants that path owns. Both the library header and the registry say so explicitly, so its absence is not misread as "this task did not land".
  • kind= is not removed. The ruling permits retiring it only after a full proven cycle; the registry records the exact retirement condition.

Safety property worth review attention

A task's metadata doubles as PR identity, and its parser refuses any unrecognized key appearing after the pr= line - the rule that stops a tampered record from smuggling a second identity past an armed merge poll. An early version of the backfill appended, which would have invalidated exactly the records carrying a live merge watch, silently stopping the watcher from honoring those polls. Axis writes now land before pr= through one shared helper used by both the backfill and the reflag, and the regression pins the position, not just the parse.

Relatedly, a record whose deprecated kind= contradicts its explicit axes is refused rather than silently resolved, in the library, in reflagging, and in teardown - because either side could be the stale one, and letting teardown choose between a protected ship worktree and a scratch scout worktree by luck is how unlanded work gets discarded.

Verification

  • bin/fm-lint.sh clean (ShellCheck 0.11.0, the pinned version).
  • bin/fm-doc-audience-check.sh ok - 74 surfaces, 235 local links.
  • bin/fm-test-run.sh --check-coverage ok.
  • Targeted behavioral suites on this exact base: 309 passing assertions, zero failures across fm-task-axis, fm-task-delivery, fm-teardown, fm-gotmp, fm-backend, fm-brief, fm-admission, fm-crew-state, fm-bootstrap, fm-fleet-snapshot-view, fm-bearings-snapshot, fm-wake-ledger.
  • Earlier full changed-file sweep on the original base: 74 suites, 1563 passing assertions, with nine failures each verified pre-existing by running the same suite against a pristine copy of the base commit (Pi/node ESM .ts loader, tmux unavailability, a session-start MISSING diagnostic, a tmux agent-liveness classification, a launch-template assertion, a watcher-lock restart attach, and a suite that emits the worktree-tangle banner whenever run from any task branch).
  • New tests/fm-task-axis.test.sh covers dual-write, backfill determinism and idempotence, PR-identity preservation, each reader migration, and the alias refusing stale writers. Each guard was witnessed failing with its behavior removed before being trusted green.

Check state - please read before merging

This PR has no CI checks configured, so nothing here has been verified green by CI. The validation above is local evidence only. The fleet's own merge guard refuses merges without verified green checks, and that refusal is correct for this PR.

Provenance of this branch

This branch was cut fresh from the current fork trunk and carries only this change: 15 commits, 39 files, zero replayed fork-landing commits, with the fork trunk as a strict ancestor (clean fast-forward, nothing behind).

It exists because a validation pipeline run rebased the original branch onto the upstream trunk while resolving conflicts, which replayed the fork's landing queue under new SHAs and left that head 107 commits / 272 files divergent from the fork trunk. That head and its upstream PR are deliberately untouched. Three of these commits (no-mistakes(review) and no-mistakes(document)) are fixes that pipeline applied and are carried across intact.

One concrete casualty of that rebase is fixed here: it silently dropped the change adding fm-task-axis-lib.sh to the old-bin conformance fixture's sibling list, resolving the conflict by taking the base side. This branch keeps both the base's addition and this change's.

@sbracewell64

Copy link
Copy Markdown
Owner Author

Check state after the first CI pass: 12 passed, 2 failed

Both failures are accounted for, and neither is a defect in this change.

1. PR must be raised via no-mistakes — fails by construction.
This PR was opened by hand rather than by the validation pipeline, so the attestation check cannot pass. That is the established shape for fork landing PRs here: the same check is currently failing on #63 and #64 for the same reason.

The change itself was validated by the pipeline, on the upstream contribution PR: intent, review, test, document, lint, push, and pr all completed with zero remaining findings. Three of the commits on this branch (no-mistakes(review), no-mistakes(document) x2) are fixes that run applied, carried across intact.

2. Behavior portable serial 1 — pre-existing on the fork trunk.
The failing assertion is not ok - first inheritance transaction never reached its blocked write in tests/fm-remote-secondmate-lifecycle-e2e.test.sh.

Verified not to be caused by this change: extracting the fork trunk (e3432d3e) to a clean directory with this change absent and running that same suite reproduces the identical failure. It also reproduces locally on this branch, so it is deterministic rather than a CI-only flake.

Everything else is green, including all four other behavior shards, Herdr behavior tests, lint, the coverage guard, repo invariants, the macOS stock-Bash snapshot, and the Windows launcher bridge.

What this change was verified against directly: 309 passing assertions with zero failures across fm-task-axis, fm-task-delivery, fm-teardown, fm-gotmp, fm-backend, fm-brief, fm-admission, fm-crew-state, fm-bootstrap, fm-fleet-snapshot-view, fm-bearings-snapshot, and fm-wake-ledger, run on this exact base.

The fleet resolved name collisions wherever they surfaced, so a ruling was
only ever findable by whoever remembered making it, and the platform's own
Register 3 had no counterpart on this side.

Open docs/vocabulary-collisions.md as the single owner of every word carrying
more than one meaning across the fleet, the platform, and the vendor tools both
depend on. It ships seeded with the ruled dispositions rather than empty: axi
and execution keep their names with the evidence recorded, skill and watch and
lifecycle take mandated qualified forms, promotion splits with the fleet verb
becoming reflag, and kind splits into three axes.

A rename or a split row also states the obsolete name's retirement condition,
so no superseded path is left with an open-ended life. This lands before any
rename it governs: the map exists first, then the moves it records.
One kind= field carried three independent facts at once: who the worker is,
what the task produces, and where the task stands in its life. Every consumer
reconstructed the axis it cared about from a value that also encoded the two it
did not, and the scout-to-ship operation expressed a lifecycle transition by
rewriting a deliverable type - which is why a reflagged ship and a commissioned
one were indistinguishable, and why a requested agent role had nowhere to land
that would not have made a fourth conflated dimension.

bin/fm-task-axis-lib.sh becomes the single owner of role=, deliverable=, and
stage=, of their values, and of the total derivation from the retired field, so
no consumer spells that mapping itself. Migration follows the ordered contract:

- Dual-write first. Every writer emits the axes beside kind=, which keeps
  reading unchanged while records converge.
- Backfill by derivation, forward-only and idempotent, in a startup sweep that
  runs only under the fleet lock and leaves a converged home byte-identical.
  Stage is deliberately NOT derived: the old field could not distinguish a
  reflagged ship from a commissioned one, so backfill records the
  lower-information value rather than inventing a fact.
- Migrate readers one axis at a time - role across the secondmate-membership
  consumers, then deliverable across the pipeline and teardown-protection
  consumers - splitting the conditions that had mixed both.

kind= stays dual-written for now; the registry owns its retirement condition.
While it stays, a record whose alias contradicts its axes is REFUSED rather
than resolved, because either side could be the stale one and teardown choosing
between a protected ship worktree and a scratch scout worktree by luck is how
unlanded work gets discarded.

The scout-to-ship operation becomes bin/fm-reflag.sh in the same change, since
it is what makes the stage axis true. The old name stays only as a bounded shim
that forwards, warns, and records each use, so its own retirement is settled by
evidence rather than by memory.

Coverage is red-capable: each guard was witnessed failing with its behavior
removed before being trusted green - the derivation table, dual-write,
backfill idempotence, the shim's evidence, and the refusal in the library, in
reflagging, and in teardown.
The rename and the axis split are only real once the instructions that drive
them say so, so this carries the fleet's own vocabulary across: the scout
outcome section reflags rather than promotes, the metadata field list names the
three axes and the deprecated field they replace, and the captain-facing
do-not-expose list drops a word the fleet no longer uses internally.

Knowledge routing gains one line: a word with a second live meaning goes to the
collision registry, never settled locally in whichever file it surfaced in.
That is the rule that keeps the registry from going stale the first time
someone is in a hurry.

Also moves teardown's admission release reminder into the admission library,
which already owns that policy. Its test previously reconstructed the function
by parsing teardown's source and eval-ing the fragment, so it broke the moment
the function read a variable defined outside it - and a test that reads
implementation source is exactly what CONTRIBUTING forbids. The reminder now
takes its inputs as arguments and the test calls it directly.
Two consumers still read the deprecated field: the watcher classified every
supervised window by it, and teardown's nested-home check read it on a parent
record. Both only ever ask whether they are looking at a persistent direct
report, so both are the role axis and neither needed what the work produces.

The watcher's helper is renamed to say what it answers, which is what made the
one remaining stale reference visible - a variable read with no assignment left,
caught at runtime by the triage suite rather than by lint.
The old-versus-new teardown conformance case builds a mixed tree: the entry
points come from the baseline commit while their siblings are copied from the
working tree. Several of those copied siblings now source the axis library, so
the fixture needs it present or the baseline entry point dies looking for it.
The recovery and secondmate procedures still told an agent to look for the
retired single field, so the instructions that decide which playbook applies
would have gone on naming a field the fleet is removing. Each of these asks
only who the worker is, so each now says role.
The ruled value set includes delivered, and the spawn and the reflag write the
other two. Nothing writes delivered: the honest place to stamp it is after a
confirmed landing, inside the merge path's own private metadata rewrite, whose
ordering, device, and single-link invariants that path owns - so the writer is
a deliberate follow-up rather than something bolted on beside this split.

Say so in both the library and the registry, because a declared value that
never appears is otherwise read as evidence that the task did not land.
Reads in the order it acts: the refusal and why it exists, then the axes it
protects. Also drops a local the remote path no longer reads and closes the gap
the moved admission reminder left.
Every teardown branch reads an axis now, and the ledger record carries role and
deliverable, so nothing consulted the alias teardown still parsed on the way in.

With that gone no consumer anywhere branches on the deprecated field, which
changes what its retirement is waiting for: not a reader migration, but a full
task cycle on the axes across every home. The registry says so precisely,
because a retirement condition nobody can evaluate is how an alias becomes
permanent.
The view's type column was the last thing reading the deprecated field, and it
could only ever show one of the three facts a row carries. It now renders the
role and deliverable together and appends the stage when it is not the spawn
default, so a reflagged ship is visible as one at a glance instead of being
indistinguishable from a commissioned one - which is the whole reason the
lifecycle axis exists.
Both fake roots symlink the real teardown and each sibling it sources, so the
new axis library has to be linked in beside them or teardown dies looking for it
before it reaches the behavior these cases assert.
A task's metadata doubles as PR identity, and its parser refuses any
unrecognized key that appears after the pr= line - the rule that stops a
tampered record from smuggling a second identity past an armed merge poll.
Backfill appended, so the first startup sweep over a home with a live merge
watch invalidated exactly the records that had one, and the watcher stopped
honoring those polls. Nothing reported it, because a poll that is refused looks
the same as a poll with nothing to say.

Axis writes now land before the pr= line, through one helper both the backfill
and the reflag use, and the regression pins the position rather than only the
parse so a future writer cannot reintroduce it by appending again.

Found by the PR-check security suite, which was green on the base and red here.
@sbracewell64

Copy link
Copy Markdown
Owner Author

Rebased onto the current fork trunk

Rebased onto 0204baa (fork main, after #60, #62 and #63 merged). Pre-rewrite head db80f18 is preserved at archive/prerebase-2026-08-09/cfvc-16-landing; the head branch is fork-only and is not shared with any open upstream PR - the upstream contribution rides fm/cfvc-16-fleet-implementation, a different branch, and was not touched. All fifteen commits survived, in order, including every axis-migration commit and all four pipeline-fix commits.

Conflicts and how they were resolved

Eleven conflicts across three of the fifteen commits, in bin/fm-promote.sh, bin/fm-reflag.sh (twice), bin/fm-bootstrap.sh (twice), bin/fm-brief.sh, bin/fm-session-start.sh, bin/fm-teardown.sh, bin/fm-wake-ledger.sh (twice), AGENTS.md (three), docs/scripts.md, and the bootstrap-diagnostics skill (two). Most were unions of independent additions. Three needed real reconciliation:

  • bin/fm-promote.sh becoming a shim. The trunk had grown new behavior in this file - the derived escalation_policy recompute from the feat(bin): record why every agent dispatch was necessary at the spawn chokepoint kunchenguid/firstmate#1830 landing - while this branch replaced the whole file with a forward to bin/fm-reflag.sh. Taking either side alone loses something: the branch's rename, or the trunk's behavior. Took the branch's shim, and ported the escalation recompute into bin/fm-reflag.sh, which is where that behavior now lives. See the verification note below - this is the resolution most worth checking.
  • The ledger's terminal record. The trunk added outcome_source and a recording sweep; this branch replaced the record's kind field with the role/deliverable axes. Both are kept, and the sweep, which the trunk added and which passed --kind, was moved onto the axes through fm_task_role / fm_task_deliverable so a pre-split record still resolves correctly through the same owner every other reader uses.
  • The mutating-sweep enumeration. This branch re-counted the list as "the seven MUTATING sweeps" in bin/fm-session-start.sh and AGENTS.md, while feat(bin): let the terminal record say a task failed (land of upstream #1978) #62 had just de-counted both and pointed them at bin/fm-bootstrap.sh's header as the single owner. Kept the de-counted pointer form and registered the new task_axis_backfill_sweep in the owner's own list instead, so the one-owner rule feat(bin): let the terminal record say a task failed (land of upstream #1978) #62 established is not undone.

The AGENTS.md metadata inventory line took all three: this branch's identity axes, #63's attempt fields, and the trunk's agent-justification fields.

Verifying the ported behavior, not assuming it

tests/fm-reasoning-required.test.sh drives bin/fm-promote.sh directly, which now execs the shim into fm-reflag.sh, so its test_promotion_recomputes_the_derived_escalation_policy case exercises the port end to end. It passes.

That was not trusted on a pass alone. Removing the ported lines and re-running produced exactly the expected failure - not ok - promotion left the scout's report-only posture on a ship task - and restoring them turned it green again, so the case genuinely binds the behavior rather than passing vacuously.

Test results, disclosed in full

bin/fm-test-run.sh --changed --base fork/main selected 92 suites; 12 failed. tests/fm-attempt.test.sh and tests/fm-reasoning-required.test.sh were run separately and pass.

All twelve failures are pre-existing and unrelated, reproduced on unmodified fork/main in a separate clean checkout:

  • Local Node ESM loader: fm-calm-pi-extension.
  • Local tmux harness: fm-backend-tmux-smoke, fm-launch, fm-tmux-agent-liveness, fm-pi-watch-extension, fm-turnend-guard, fm-watcher-lock.
  • Local VALIDATION_DAEMON: down noise from an absent no-mistakes daemon root: fm-bootstrap, fm-session-start, fm-secondmate-sync, fm-startup-memory-budget.
  • fm-secondmate-harness reads the real repository state and reports the worktree-tangle warning because the rebase worktree sits on a task branch rather than the default branch. The control for this one was run on a deliberately non-default branch at bare fork/main, where it fails identically, so the cause is the branch name and not this change.

Every suite this branch actually owns is green: fm-task-axis, fm-teardown, fm-teardown-endpoint-safety, fm-wake-ledger, fm-brief, fm-fleet-snapshot-view, fm-task-delivery.

bin/fm-lint.sh is clean.

@sbracewell64
sbracewell64 merged commit 825e965 into main Aug 9, 2026
13 of 14 checks passed
sbracewell64 added a commit that referenced this pull request Aug 9, 2026
…ename promote to reflag (#65)

* docs: open the fleet's vocabulary-collision registry

The fleet resolved name collisions wherever they surfaced, so a ruling was
only ever findable by whoever remembered making it, and the platform's own
Register 3 had no counterpart on this side.

Open docs/vocabulary-collisions.md as the single owner of every word carrying
more than one meaning across the fleet, the platform, and the vendor tools both
depend on. It ships seeded with the ruled dispositions rather than empty: axi
and execution keep their names with the evidence recorded, skill and watch and
lifecycle take mandated qualified forms, promotion splits with the fleet verb
becoming reflag, and kind splits into three axes.

A rename or a split row also states the obsolete name's retirement condition,
so no superseded path is left with an open-ended life. This lands before any
rename it governs: the map exists first, then the moves it records.

* feat(bin): split task identity into role, deliverable, and stage axes

One kind= field carried three independent facts at once: who the worker is,
what the task produces, and where the task stands in its life. Every consumer
reconstructed the axis it cared about from a value that also encoded the two it
did not, and the scout-to-ship operation expressed a lifecycle transition by
rewriting a deliverable type - which is why a reflagged ship and a commissioned
one were indistinguishable, and why a requested agent role had nowhere to land
that would not have made a fourth conflated dimension.

bin/fm-task-axis-lib.sh becomes the single owner of role=, deliverable=, and
stage=, of their values, and of the total derivation from the retired field, so
no consumer spells that mapping itself. Migration follows the ordered contract:

- Dual-write first. Every writer emits the axes beside kind=, which keeps
  reading unchanged while records converge.
- Backfill by derivation, forward-only and idempotent, in a startup sweep that
  runs only under the fleet lock and leaves a converged home byte-identical.
  Stage is deliberately NOT derived: the old field could not distinguish a
  reflagged ship from a commissioned one, so backfill records the
  lower-information value rather than inventing a fact.
- Migrate readers one axis at a time - role across the secondmate-membership
  consumers, then deliverable across the pipeline and teardown-protection
  consumers - splitting the conditions that had mixed both.

kind= stays dual-written for now; the registry owns its retirement condition.
While it stays, a record whose alias contradicts its axes is REFUSED rather
than resolved, because either side could be the stale one and teardown choosing
between a protected ship worktree and a scratch scout worktree by luck is how
unlanded work gets discarded.

The scout-to-ship operation becomes bin/fm-reflag.sh in the same change, since
it is what makes the stage axis true. The old name stays only as a bounded shim
that forwards, warns, and records each use, so its own retirement is settled by
evidence rather than by memory.

Coverage is red-capable: each guard was witnessed failing with its behavior
removed before being trusted green - the derivation table, dual-write,
backfill idempotence, the shim's evidence, and the refusal in the library, in
reflagging, and in teardown.

* docs: move the fleet's instructions onto the new names and axes

The rename and the axis split are only real once the instructions that drive
them say so, so this carries the fleet's own vocabulary across: the scout
outcome section reflags rather than promotes, the metadata field list names the
three axes and the deprecated field they replace, and the captain-facing
do-not-expose list drops a word the fleet no longer uses internally.

Knowledge routing gains one line: a word with a second live meaning goes to the
collision registry, never settled locally in whichever file it surfaced in.
That is the rule that keeps the registry from going stale the first time
someone is in a hurry.

Also moves teardown's admission release reminder into the admission library,
which already owns that policy. Its test previously reconstructed the function
by parsing teardown's source and eval-ing the fragment, so it broke the moment
the function read a variable defined outside it - and a test that reads
implementation source is exactly what CONTRIBUTING forbids. The reminder now
takes its inputs as arguments and the test calls it directly.

* fix(bin): move the watcher and nested-home checks onto the role axis

Two consumers still read the deprecated field: the watcher classified every
supervised window by it, and teardown's nested-home check read it on a parent
record. Both only ever ask whether they are looking at a persistent direct
report, so both are the role axis and neither needed what the work produces.

The watcher's helper is renamed to say what it answers, which is what made the
one remaining stale reference visible - a variable read with no assignment left,
caught at runtime by the triage suite rather than by lint.

* test: carry the axis library into the old-bin conformance fixture

The old-versus-new teardown conformance case builds a mixed tree: the entry
points come from the baseline commit while their siblings are copied from the
working tree. Several of those copied siblings now source the axis library, so
the fixture needs it present or the baseline entry point dies looking for it.

* docs: teach recovery and provisioning the identity axes

The recovery and secondmate procedures still told an agent to look for the
retired single field, so the instructions that decide which playbook applies
would have gone on naming a field the fleet is removing. Each of these asks
only who the worker is, so each now says role.

* docs: state plainly that the stage axis has one value nothing writes yet

The ruled value set includes delivered, and the spawn and the reflag write the
other two. Nothing writes delivered: the honest place to stamp it is after a
confirmed landing, inside the merge path's own private metadata rewrite, whose
ordering, device, and single-link invariants that path owns - so the writer is
a deliberate follow-up rather than something bolted on beside this split.

Say so in both the library and the registry, because a declared value that
never appears is otherwise read as evidence that the task did not land.

* refactor(bin): tidy the teardown identity block

Reads in the order it acts: the refusal and why it exists, then the axes it
protects. Also drops a local the remote path no longer reads and closes the gap
the moved admission reminder left.

* refactor(bin): drop teardown's now-unread copy of the deprecated field

Every teardown branch reads an axis now, and the ledger record carries role and
deliverable, so nothing consulted the alias teardown still parsed on the way in.

With that gone no consumer anywhere branches on the deprecated field, which
changes what its retirement is waiting for: not a reader migration, but a full
task cycle on the axes across every home. The registry says so precisely,
because a retirement condition nobody can evaluate is how an alias becomes
permanent.

* feat(bin): render the identity axes in the fleet view

The view's type column was the last thing reading the deprecated field, and it
could only ever show one of the three facts a row carries. It now renders the
role and deliverable together and appends the stage when it is not the spawn
default, so a reflagged ship is visible as one at a glance instead of being
indistinguishable from a commissioned one - which is the whole reason the
lifecycle axis exists.

* test: carry the axis library into the gotmp fake roots

Both fake roots symlink the real teardown and each sibling it sources, so the
new axis library has to be linked in beside them or teardown dies looking for it
before it reaches the behavior these cases assert.

* fix(bin): keep a polled record's PR identity intact when writing an axis

A task's metadata doubles as PR identity, and its parser refuses any
unrecognized key that appears after the pr= line - the rule that stops a
tampered record from smuggling a second identity past an armed merge poll.
Backfill appended, so the first startup sweep over a home with a live merge
watch invalidated exactly the records that had one, and the watcher stopped
honoring those polls. Nothing reported it, because a poll that is refused looks
the same as a poll with nothing to say.

Axis writes now land before the pr= line, through one helper both the backfill
and the reflag use, and the regression pins the position rather than only the
parse so a future writer cannot reintroduce it by appending again.

Found by the PR-check security suite, which was green on the base and red here.

* no-mistakes(review): make reflag atomic, dual-carry kind on v1 wire surfaces

* no-mistakes(document): migrate leftover scout-promotion wording to reflag

* no-mistakes(document): reflag retired promote verb in scout brief output
sbracewell64 added a commit that referenced this pull request Aug 10, 2026
…ename promote to reflag (#65)

* docs: open the fleet's vocabulary-collision registry

The fleet resolved name collisions wherever they surfaced, so a ruling was
only ever findable by whoever remembered making it, and the platform's own
Register 3 had no counterpart on this side.

Open docs/vocabulary-collisions.md as the single owner of every word carrying
more than one meaning across the fleet, the platform, and the vendor tools both
depend on. It ships seeded with the ruled dispositions rather than empty: axi
and execution keep their names with the evidence recorded, skill and watch and
lifecycle take mandated qualified forms, promotion splits with the fleet verb
becoming reflag, and kind splits into three axes.

A rename or a split row also states the obsolete name's retirement condition,
so no superseded path is left with an open-ended life. This lands before any
rename it governs: the map exists first, then the moves it records.

* feat(bin): split task identity into role, deliverable, and stage axes

One kind= field carried three independent facts at once: who the worker is,
what the task produces, and where the task stands in its life. Every consumer
reconstructed the axis it cared about from a value that also encoded the two it
did not, and the scout-to-ship operation expressed a lifecycle transition by
rewriting a deliverable type - which is why a reflagged ship and a commissioned
one were indistinguishable, and why a requested agent role had nowhere to land
that would not have made a fourth conflated dimension.

bin/fm-task-axis-lib.sh becomes the single owner of role=, deliverable=, and
stage=, of their values, and of the total derivation from the retired field, so
no consumer spells that mapping itself. Migration follows the ordered contract:

- Dual-write first. Every writer emits the axes beside kind=, which keeps
  reading unchanged while records converge.
- Backfill by derivation, forward-only and idempotent, in a startup sweep that
  runs only under the fleet lock and leaves a converged home byte-identical.
  Stage is deliberately NOT derived: the old field could not distinguish a
  reflagged ship from a commissioned one, so backfill records the
  lower-information value rather than inventing a fact.
- Migrate readers one axis at a time - role across the secondmate-membership
  consumers, then deliverable across the pipeline and teardown-protection
  consumers - splitting the conditions that had mixed both.

kind= stays dual-written for now; the registry owns its retirement condition.
While it stays, a record whose alias contradicts its axes is REFUSED rather
than resolved, because either side could be the stale one and teardown choosing
between a protected ship worktree and a scratch scout worktree by luck is how
unlanded work gets discarded.

The scout-to-ship operation becomes bin/fm-reflag.sh in the same change, since
it is what makes the stage axis true. The old name stays only as a bounded shim
that forwards, warns, and records each use, so its own retirement is settled by
evidence rather than by memory.

Coverage is red-capable: each guard was witnessed failing with its behavior
removed before being trusted green - the derivation table, dual-write,
backfill idempotence, the shim's evidence, and the refusal in the library, in
reflagging, and in teardown.

* docs: move the fleet's instructions onto the new names and axes

The rename and the axis split are only real once the instructions that drive
them say so, so this carries the fleet's own vocabulary across: the scout
outcome section reflags rather than promotes, the metadata field list names the
three axes and the deprecated field they replace, and the captain-facing
do-not-expose list drops a word the fleet no longer uses internally.

Knowledge routing gains one line: a word with a second live meaning goes to the
collision registry, never settled locally in whichever file it surfaced in.
That is the rule that keeps the registry from going stale the first time
someone is in a hurry.

Also moves teardown's admission release reminder into the admission library,
which already owns that policy. Its test previously reconstructed the function
by parsing teardown's source and eval-ing the fragment, so it broke the moment
the function read a variable defined outside it - and a test that reads
implementation source is exactly what CONTRIBUTING forbids. The reminder now
takes its inputs as arguments and the test calls it directly.

* fix(bin): move the watcher and nested-home checks onto the role axis

Two consumers still read the deprecated field: the watcher classified every
supervised window by it, and teardown's nested-home check read it on a parent
record. Both only ever ask whether they are looking at a persistent direct
report, so both are the role axis and neither needed what the work produces.

The watcher's helper is renamed to say what it answers, which is what made the
one remaining stale reference visible - a variable read with no assignment left,
caught at runtime by the triage suite rather than by lint.

* test: carry the axis library into the old-bin conformance fixture

The old-versus-new teardown conformance case builds a mixed tree: the entry
points come from the baseline commit while their siblings are copied from the
working tree. Several of those copied siblings now source the axis library, so
the fixture needs it present or the baseline entry point dies looking for it.

* docs: teach recovery and provisioning the identity axes

The recovery and secondmate procedures still told an agent to look for the
retired single field, so the instructions that decide which playbook applies
would have gone on naming a field the fleet is removing. Each of these asks
only who the worker is, so each now says role.

* docs: state plainly that the stage axis has one value nothing writes yet

The ruled value set includes delivered, and the spawn and the reflag write the
other two. Nothing writes delivered: the honest place to stamp it is after a
confirmed landing, inside the merge path's own private metadata rewrite, whose
ordering, device, and single-link invariants that path owns - so the writer is
a deliberate follow-up rather than something bolted on beside this split.

Say so in both the library and the registry, because a declared value that
never appears is otherwise read as evidence that the task did not land.

* refactor(bin): tidy the teardown identity block

Reads in the order it acts: the refusal and why it exists, then the axes it
protects. Also drops a local the remote path no longer reads and closes the gap
the moved admission reminder left.

* refactor(bin): drop teardown's now-unread copy of the deprecated field

Every teardown branch reads an axis now, and the ledger record carries role and
deliverable, so nothing consulted the alias teardown still parsed on the way in.

With that gone no consumer anywhere branches on the deprecated field, which
changes what its retirement is waiting for: not a reader migration, but a full
task cycle on the axes across every home. The registry says so precisely,
because a retirement condition nobody can evaluate is how an alias becomes
permanent.

* feat(bin): render the identity axes in the fleet view

The view's type column was the last thing reading the deprecated field, and it
could only ever show one of the three facts a row carries. It now renders the
role and deliverable together and appends the stage when it is not the spawn
default, so a reflagged ship is visible as one at a glance instead of being
indistinguishable from a commissioned one - which is the whole reason the
lifecycle axis exists.

* test: carry the axis library into the gotmp fake roots

Both fake roots symlink the real teardown and each sibling it sources, so the
new axis library has to be linked in beside them or teardown dies looking for it
before it reaches the behavior these cases assert.

* fix(bin): keep a polled record's PR identity intact when writing an axis

A task's metadata doubles as PR identity, and its parser refuses any
unrecognized key that appears after the pr= line - the rule that stops a
tampered record from smuggling a second identity past an armed merge poll.
Backfill appended, so the first startup sweep over a home with a live merge
watch invalidated exactly the records that had one, and the watcher stopped
honoring those polls. Nothing reported it, because a poll that is refused looks
the same as a poll with nothing to say.

Axis writes now land before the pr= line, through one helper both the backfill
and the reflag use, and the regression pins the position rather than only the
parse so a future writer cannot reintroduce it by appending again.

Found by the PR-check security suite, which was green on the base and red here.

* no-mistakes(review): make reflag atomic, dual-carry kind on v1 wire surfaces

* no-mistakes(document): migrate leftover scout-promotion wording to reflag

* no-mistakes(document): reflag retired promote verb in scout brief output
sbracewell64 added a commit that referenced this pull request Aug 11, 2026
…ename promote to reflag (#65)

* docs: open the fleet's vocabulary-collision registry

The fleet resolved name collisions wherever they surfaced, so a ruling was
only ever findable by whoever remembered making it, and the platform's own
Register 3 had no counterpart on this side.

Open docs/vocabulary-collisions.md as the single owner of every word carrying
more than one meaning across the fleet, the platform, and the vendor tools both
depend on. It ships seeded with the ruled dispositions rather than empty: axi
and execution keep their names with the evidence recorded, skill and watch and
lifecycle take mandated qualified forms, promotion splits with the fleet verb
becoming reflag, and kind splits into three axes.

A rename or a split row also states the obsolete name's retirement condition,
so no superseded path is left with an open-ended life. This lands before any
rename it governs: the map exists first, then the moves it records.

* feat(bin): split task identity into role, deliverable, and stage axes

One kind= field carried three independent facts at once: who the worker is,
what the task produces, and where the task stands in its life. Every consumer
reconstructed the axis it cared about from a value that also encoded the two it
did not, and the scout-to-ship operation expressed a lifecycle transition by
rewriting a deliverable type - which is why a reflagged ship and a commissioned
one were indistinguishable, and why a requested agent role had nowhere to land
that would not have made a fourth conflated dimension.

bin/fm-task-axis-lib.sh becomes the single owner of role=, deliverable=, and
stage=, of their values, and of the total derivation from the retired field, so
no consumer spells that mapping itself. Migration follows the ordered contract:

- Dual-write first. Every writer emits the axes beside kind=, which keeps
  reading unchanged while records converge.
- Backfill by derivation, forward-only and idempotent, in a startup sweep that
  runs only under the fleet lock and leaves a converged home byte-identical.
  Stage is deliberately NOT derived: the old field could not distinguish a
  reflagged ship from a commissioned one, so backfill records the
  lower-information value rather than inventing a fact.
- Migrate readers one axis at a time - role across the secondmate-membership
  consumers, then deliverable across the pipeline and teardown-protection
  consumers - splitting the conditions that had mixed both.

kind= stays dual-written for now; the registry owns its retirement condition.
While it stays, a record whose alias contradicts its axes is REFUSED rather
than resolved, because either side could be the stale one and teardown choosing
between a protected ship worktree and a scratch scout worktree by luck is how
unlanded work gets discarded.

The scout-to-ship operation becomes bin/fm-reflag.sh in the same change, since
it is what makes the stage axis true. The old name stays only as a bounded shim
that forwards, warns, and records each use, so its own retirement is settled by
evidence rather than by memory.

Coverage is red-capable: each guard was witnessed failing with its behavior
removed before being trusted green - the derivation table, dual-write,
backfill idempotence, the shim's evidence, and the refusal in the library, in
reflagging, and in teardown.

* docs: move the fleet's instructions onto the new names and axes

The rename and the axis split are only real once the instructions that drive
them say so, so this carries the fleet's own vocabulary across: the scout
outcome section reflags rather than promotes, the metadata field list names the
three axes and the deprecated field they replace, and the captain-facing
do-not-expose list drops a word the fleet no longer uses internally.

Knowledge routing gains one line: a word with a second live meaning goes to the
collision registry, never settled locally in whichever file it surfaced in.
That is the rule that keeps the registry from going stale the first time
someone is in a hurry.

Also moves teardown's admission release reminder into the admission library,
which already owns that policy. Its test previously reconstructed the function
by parsing teardown's source and eval-ing the fragment, so it broke the moment
the function read a variable defined outside it - and a test that reads
implementation source is exactly what CONTRIBUTING forbids. The reminder now
takes its inputs as arguments and the test calls it directly.

* fix(bin): move the watcher and nested-home checks onto the role axis

Two consumers still read the deprecated field: the watcher classified every
supervised window by it, and teardown's nested-home check read it on a parent
record. Both only ever ask whether they are looking at a persistent direct
report, so both are the role axis and neither needed what the work produces.

The watcher's helper is renamed to say what it answers, which is what made the
one remaining stale reference visible - a variable read with no assignment left,
caught at runtime by the triage suite rather than by lint.

* test: carry the axis library into the old-bin conformance fixture

The old-versus-new teardown conformance case builds a mixed tree: the entry
points come from the baseline commit while their siblings are copied from the
working tree. Several of those copied siblings now source the axis library, so
the fixture needs it present or the baseline entry point dies looking for it.

* docs: teach recovery and provisioning the identity axes

The recovery and secondmate procedures still told an agent to look for the
retired single field, so the instructions that decide which playbook applies
would have gone on naming a field the fleet is removing. Each of these asks
only who the worker is, so each now says role.

* docs: state plainly that the stage axis has one value nothing writes yet

The ruled value set includes delivered, and the spawn and the reflag write the
other two. Nothing writes delivered: the honest place to stamp it is after a
confirmed landing, inside the merge path's own private metadata rewrite, whose
ordering, device, and single-link invariants that path owns - so the writer is
a deliberate follow-up rather than something bolted on beside this split.

Say so in both the library and the registry, because a declared value that
never appears is otherwise read as evidence that the task did not land.

* refactor(bin): tidy the teardown identity block

Reads in the order it acts: the refusal and why it exists, then the axes it
protects. Also drops a local the remote path no longer reads and closes the gap
the moved admission reminder left.

* refactor(bin): drop teardown's now-unread copy of the deprecated field

Every teardown branch reads an axis now, and the ledger record carries role and
deliverable, so nothing consulted the alias teardown still parsed on the way in.

With that gone no consumer anywhere branches on the deprecated field, which
changes what its retirement is waiting for: not a reader migration, but a full
task cycle on the axes across every home. The registry says so precisely,
because a retirement condition nobody can evaluate is how an alias becomes
permanent.

* feat(bin): render the identity axes in the fleet view

The view's type column was the last thing reading the deprecated field, and it
could only ever show one of the three facts a row carries. It now renders the
role and deliverable together and appends the stage when it is not the spawn
default, so a reflagged ship is visible as one at a glance instead of being
indistinguishable from a commissioned one - which is the whole reason the
lifecycle axis exists.

* test: carry the axis library into the gotmp fake roots

Both fake roots symlink the real teardown and each sibling it sources, so the
new axis library has to be linked in beside them or teardown dies looking for it
before it reaches the behavior these cases assert.

* fix(bin): keep a polled record's PR identity intact when writing an axis

A task's metadata doubles as PR identity, and its parser refuses any
unrecognized key that appears after the pr= line - the rule that stops a
tampered record from smuggling a second identity past an armed merge poll.
Backfill appended, so the first startup sweep over a home with a live merge
watch invalidated exactly the records that had one, and the watcher stopped
honoring those polls. Nothing reported it, because a poll that is refused looks
the same as a poll with nothing to say.

Axis writes now land before the pr= line, through one helper both the backfill
and the reflag use, and the regression pins the position rather than only the
parse so a future writer cannot reintroduce it by appending again.

Found by the PR-check security suite, which was green on the base and red here.

* no-mistakes(review): make reflag atomic, dual-carry kind on v1 wire surfaces

* no-mistakes(document): migrate leftover scout-promotion wording to reflag

* no-mistakes(document): reflag retired promote verb in scout brief output
sbracewell64 added a commit that referenced this pull request Aug 11, 2026
…ename promote to reflag (#65)

* docs: open the fleet's vocabulary-collision registry

The fleet resolved name collisions wherever they surfaced, so a ruling was
only ever findable by whoever remembered making it, and the platform's own
Register 3 had no counterpart on this side.

Open docs/vocabulary-collisions.md as the single owner of every word carrying
more than one meaning across the fleet, the platform, and the vendor tools both
depend on. It ships seeded with the ruled dispositions rather than empty: axi
and execution keep their names with the evidence recorded, skill and watch and
lifecycle take mandated qualified forms, promotion splits with the fleet verb
becoming reflag, and kind splits into three axes.

A rename or a split row also states the obsolete name's retirement condition,
so no superseded path is left with an open-ended life. This lands before any
rename it governs: the map exists first, then the moves it records.

* feat(bin): split task identity into role, deliverable, and stage axes

One kind= field carried three independent facts at once: who the worker is,
what the task produces, and where the task stands in its life. Every consumer
reconstructed the axis it cared about from a value that also encoded the two it
did not, and the scout-to-ship operation expressed a lifecycle transition by
rewriting a deliverable type - which is why a reflagged ship and a commissioned
one were indistinguishable, and why a requested agent role had nowhere to land
that would not have made a fourth conflated dimension.

bin/fm-task-axis-lib.sh becomes the single owner of role=, deliverable=, and
stage=, of their values, and of the total derivation from the retired field, so
no consumer spells that mapping itself. Migration follows the ordered contract:

- Dual-write first. Every writer emits the axes beside kind=, which keeps
  reading unchanged while records converge.
- Backfill by derivation, forward-only and idempotent, in a startup sweep that
  runs only under the fleet lock and leaves a converged home byte-identical.
  Stage is deliberately NOT derived: the old field could not distinguish a
  reflagged ship from a commissioned one, so backfill records the
  lower-information value rather than inventing a fact.
- Migrate readers one axis at a time - role across the secondmate-membership
  consumers, then deliverable across the pipeline and teardown-protection
  consumers - splitting the conditions that had mixed both.

kind= stays dual-written for now; the registry owns its retirement condition.
While it stays, a record whose alias contradicts its axes is REFUSED rather
than resolved, because either side could be the stale one and teardown choosing
between a protected ship worktree and a scratch scout worktree by luck is how
unlanded work gets discarded.

The scout-to-ship operation becomes bin/fm-reflag.sh in the same change, since
it is what makes the stage axis true. The old name stays only as a bounded shim
that forwards, warns, and records each use, so its own retirement is settled by
evidence rather than by memory.

Coverage is red-capable: each guard was witnessed failing with its behavior
removed before being trusted green - the derivation table, dual-write,
backfill idempotence, the shim's evidence, and the refusal in the library, in
reflagging, and in teardown.

* docs: move the fleet's instructions onto the new names and axes

The rename and the axis split are only real once the instructions that drive
them say so, so this carries the fleet's own vocabulary across: the scout
outcome section reflags rather than promotes, the metadata field list names the
three axes and the deprecated field they replace, and the captain-facing
do-not-expose list drops a word the fleet no longer uses internally.

Knowledge routing gains one line: a word with a second live meaning goes to the
collision registry, never settled locally in whichever file it surfaced in.
That is the rule that keeps the registry from going stale the first time
someone is in a hurry.

Also moves teardown's admission release reminder into the admission library,
which already owns that policy. Its test previously reconstructed the function
by parsing teardown's source and eval-ing the fragment, so it broke the moment
the function read a variable defined outside it - and a test that reads
implementation source is exactly what CONTRIBUTING forbids. The reminder now
takes its inputs as arguments and the test calls it directly.

* fix(bin): move the watcher and nested-home checks onto the role axis

Two consumers still read the deprecated field: the watcher classified every
supervised window by it, and teardown's nested-home check read it on a parent
record. Both only ever ask whether they are looking at a persistent direct
report, so both are the role axis and neither needed what the work produces.

The watcher's helper is renamed to say what it answers, which is what made the
one remaining stale reference visible - a variable read with no assignment left,
caught at runtime by the triage suite rather than by lint.

* test: carry the axis library into the old-bin conformance fixture

The old-versus-new teardown conformance case builds a mixed tree: the entry
points come from the baseline commit while their siblings are copied from the
working tree. Several of those copied siblings now source the axis library, so
the fixture needs it present or the baseline entry point dies looking for it.

* docs: teach recovery and provisioning the identity axes

The recovery and secondmate procedures still told an agent to look for the
retired single field, so the instructions that decide which playbook applies
would have gone on naming a field the fleet is removing. Each of these asks
only who the worker is, so each now says role.

* docs: state plainly that the stage axis has one value nothing writes yet

The ruled value set includes delivered, and the spawn and the reflag write the
other two. Nothing writes delivered: the honest place to stamp it is after a
confirmed landing, inside the merge path's own private metadata rewrite, whose
ordering, device, and single-link invariants that path owns - so the writer is
a deliberate follow-up rather than something bolted on beside this split.

Say so in both the library and the registry, because a declared value that
never appears is otherwise read as evidence that the task did not land.

* refactor(bin): tidy the teardown identity block

Reads in the order it acts: the refusal and why it exists, then the axes it
protects. Also drops a local the remote path no longer reads and closes the gap
the moved admission reminder left.

* refactor(bin): drop teardown's now-unread copy of the deprecated field

Every teardown branch reads an axis now, and the ledger record carries role and
deliverable, so nothing consulted the alias teardown still parsed on the way in.

With that gone no consumer anywhere branches on the deprecated field, which
changes what its retirement is waiting for: not a reader migration, but a full
task cycle on the axes across every home. The registry says so precisely,
because a retirement condition nobody can evaluate is how an alias becomes
permanent.

* feat(bin): render the identity axes in the fleet view

The view's type column was the last thing reading the deprecated field, and it
could only ever show one of the three facts a row carries. It now renders the
role and deliverable together and appends the stage when it is not the spawn
default, so a reflagged ship is visible as one at a glance instead of being
indistinguishable from a commissioned one - which is the whole reason the
lifecycle axis exists.

* test: carry the axis library into the gotmp fake roots

Both fake roots symlink the real teardown and each sibling it sources, so the
new axis library has to be linked in beside them or teardown dies looking for it
before it reaches the behavior these cases assert.

* fix(bin): keep a polled record's PR identity intact when writing an axis

A task's metadata doubles as PR identity, and its parser refuses any
unrecognized key that appears after the pr= line - the rule that stops a
tampered record from smuggling a second identity past an armed merge poll.
Backfill appended, so the first startup sweep over a home with a live merge
watch invalidated exactly the records that had one, and the watcher stopped
honoring those polls. Nothing reported it, because a poll that is refused looks
the same as a poll with nothing to say.

Axis writes now land before the pr= line, through one helper both the backfill
and the reflag use, and the regression pins the position rather than only the
parse so a future writer cannot reintroduce it by appending again.

Found by the PR-check security suite, which was green on the base and red here.

* no-mistakes(review): make reflag atomic, dual-carry kind on v1 wire surfaces

* no-mistakes(document): migrate leftover scout-promotion wording to reflag

* no-mistakes(document): reflag retired promote verb in scout brief output
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