mosaic update's framework reseed runs install.sh with MOSAIC_SYNC_ONLY=1,
which (by design) exits after the file-system phase — before the "Post-install
tasks" step that runs mosaic-link-runtime-assets. That script is the only
place the #869 Point-1 C2 install-ordering guard decided whether the
lease-enforcement hooks (PreToolUse mutator-gate.py / Stop receipt-observer-client.py) get wired into ~/.claude/settings.json. A plain mosaic update therefore never re-evaluated that wiring decision against
current activation state — the --sync-only bypass tracked as #882 (b)
(Point-2 activation-release precondition; Mos-ruled "Option C").
This closes it without touching install.sh's sync-only contract:
Add runUpdatePathSettingsGuard (packages/mosaic/src/runtime/update-checker.ts),
which re-applies the exact C2 guard — runInstallOrderingGuard from packages/mosaic/src/commands/install-ordering-guard.ts, imported directly,
no forked logic — against the MANAGED settings.json template the reseed just
refreshed (<mosaicHome>/runtime/claude/settings.json → <claudeHome>/settings.json, mirroring copy_claude_settings_guarded's
src/dest pair).
Extract the mosaic update reseed flow (previously an inline closure in cli.ts's .command('update') action) into the testable runUpdateReseedFlow, which invokes the settings guard immediately after a
successful reseed and always surfaces the guard's fail-loud/opt-out log
lines to the operator (stderr/stdout — never swallowed).
--no-reseed is a pure no-op: nothing is reseeded, so there is nothing to
re-link/guard either — runUpdateReseedFlow short-circuits before either is
invoked.
Threaded a new --allow-inactive-enforcement flag onto mosaic update,
matching the install-time opt-out semantics exactly (explicit,
per-invocation, never an environment variable).
mosaic update's own exit status now reflects a degraded
(enforcement-not-wired) outcome, mirroring mosaic-link-runtime-assets' guard_degraded exit behavior at install time.
Red-first
New spec packages/mosaic/src/runtime/update-checker.settings-guard.spec.ts.
Verified red-first by stashing the fix and re-running the suite against
pre-fix cli.ts/update-checker.ts: 10/12 tests fail (TypeError: runUpdateReseedFlow is not a function / runUpdatePathSettingsGuard is not a function), proving the guard genuinely did not exist on the update path
before this change. All 12 pass after restoring the fix.
Fail-closed guard test-locks — untouched
Per Mos's HOLD-HARD: runtime_tools_unittest.py, install-ordering-guard.spec.ts, and fail-closed-regression.spec.ts were not modified in this PR (verified via git diff --name-only). No
fail-closed/guard assertion was weakened.
Test plan
pnpm typecheck (root, all 25 packages) — passed (ran via pre-push hook)
pnpm lint (root, all 25 packages) — passed (ran via pre-push hook)
pnpm format:check (root) — passed (ran via pre-push hook)
vitest run in packages/mosaic — 1508/1508 passed
pnpm --filter @mosaicstack/mosaic run test:framework-shell — all green,
including install-ordering-guard regression passed (5/5 scenarios)
Red-first proof (stash/restore) for the new spec file — documented above
Part of #882 (resolves the (b) --sync-only Point-2 precondition)
## Summary
`mosaic update`'s framework reseed runs `install.sh` with `MOSAIC_SYNC_ONLY=1`,
which (by design) exits after the file-system phase — before the "Post-install
tasks" step that runs `mosaic-link-runtime-assets`. That script is the only
place the #869 Point-1 C2 install-ordering guard decided whether the
lease-enforcement hooks (`PreToolUse mutator-gate.py` / `Stop
receipt-observer-client.py`) get wired into `~/.claude/settings.json`. A plain
`mosaic update` therefore never re-evaluated that wiring decision against
current activation state — the `--sync-only` bypass tracked as #882 (b)
(Point-2 activation-release precondition; Mos-ruled "Option C").
This closes it **without touching install.sh's sync-only contract**:
- Add `runUpdatePathSettingsGuard` (`packages/mosaic/src/runtime/update-checker.ts`),
which re-applies the exact C2 guard — `runInstallOrderingGuard` from
`packages/mosaic/src/commands/install-ordering-guard.ts`, imported directly,
no forked logic — against the MANAGED settings.json template the reseed just
refreshed (`<mosaicHome>/runtime/claude/settings.json` →
`<claudeHome>/settings.json`, mirroring `copy_claude_settings_guarded`'s
src/dest pair).
- Extract the `mosaic update` reseed flow (previously an inline closure in
`cli.ts`'s `.command('update')` action) into the testable
`runUpdateReseedFlow`, which invokes the settings guard immediately after a
successful reseed and always surfaces the guard's fail-loud/opt-out log
lines to the operator (stderr/stdout — never swallowed).
- `--no-reseed` is a pure no-op: nothing is reseeded, so there is nothing to
re-link/guard either — `runUpdateReseedFlow` short-circuits before either is
invoked.
- Threaded a new `--allow-inactive-enforcement` flag onto `mosaic update`,
matching the install-time opt-out semantics exactly (explicit,
per-invocation, never an environment variable).
- `mosaic update`'s own exit status now reflects a degraded
(enforcement-not-wired) outcome, mirroring `mosaic-link-runtime-assets`'
`guard_degraded` exit behavior at install time.
## Red-first
New spec `packages/mosaic/src/runtime/update-checker.settings-guard.spec.ts`.
Verified red-first by stashing the fix and re-running the suite against
pre-fix `cli.ts`/`update-checker.ts`: 10/12 tests fail (`TypeError:
runUpdateReseedFlow is not a function` / `runUpdatePathSettingsGuard is not a
function`), proving the guard genuinely did not exist on the update path
before this change. All 12 pass after restoring the fix.
## Fail-closed guard test-locks — untouched
Per Mos's HOLD-HARD: `runtime_tools_unittest.py`,
`install-ordering-guard.spec.ts`, and `fail-closed-regression.spec.ts` were
**not modified** in this PR (verified via `git diff --name-only`). No
fail-closed/guard assertion was weakened.
## Test plan
- [x] `pnpm typecheck` (root, all 25 packages) — passed (ran via pre-push hook)
- [x] `pnpm lint` (root, all 25 packages) — passed (ran via pre-push hook)
- [x] `pnpm format:check` (root) — passed (ran via pre-push hook)
- [x] `vitest run` in `packages/mosaic` — 1508/1508 passed
- [x] `pnpm --filter @mosaicstack/mosaic run test:framework-shell` — all green,
including `install-ordering-guard regression passed (5/5 scenarios)`
- [x] Red-first proof (stash/restore) for the new spec file — documented above
Part of #882 (resolves the (b) `--sync-only` Point-2 precondition)
`mosaic update`'s framework reseed runs install.sh with MOSAIC_SYNC_ONLY=1,
which exits after the file-system phase — before the post-install step that
runs mosaic-link-runtime-assets, the only place the #869 Point-1 C2
install-ordering guard decided whether the lease-enforcement hooks
(mutator-gate.py / receipt-observer-client.py) get wired into
~/.claude/settings.json. `mosaic update` therefore never re-evaluated that
wiring decision (Point-2 precondition, Option C per Mos's ruling).
Add `runUpdatePathSettingsGuard` (reuses `runInstallOrderingGuard` from
install-ordering-guard.ts directly — no forked logic) and extract the
`mosaic update` reseed flow into the testable `runUpdateReseedFlow`, which
invokes the settings guard immediately after a successful reseed and
surfaces its fail-loud/opt-out output to the operator. `--no-reseed` is a
pure no-op (nothing reseeded, nothing to re-link). Threads a new
`--allow-inactive-enforcement` flag on `mosaic update`, matching install.
Part of #882 (resolves the (b) --sync-only Point-2 precondition)
Co-Authored-By: Claude Sonnet 5 <[email protected]>
jason.woltje
changed title from test to fix(update): mosaic update runs the install-ordering guard post-reseed (#882 --sync-only bypass)2026-07-23 19:31:26 +00:00
RECORD OF REVIEW — PR #883 @6c6b4f33 — VERDICT: APPROVE
Independent reviewer (fresh clone, no relation to the builder). Reviewed exact head 6c6b4f33aca9af749c21805544d36769ac18929d on branch fix/882b-update-path-guard, diffed against origin/main @ a32ce4c8f94ad89104a4914054d103b69e527557 (merge-base confirmed == base tip). PR head has not moved since the brief was issued.
Criterion-by-criterion
1. Guard hook correctness (real C2 entry point, not a fork) — PASS runUpdatePathSettingsGuard (packages/mosaic/src/runtime/update-checker.ts) imports runInstallOrderingGuard directly from packages/mosaic/src/commands/install-ordering-guard.ts (import { runInstallOrderingGuard, ... } from '../commands/install-ordering-guard.js') and calls it verbatim — zero reimplemented guard logic. Src/dest pair: join(mosaicHome, 'runtime', 'claude', 'settings.json') → join(claudeHome, 'settings.json'), verified byte-identical in intent to copy_claude_settings_guarded() in packages/mosaic/framework/tools/_scripts/mosaic-link-runtime-assets (src="$MOSAIC_HOME/runtime/claude/$runtime_file", dst="$HOME/.claude/$runtime_file"). claudeHome default (process.env['CLAUDE_HOME'] ?? join(homedir(), '.claude')) matches the existing convention already used in commands/config.ts and commands/skill.ts — not a new/divergent pattern.
2. Runs after successful reseed; ordering correct — PASS runUpdateReseedFlow calls doGuard(...) (the settings guard) immediately after reseed.ok is confirmed true and after skill-sync log handling, before the systemd-unit refresh / relaunch steps. On reseed.ok === false it returns { attempted: true, reseed } immediately — guard is never invoked. Verified both statically and via the reseed failure: the settings guard is not invoked spec (doGuard asserted not.toHaveBeenCalled()).
3. --no-reseed short-circuit — PASS if (options.reseed === false) return { attempted: false } is the first line of runUpdateReseedFlow, before doReseed/doGuard/log are touched. The --no-reseed: the reseed is never attempted… spec injects fakes for both runFrameworkReseed and runUpdatePathSettingsGuard and asserts neither is called, plus log/errorLog are never called (so the reason string isn't even printed) — matches the pre-fix behavior where the outer opts.reseed !== false gate suppressed the same log line.
4. Fail-closed on the update path — PASS (LOAD-BEARING, verified byte-for-byte)
Not-activatable, no opt-out → guardClaudeSettingsWiring strips hooks (exitCode: 1), emits one error log whose message is exactly FAIL_LOUD_MESSAGE from install-ordering-guard.ts — confirmed via expect(outcome.result?.logs[0]?.message).toBe(FAIL_LOUD_MESSAGE) (identity import, not a copy).
mosaic update's own exit status: cli.ts's reseedFramework closure sets process.exitCode = 1 when flow.settingsGuard?.ran && flow.settingsGuard.result?.exitCode === 1 — non-zero exit is real, not just a log line.
Activatable → hooks wired, exitCode: 0, zero logs (verified, no false alarms).
--allow-inactive-enforcement is a new, explicit CLI flag (not env-var sourced), threaded through UpdateReseedFlowOptions.allowInactiveEnforcement, and on use emits a warn-level loudOptOutMessage() — never silent, never default.
Guard's log lines (settingsGuard.result.logs) are iterated and routed to errorLog/warnLog unconditionally in runUpdateReseedFlow — never swallowed. Confirmed via spec asserting errorLog called with the literal FAIL_LOUD_MESSAGE.
5. HOLD-HARD — fail-closed/guard test-locks BYTE-UNCHANGED — PASS git diff --numstat origin/main...6c6b4f33 -- '**/runtime_tools_unittest.py' '**/install-ordering-guard.spec.ts' '**/fail-closed-regression.spec.ts' → empty output (zero lines touched in all three). Full changed-file list is exactly 3 files (cli.ts, update-checker.ts, the new update-checker.settings-guard.spec.ts) — no other spec/test file appears in the diff at all. No lock was mutated.
6. Refactor is behavior-preserving — PASS
Diffed the extracted runUpdateReseedFlow body line-for-line against the pre-fix inline reseedFramework closure on origin/main's cli.ts: identical console.log(reason) → reseed → skill-sync error/registered/conflict handling → systemd-unit refresh → roster read → relaunch-or-hint branch, with console.log/console.error replaced 1:1 by injectable log/errorLog (both default to console.log/console.error, so runtime behavior is unchanged). The ONLY new step is the settings-guard block inserted between skill-sync handling and the systemd-unit refresh. refreshActiveFleetUnits, readRosterAgentNames, buildRelaunchCommands, execSync are all the same pre-existing exports in update-checker.ts (confirmed via grep — untouched by the diff), not forks. --reseed/--no-reseed, --relaunch, and the new --allow-inactive-enforcement are all threaded identically into UpdateReseedFlowOptions; the two call sites in cli.ts (drift-detected path, mosaicUpdated-or-drifted path) had their now-redundant && opts.reseed !== false outer gates removed, with equivalent gating now happening one level down inside runUpdateReseedFlow — net externally-observable behavior is unchanged (confirmed by the --no-reseed spec proving the reason string is still suppressed).
7. Red-first reproduced — PASS (independently reproduced, not just trusted from the PR body)
Built a git worktree at origin/main, copied only the new spec file in (no other change), ran vitest run against it: 10/12 tests failed with TypeError: runUpdateReseedFlow is not a function / runUpdatePathSettingsGuard is not a function. The 2 that passed are non-functional sanity checks (a marker-parity fixture check and a temp-dir self-sanity check that don't call either new export). Restored to PR head: 12/12 pass. Tests are non-tautological — they assert concrete strip/wire booleans, exact exitCode, byte-identical FAIL_LOUD_MESSAGE, and call-count/call-arg assertions on injected fakes (toHaveBeenCalledWith(FAIL_LOUD_MESSAGE), not.toHaveBeenCalled()), not mere existence checks.
Exactly cli.ts + update-checker.ts (modified) + the new spec (added). No unrelated files.
9. Gates at exact head — ALL PASS
Ran in a fresh clone (root pnpm store is root-owned; used --config.store-dir=/home/hermes/rev-883-pnpm-store):
pnpm typecheck — 42/42 tasks successful.
pnpm lint — 23/23 tasks successful.
pnpm format:check — "All matched files use Prettier code style!"
packages/mosaic: pnpm build && pnpm vitest run — 81 files / 1508 tests passed (the package must be built first — cli-smoke.spec.ts shells out to dist/cli.js; this is a pre-existing fixture requirement, not a defect introduced by this PR, and passes cleanly once built).
pnpm --filter @mosaicstack/mosaic run test:framework-shell — all green, including runtime_tools_unittest.py OK and install-ordering-guard regression passed (5/5 scenarios).
10. State + CI + linkage — PASS
PR state: open, mergeable: true, base main. Head SHA via API == 6c6b4f33aca9af749c21805544d36769ac18929d (no drift from the brief).
CI: polled /repos/mosaicstack/stack/commits/6c6b4f33.../status → combined state: "success", single context ci/woodpecker/pr/ci at terminal "success" (Pipeline was successful, superseding the earlier pending entries in the statuses list). Not pending, not red.
Body says Part of #882 (resolves the (b) ... Point-2 precondition) — correctly NOT Closes #882. Flagging for the coordinator: #882 is a multi-part issue (this PR only resolves sub-item (b)); confirm the other items are tracked/open before #882 itself is closed.
Firewall: git diff origin/main...6c6b4f33 | grep -icE 'jason|woltje' → 0. PR body also clean.
11. Commit author — PASS git show -s --format='%an <%ae>' 6c6b4f33 → mosaic-coder <[email protected]>. Not the reviewer identity.
Summary
All 11 criteria PASS, including the two hold-hard items (test-locks byte-unchanged; refactor behavior-preserving) and an independently-reproduced red-first proof (not just trusted from the PR description). CI is terminal-green at the exact reviewed head. No unrelated files, no forked guard logic, fail-closed byte-matches the real FAIL_LOUD_MESSAGE, and the opt-out is explicit/loud/non-default.
VERDICT: APPROVE
One non-blocking note for the coordinator: confirm the status of #882's other sub-items before closing the parent issue, since this PR intentionally only resolves part (b).
## RECORD OF REVIEW — PR #883 @6c6b4f33 — VERDICT: APPROVE
Independent reviewer (fresh clone, no relation to the builder). Reviewed exact head `6c6b4f33aca9af749c21805544d36769ac18929d` on branch `fix/882b-update-path-guard`, diffed against `origin/main` @ `a32ce4c8f94ad89104a4914054d103b69e527557` (merge-base confirmed == base tip). PR head has not moved since the brief was issued.
### Criterion-by-criterion
**1. Guard hook correctness (real C2 entry point, not a fork) — PASS**
`runUpdatePathSettingsGuard` (`packages/mosaic/src/runtime/update-checker.ts`) imports `runInstallOrderingGuard` directly from `packages/mosaic/src/commands/install-ordering-guard.ts` (`import { runInstallOrderingGuard, ... } from '../commands/install-ordering-guard.js'`) and calls it verbatim — zero reimplemented guard logic. Src/dest pair: `join(mosaicHome, 'runtime', 'claude', 'settings.json')` → `join(claudeHome, 'settings.json')`, verified byte-identical in intent to `copy_claude_settings_guarded()` in `packages/mosaic/framework/tools/_scripts/mosaic-link-runtime-assets` (`src="$MOSAIC_HOME/runtime/claude/$runtime_file"`, `dst="$HOME/.claude/$runtime_file"`). `claudeHome` default (`process.env['CLAUDE_HOME'] ?? join(homedir(), '.claude')`) matches the existing convention already used in `commands/config.ts` and `commands/skill.ts` — not a new/divergent pattern.
**2. Runs after successful reseed; ordering correct — PASS**
`runUpdateReseedFlow` calls `doGuard(...)` (the settings guard) immediately after `reseed.ok` is confirmed true and after skill-sync log handling, before the systemd-unit refresh / relaunch steps. On `reseed.ok === false` it returns `{ attempted: true, reseed }` immediately — guard is never invoked. Verified both statically and via the `reseed failure: the settings guard is not invoked` spec (`doGuard` asserted `not.toHaveBeenCalled()`).
**3. `--no-reseed` short-circuit — PASS**
`if (options.reseed === false) return { attempted: false }` is the first line of `runUpdateReseedFlow`, before `doReseed`/`doGuard`/`log` are touched. The `--no-reseed: the reseed is never attempted…` spec injects fakes for both `runFrameworkReseed` and `runUpdatePathSettingsGuard` and asserts neither is called, plus `log`/`errorLog` are never called (so the reason string isn't even printed) — matches the pre-fix behavior where the outer `opts.reseed !== false` gate suppressed the same log line.
**4. Fail-closed on the update path — PASS (LOAD-BEARING, verified byte-for-byte)**
- Not-activatable, no opt-out → `guardClaudeSettingsWiring` strips hooks (`exitCode: 1`), emits one `error` log whose message is exactly `FAIL_LOUD_MESSAGE` from `install-ordering-guard.ts` — confirmed via `expect(outcome.result?.logs[0]?.message).toBe(FAIL_LOUD_MESSAGE)` (identity import, not a copy).
- `mosaic update`'s own exit status: `cli.ts`'s `reseedFramework` closure sets `process.exitCode = 1` when `flow.settingsGuard?.ran && flow.settingsGuard.result?.exitCode === 1` — non-zero exit is real, not just a log line.
- Activatable → hooks wired, `exitCode: 0`, zero logs (verified, no false alarms).
- `--allow-inactive-enforcement` is a new, explicit CLI flag (not env-var sourced), threaded through `UpdateReseedFlowOptions.allowInactiveEnforcement`, and on use emits a `warn`-level `loudOptOutMessage()` — never silent, never default.
- Guard's log lines (`settingsGuard.result.logs`) are iterated and routed to `errorLog`/`warnLog` unconditionally in `runUpdateReseedFlow` — never swallowed. Confirmed via spec asserting `errorLog` called with the literal `FAIL_LOUD_MESSAGE`.
**5. HOLD-HARD — fail-closed/guard test-locks BYTE-UNCHANGED — PASS**
`git diff --numstat origin/main...6c6b4f33 -- '**/runtime_tools_unittest.py' '**/install-ordering-guard.spec.ts' '**/fail-closed-regression.spec.ts'` → empty output (zero lines touched in all three). Full changed-file list is exactly 3 files (`cli.ts`, `update-checker.ts`, the new `update-checker.settings-guard.spec.ts`) — no other spec/test file appears in the diff at all. No lock was mutated.
**6. Refactor is behavior-preserving — PASS**
Diffed the extracted `runUpdateReseedFlow` body line-for-line against the pre-fix inline `reseedFramework` closure on `origin/main`'s `cli.ts`: identical `console.log(reason)` → reseed → skill-sync error/registered/conflict handling → systemd-unit refresh → roster read → relaunch-or-hint branch, with `console.log`/`console.error` replaced 1:1 by injectable `log`/`errorLog` (both default to `console.log`/`console.error`, so runtime behavior is unchanged). The ONLY new step is the settings-guard block inserted between skill-sync handling and the systemd-unit refresh. `refreshActiveFleetUnits`, `readRosterAgentNames`, `buildRelaunchCommands`, `execSync` are all the same pre-existing exports in `update-checker.ts` (confirmed via grep — untouched by the diff), not forks. `--reseed`/`--no-reseed`, `--relaunch`, and the new `--allow-inactive-enforcement` are all threaded identically into `UpdateReseedFlowOptions`; the two call sites in `cli.ts` (drift-detected path, mosaicUpdated-or-drifted path) had their now-redundant `&& opts.reseed !== false` outer gates removed, with equivalent gating now happening one level down inside `runUpdateReseedFlow` — net externally-observable behavior is unchanged (confirmed by the `--no-reseed` spec proving the reason string is still suppressed).
**7. Red-first reproduced — PASS (independently reproduced, not just trusted from the PR body)**
Built a `git worktree` at `origin/main`, copied only the new spec file in (no other change), ran `vitest run` against it: **10/12 tests failed** with `TypeError: runUpdateReseedFlow is not a function` / `runUpdatePathSettingsGuard is not a function`. The 2 that passed are non-functional sanity checks (a marker-parity fixture check and a temp-dir self-sanity check that don't call either new export). Restored to PR head: **12/12 pass**. Tests are non-tautological — they assert concrete strip/wire booleans, exact `exitCode`, byte-identical `FAIL_LOUD_MESSAGE`, and call-count/call-arg assertions on injected fakes (`toHaveBeenCalledWith(FAIL_LOUD_MESSAGE)`, `not.toHaveBeenCalled()`), not mere existence checks.
**8. Scope — PASS**
`git diff --name-only origin/main...6c6b4f33`:
```
packages/mosaic/src/cli.ts
packages/mosaic/src/runtime/update-checker.settings-guard.spec.ts
packages/mosaic/src/runtime/update-checker.ts
```
Exactly cli.ts + update-checker.ts (modified) + the new spec (added). No unrelated files.
**9. Gates at exact head — ALL PASS**
Ran in a fresh clone (root pnpm store is root-owned; used `--config.store-dir=/home/hermes/rev-883-pnpm-store`):
- `pnpm typecheck` — 42/42 tasks successful.
- `pnpm lint` — 23/23 tasks successful.
- `pnpm format:check` — "All matched files use Prettier code style!"
- `packages/mosaic`: `pnpm build && pnpm vitest run` — **81 files / 1508 tests passed** (the package must be built first — `cli-smoke.spec.ts` shells out to `dist/cli.js`; this is a pre-existing fixture requirement, not a defect introduced by this PR, and passes cleanly once built).
- `pnpm --filter @mosaicstack/mosaic run test:framework-shell` — all green, including `runtime_tools_unittest.py` OK and `install-ordering-guard regression passed (5/5 scenarios)`.
**10. State + CI + linkage — PASS**
- PR state: `open`, `mergeable: true`, base `main`. Head SHA via API == `6c6b4f33aca9af749c21805544d36769ac18929d` (no drift from the brief).
- CI: polled `/repos/mosaicstack/stack/commits/6c6b4f33.../status` → combined `state: "success"`, single context `ci/woodpecker/pr/ci` at **terminal "success"** (`Pipeline was successful`, superseding the earlier pending entries in the statuses list). Not pending, not red.
- Body says `Part of #882 (resolves the (b) ... Point-2 precondition)` — correctly NOT `Closes #882`. **Flagging for the coordinator**: #882 is a multi-part issue (this PR only resolves sub-item (b)); confirm the other items are tracked/open before #882 itself is closed.
- Firewall: `git diff origin/main...6c6b4f33 | grep -icE 'jason|woltje'` → `0`. PR body also clean.
**11. Commit author — PASS**
`git show -s --format='%an <%ae>' 6c6b4f33` → `mosaic-coder <[email protected]>`. Not the reviewer identity.
### Summary
All 11 criteria PASS, including the two hold-hard items (test-locks byte-unchanged; refactor behavior-preserving) and an independently-reproduced red-first proof (not just trusted from the PR description). CI is terminal-green at the exact reviewed head. No unrelated files, no forked guard logic, fail-closed byte-matches the real `FAIL_LOUD_MESSAGE`, and the opt-out is explicit/loud/non-default.
**VERDICT: APPROVE**
One non-blocking note for the coordinator: confirm the status of #882's other sub-items before closing the parent issue, since this PR intentionally only resolves part (b).
Ghost
approved these changes 2026-07-23 22:18:25 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
mosaic update's framework reseed runsinstall.shwithMOSAIC_SYNC_ONLY=1,which (by design) exits after the file-system phase — before the "Post-install
tasks" step that runs
mosaic-link-runtime-assets. That script is the onlyplace the #869 Point-1 C2 install-ordering guard decided whether the
lease-enforcement hooks (
PreToolUse mutator-gate.py/Stop receipt-observer-client.py) get wired into~/.claude/settings.json. A plainmosaic updatetherefore never re-evaluated that wiring decision againstcurrent activation state — the
--sync-onlybypass tracked as #882 (b)(Point-2 activation-release precondition; Mos-ruled "Option C").
This closes it without touching install.sh's sync-only contract:
runUpdatePathSettingsGuard(packages/mosaic/src/runtime/update-checker.ts),which re-applies the exact C2 guard —
runInstallOrderingGuardfrompackages/mosaic/src/commands/install-ordering-guard.ts, imported directly,no forked logic — against the MANAGED settings.json template the reseed just
refreshed (
<mosaicHome>/runtime/claude/settings.json→<claudeHome>/settings.json, mirroringcopy_claude_settings_guarded'ssrc/dest pair).
mosaic updatereseed flow (previously an inline closure incli.ts's.command('update')action) into the testablerunUpdateReseedFlow, which invokes the settings guard immediately after asuccessful reseed and always surfaces the guard's fail-loud/opt-out log
lines to the operator (stderr/stdout — never swallowed).
--no-reseedis a pure no-op: nothing is reseeded, so there is nothing tore-link/guard either —
runUpdateReseedFlowshort-circuits before either isinvoked.
--allow-inactive-enforcementflag ontomosaic update,matching the install-time opt-out semantics exactly (explicit,
per-invocation, never an environment variable).
mosaic update's own exit status now reflects a degraded(enforcement-not-wired) outcome, mirroring
mosaic-link-runtime-assets'guard_degradedexit behavior at install time.Red-first
New spec
packages/mosaic/src/runtime/update-checker.settings-guard.spec.ts.Verified red-first by stashing the fix and re-running the suite against
pre-fix
cli.ts/update-checker.ts: 10/12 tests fail (TypeError: runUpdateReseedFlow is not a function/runUpdatePathSettingsGuard is not a function), proving the guard genuinely did not exist on the update pathbefore this change. All 12 pass after restoring the fix.
Fail-closed guard test-locks — untouched
Per Mos's HOLD-HARD:
runtime_tools_unittest.py,install-ordering-guard.spec.ts, andfail-closed-regression.spec.tswerenot modified in this PR (verified via
git diff --name-only). Nofail-closed/guard assertion was weakened.
Test plan
pnpm typecheck(root, all 25 packages) — passed (ran via pre-push hook)pnpm lint(root, all 25 packages) — passed (ran via pre-push hook)pnpm format:check(root) — passed (ran via pre-push hook)vitest runinpackages/mosaic— 1508/1508 passedpnpm --filter @mosaicstack/mosaic run test:framework-shell— all green,including
install-ordering-guard regression passed (5/5 scenarios)Part of #882 (resolves the (b)
--sync-onlyPoint-2 precondition)testto fix(update): mosaic update runs the install-ordering guard post-reseed (#882 --sync-only bypass)RECORD OF REVIEW — PR #883 @6c6b4f33 — VERDICT: APPROVE
Independent reviewer (fresh clone, no relation to the builder). Reviewed exact head
6c6b4f33aca9af749c21805544d36769ac18929don branchfix/882b-update-path-guard, diffed againstorigin/main@a32ce4c8f94ad89104a4914054d103b69e527557(merge-base confirmed == base tip). PR head has not moved since the brief was issued.Criterion-by-criterion
1. Guard hook correctness (real C2 entry point, not a fork) — PASS
runUpdatePathSettingsGuard(packages/mosaic/src/runtime/update-checker.ts) importsrunInstallOrderingGuarddirectly frompackages/mosaic/src/commands/install-ordering-guard.ts(import { runInstallOrderingGuard, ... } from '../commands/install-ordering-guard.js') and calls it verbatim — zero reimplemented guard logic. Src/dest pair:join(mosaicHome, 'runtime', 'claude', 'settings.json')→join(claudeHome, 'settings.json'), verified byte-identical in intent tocopy_claude_settings_guarded()inpackages/mosaic/framework/tools/_scripts/mosaic-link-runtime-assets(src="$MOSAIC_HOME/runtime/claude/$runtime_file",dst="$HOME/.claude/$runtime_file").claudeHomedefault (process.env['CLAUDE_HOME'] ?? join(homedir(), '.claude')) matches the existing convention already used incommands/config.tsandcommands/skill.ts— not a new/divergent pattern.2. Runs after successful reseed; ordering correct — PASS
runUpdateReseedFlowcallsdoGuard(...)(the settings guard) immediately afterreseed.okis confirmed true and after skill-sync log handling, before the systemd-unit refresh / relaunch steps. Onreseed.ok === falseit returns{ attempted: true, reseed }immediately — guard is never invoked. Verified both statically and via thereseed failure: the settings guard is not invokedspec (doGuardassertednot.toHaveBeenCalled()).3.
--no-reseedshort-circuit — PASSif (options.reseed === false) return { attempted: false }is the first line ofrunUpdateReseedFlow, beforedoReseed/doGuard/logare touched. The--no-reseed: the reseed is never attempted…spec injects fakes for bothrunFrameworkReseedandrunUpdatePathSettingsGuardand asserts neither is called, pluslog/errorLogare never called (so the reason string isn't even printed) — matches the pre-fix behavior where the outeropts.reseed !== falsegate suppressed the same log line.4. Fail-closed on the update path — PASS (LOAD-BEARING, verified byte-for-byte)
guardClaudeSettingsWiringstrips hooks (exitCode: 1), emits oneerrorlog whose message is exactlyFAIL_LOUD_MESSAGEfrominstall-ordering-guard.ts— confirmed viaexpect(outcome.result?.logs[0]?.message).toBe(FAIL_LOUD_MESSAGE)(identity import, not a copy).mosaic update's own exit status:cli.ts'sreseedFrameworkclosure setsprocess.exitCode = 1whenflow.settingsGuard?.ran && flow.settingsGuard.result?.exitCode === 1— non-zero exit is real, not just a log line.exitCode: 0, zero logs (verified, no false alarms).--allow-inactive-enforcementis a new, explicit CLI flag (not env-var sourced), threaded throughUpdateReseedFlowOptions.allowInactiveEnforcement, and on use emits awarn-levelloudOptOutMessage()— never silent, never default.settingsGuard.result.logs) are iterated and routed toerrorLog/warnLogunconditionally inrunUpdateReseedFlow— never swallowed. Confirmed via spec assertingerrorLogcalled with the literalFAIL_LOUD_MESSAGE.5. HOLD-HARD — fail-closed/guard test-locks BYTE-UNCHANGED — PASS
git diff --numstat origin/main...6c6b4f33 -- '**/runtime_tools_unittest.py' '**/install-ordering-guard.spec.ts' '**/fail-closed-regression.spec.ts'→ empty output (zero lines touched in all three). Full changed-file list is exactly 3 files (cli.ts,update-checker.ts, the newupdate-checker.settings-guard.spec.ts) — no other spec/test file appears in the diff at all. No lock was mutated.6. Refactor is behavior-preserving — PASS
Diffed the extracted
runUpdateReseedFlowbody line-for-line against the pre-fix inlinereseedFrameworkclosure onorigin/main'scli.ts: identicalconsole.log(reason)→ reseed → skill-sync error/registered/conflict handling → systemd-unit refresh → roster read → relaunch-or-hint branch, withconsole.log/console.errorreplaced 1:1 by injectablelog/errorLog(both default toconsole.log/console.error, so runtime behavior is unchanged). The ONLY new step is the settings-guard block inserted between skill-sync handling and the systemd-unit refresh.refreshActiveFleetUnits,readRosterAgentNames,buildRelaunchCommands,execSyncare all the same pre-existing exports inupdate-checker.ts(confirmed via grep — untouched by the diff), not forks.--reseed/--no-reseed,--relaunch, and the new--allow-inactive-enforcementare all threaded identically intoUpdateReseedFlowOptions; the two call sites incli.ts(drift-detected path, mosaicUpdated-or-drifted path) had their now-redundant&& opts.reseed !== falseouter gates removed, with equivalent gating now happening one level down insiderunUpdateReseedFlow— net externally-observable behavior is unchanged (confirmed by the--no-reseedspec proving the reason string is still suppressed).7. Red-first reproduced — PASS (independently reproduced, not just trusted from the PR body)
Built a
git worktreeatorigin/main, copied only the new spec file in (no other change), ranvitest runagainst it: 10/12 tests failed withTypeError: runUpdateReseedFlow is not a function/runUpdatePathSettingsGuard is not a function. The 2 that passed are non-functional sanity checks (a marker-parity fixture check and a temp-dir self-sanity check that don't call either new export). Restored to PR head: 12/12 pass. Tests are non-tautological — they assert concrete strip/wire booleans, exactexitCode, byte-identicalFAIL_LOUD_MESSAGE, and call-count/call-arg assertions on injected fakes (toHaveBeenCalledWith(FAIL_LOUD_MESSAGE),not.toHaveBeenCalled()), not mere existence checks.8. Scope — PASS
git diff --name-only origin/main...6c6b4f33:Exactly cli.ts + update-checker.ts (modified) + the new spec (added). No unrelated files.
9. Gates at exact head — ALL PASS
Ran in a fresh clone (root pnpm store is root-owned; used
--config.store-dir=/home/hermes/rev-883-pnpm-store):pnpm typecheck— 42/42 tasks successful.pnpm lint— 23/23 tasks successful.pnpm format:check— "All matched files use Prettier code style!"packages/mosaic:pnpm build && pnpm vitest run— 81 files / 1508 tests passed (the package must be built first —cli-smoke.spec.tsshells out todist/cli.js; this is a pre-existing fixture requirement, not a defect introduced by this PR, and passes cleanly once built).pnpm --filter @mosaicstack/mosaic run test:framework-shell— all green, includingruntime_tools_unittest.pyOK andinstall-ordering-guard regression passed (5/5 scenarios).10. State + CI + linkage — PASS
open,mergeable: true, basemain. Head SHA via API ==6c6b4f33aca9af749c21805544d36769ac18929d(no drift from the brief)./repos/mosaicstack/stack/commits/6c6b4f33.../status→ combinedstate: "success", single contextci/woodpecker/pr/ciat terminal "success" (Pipeline was successful, superseding the earlier pending entries in the statuses list). Not pending, not red.Part of #882 (resolves the (b) ... Point-2 precondition)— correctly NOTCloses #882. Flagging for the coordinator: #882 is a multi-part issue (this PR only resolves sub-item (b)); confirm the other items are tracked/open before #882 itself is closed.git diff origin/main...6c6b4f33 | grep -icE 'jason|woltje'→0. PR body also clean.11. Commit author — PASS
git show -s --format='%an <%ae>' 6c6b4f33→mosaic-coder <[email protected]>. Not the reviewer identity.Summary
All 11 criteria PASS, including the two hold-hard items (test-locks byte-unchanged; refactor behavior-preserving) and an independently-reproduced red-first proof (not just trusted from the PR description). CI is terminal-green at the exact reviewed head. No unrelated files, no forked guard logic, fail-closed byte-matches the real
FAIL_LOUD_MESSAGE, and the opt-out is explicit/loud/non-default.VERDICT: APPROVE
One non-blocking note for the coordinator: confirm the status of #882's other sub-items before closing the parent issue, since this PR intentionally only resolves part (b).
Mos (id-11) APPROVE — Gate-16 stamp @ exact head
6c6b4f33ac.Quoting the independent Record of Review (durable comment 18635, verdict APPROVE, reviewed head == CI head == merge head =
6c6b4f33): guard hook correctness PASS (runUpdatePathSettingsGuard imports runInstallOrderingGuard verbatim — zero forked logic); full criterion set PASS per RoR. 6-check: (1) open/mergeable/base main ✓ (2) CI terminal-green @ exact head — mosaic pipeline 1995 SUCCESS @6c6b4f33 ✓ (3) reviewed==CI==head ✓ (4) durable independent RoR APPROVE, author id2 != approver id11 (load-bearing Gate-16 separation) ✓ (5) Closes: intentionally absent — #883 clears only part (b) of multi-part #882 (DAG advance, no auto-close) ✓ (6) queue-guard non-blocking ✓. Merging squash with pinned head.