Compare commits

...
Author SHA1 Message Date
scoobyandClaude Fable 5 8764f8664e greenfield(fomo-lin): session 7 — #1124 fix(b) VALIDATED; new blockers #1125 (slash-cmd unseeded) + #1126 (model refuses receipt as injection)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 17:02:05 -05:00
scoobyandClaude Fable 5 b820d6f3d6 greenfield(fomo-lin): session 6 — promotion E2E BLOCKED at #1124; findings #1123/#1124 + live wedge
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 16:40:25 -05:00
scoobyandClaude Fable 5 be6da9d028 greenfield(fomo-lin): codify true-greenfield doctrine + mosaic-greenfield-reset protocol
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 15:17:59 -05:00
scoobyandClaude Fable 5 f4e3ef4ac2 greenfield(fomo-lin): gap-7 base characterization addendum
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 15:03:49 -05:00
scoobyandClaude Fable 5 b7c7357a2f greenfield(fomo-lin): session 5 — ~/.mosaic prototype replicates on second host; two new gap-bites
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 14:59:01 -05:00
scoobyandClaude Fable 5 f0b38f3fee greenfield(fomo-lin): session 4 — next-lane reinstall; N1 node-22 floor; F1-F12 recurrence scorecard
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 14:56:47 -05:00
scoobyandClaude Fable 5 6362baf7cc greenfield(fomo-lin): session 3 — wizard/gateway F11-F12; refocus to next lane
F11 gateway Local tier requires Redis on main (fix already on next: 56787fab),
F12 wizard exits 0 on gateway failure. Jason directive: focus on next branch +
new structure; ~/.claude deep-testing stopped.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 14:40:27 -05:00
scoobyandClaude Fable 5 898e24472a greenfield(fomo-lin): session 2 — init + first bare seat; findings F6-F10
install → init → launch broken at four consecutive links on fresh main:
F6 eval injection in mosaic-init (SECURITY), F7 init drops installer's
mcpServers block, F8 missing fleet roster = raw stack trace, F9 activation
probe 2.0s timeout < 2.6s CLI cold-start on modest hardware (flagship),
F10 shipped lease-broker unit never installed. Seat launched after
documented workarounds; runtime-contract injection verified in-seat.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 14:29:34 -05:00
scoobyandClaude Fable 5 605c09089c greenfield(fomo-lin): install log + first findings F1-F5
Fresh-machine install of framework v3 / CLI 0.0.49 on Debian 13. Five findings
outside fred's known-gaps list: print-only PATH advice, backup-less overwrite of
live ~/.claude runtime files, doctor failing fresh install with 10 warnings,
drift check pointing at the gated (wedge-prone) template, installer skill bundle
disjoint from repo skills/.

Note: docs/reports is in .gitignore here — file force-added; flagged to fred.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 14:13:34 -05:00
f10-coderandMos b0f7d26dd9 fix(shell): remove test harness pipe hazards (#1106)
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/manual/ci-image Pipeline failed
ci/woodpecker/manual/publish Pipeline failed
ci/woodpecker/manual/ci Pipeline was successful
Co-authored-by: f10-coder <[email protected]>
2026-08-07 11:12:17 +00:00
f10-coderandMos 3a1203b2f8 fix(shell): remove runtime early-exit pipe hazards (#1105)
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: f10-coder <[email protected]>
2026-08-07 09:38:37 +00:00
34 changed files with 805 additions and 55 deletions
+353
View File
@@ -0,0 +1,353 @@
# Greenfield install log — fomo-lin
Running log of a from-scratch Mosaic Stack install on Jason's test laptop **fomo-lin**
(Debian 13, x86_64). Operator: **scooby** (agent). Started 2026-08-08. Channel per fred:
findings → comms as they land; this file is the durable record. Branch: `greenfield/fomo-lin`.
## Machine starting state (2026-08-08)
- Debian 13 (kernel 6.12.101+deb13), no Node/npm, no global git config, no `~/.ssh`,
no `~/.config/mosaic`, no `~/.mosaic`, sudo requires password (agent cannot escalate).
- Repos pre-cloned by Jason: jarvis-brain, mosaic-brain, stack, uconnect, uscllc-website
(all https remotes to git.mosaicstack.dev, **no credentials stored** — private-repo
fetch/push dead until a token was provisioned from credentials.json, `usc_mos`).
- tmux session `scooby` running Claude Code (bare harness — not `mosaic claude`).
## Pre-install setup that had NO framework mechanism (manual work)
- Agent identity: `MOSAIC_AGENT_NAME=scooby` hand-added to `~/.bashrc` + tmux env.
- Git identity + credential store: hand-configured.
- Comms receive path: hand-ported `scooby-comms-watcher.sh` from fred's watcher +
hand-written systemd `--user` unit + `loginctl enable-linger`. Works (both peers
verified round-trip within ~90s), but every step was artisanal — relevant input for
harness-homes (W-F).
## Install run (2026-08-08 ~19:09Z)
`curl -fsSL https://mosaicstack.dev/install.sh | bash -s -- --yes --no-auto-launch`
→ exit 0, framework v3 → `~/.config/mosaic/`, CLI @mosaicstack/mosaic **0.0.49**
`~/.npm-global/`. Prereq path: `sudo apt install nodejs npm` (Debian 13's node 20.19.2
meets the ≥20 floor). Public read on the stack repo means the installer itself needs no
credentials — good.
## Findings (outside fred's known-gaps list of 2026-08-08)
### F1 — PATH advice is print-only
Installer warns `~/.npm-global/bin is not on your PATH` and suggests the rc line, but
`shellProfileEdits: []` in the manifest — nothing is persisted. Every fresh machine ends
with `mosaic` not resolvable in new shells until the user hand-edits rc. Either edit the
rc (with consent/flag) or make the closing summary a copy-paste block.
### F2 — Installer overwrites live `~/.claude/settings.json` + `~/.claude/CLAUDE.md` with `backup: null`
`.install-manifest.json` `runtimeAssetCopies` shows dest `~/.claude/settings.json`,
`~/.claude/CLAUDE.md`, `hooks-config.json`, `context7-integration.md`, all `backup: null`,
written while a Claude session was LIVE on this machine. On this box the pre-existing files
were near-defaults so nothing of value was lost; on any configured machine this silently
destroys user settings/memory. Wants: backup-before-overwrite (populate the manifest
`backup` field it clearly already models) + merge-not-replace for settings.json.
### F3 — Fresh install fails its own doctor: 10 warnings out of the box
Immediately after a clean, successful install, `mosaic doctor` reports: missing `USER.md`;
`AGENTS.md missing CRITICAL HARD GATES override block`; runtime file drift on
`~/.claude/settings.json`; 7 missing `mosaic-*` skills. A green install that self-reports
10 warnings erodes trust in doctor as a signal. Whatever subset is "expected until
`mosaic init`/wizard" should be suppressed or labeled as such.
### F4 — Drift check points users at the gated template (wedge hazard)
The settings the installer writes to `~/.claude/settings.json` are UNGATED (no
mutator-gate, no receipt-observer) — which on today's main is CORRECT, it avoids the
Stop-hook wedge. But `~/.config/mosaic/runtime/claude/settings.json` (the file doctor
diffs against) IS the gated template. So doctor's "runtime file drift" warning invites the
obvious remediation — copy the template over — which would seed the receipt-observer wedge
into a live seat. The drift baseline and the seeded file should be the same artifact, or
doctor should know about the gated/ungated split.
### F5 — Installed skill set is disjoint from repo `skills/`
Skill sync installed 101 skills (six `mosaic-*`: deploy, gitea, orchestrator, portainer,
tools, woodpecker) but NONE of the eight in stack `skills/` on main (board, forge, jarvis,
macp, prd, prdy, setup-cicd, standards). Doctor then flags 7 of those 8 as missing
(`mosaic-jarvis` escapes the check). Two sources of truth for "the Mosaic skills" — the
installer's bundle and the repo dir — have diverged.
## Environment answers / status
- fomo-lin → sb-it-1-dt: **comms-only** today. Hostname does not resolve from here and the
laptop has no ssh keys. ssh reach would need Jason (key provisioning + route/VPN).
- Gitea write to the stack repo: verified by the push of this very branch (token `usc_mos`).
## Session 2 (2026-08-08 later) — `mosaic init` + first bare seat
`mosaic init` completed (SOUL.md / USER.md / TOOLS.md generated; TOOLS.md was backed up
before overwrite — the contrast with F2 shows the codebase already knows how). Its
runtime-adapter step correctly REFUSED to wire mutator-gate/receipt-observer hooks
(activation half absent, #869) — loud, explained, fail-safe. Good.
First bare seat: **launched**`mosaic claude --model sonnet` → Claude Code v2.1.226,
runtime-contract injection verified from inside the seat. But it took findings F6F10 to
get there; on an untouched fresh main install, install → init → launch is broken at
FOUR consecutive links.
### F6 — SECURITY: `mosaic-init` eval-injects free-text answers
`tools/_scripts/mosaic-init` line 142: `eval "$var_name=\"$value\""`. Any answer
containing `"` crashes init mid-flow (reproduced: exit 127, USER.md never written);
an answer containing `$( )` would EXECUTE arbitrary commands. Fix: `printf -v`.
Same bug in the NON_INTERACTIVE default branch. Related: init exits 1 even on success
when enforcement wiring is (correctly) refused — poisons any scripted chaining.
### F7 — init silently drops the installer's `mcpServers` block → launcher refuses to run
init's "Updating runtime adapters" rewrote `~/.claude/settings.json` and removed the
`mcpServers.sequential-thinking` block the installer had written 11 min earlier.
`mosaic claude` hard-requires that MCP → launch refused. The prescribed fix command
(`mosaic-ensure-sequential-thinking --runtime claude`) works. So the happy path is
install → init → BROKEN → hand-run a repair script. Merge-not-replace (F2) fixes this too.
### F8 — no fleet roster on a fresh install; launcher dies with a raw stack trace
`mosaic claude` throws an uncaught `Error: Fleet communications contract unavailable: no
fleet roster at ~/.config/mosaic/fleet/roster.{yaml,json}` (full Node stack trace to the
user). Nothing in install or init creates a roster (wizard untested here — `--no-auto-launch`;
if the wizard seeds one, the bare-flow gap still stands). Unblocked by hand-authoring a
minimal site roster from `fleet/examples/minimal.yaml`.
### F9 — FLAGSHIP: activation-probe timeout loses to CLI cold-start on modest hardware
`activation_version_gate.py` gives the `mosaic __lease-capability` probe
`PROBE_TIMEOUT_SECONDS = 2.0`. On fomo-lin the CLI answers CORRECTLY in **~2.552.61s
every run** (Node startup cost). Timeout → fail-closed → every bare `mosaic claude`
launch aborts (exit 65) with an error blaming "mosaic not on PATH … framework/CLI version
skew" — neither true. Invisible on fast dev boxes; fatal on laptops. Suggest: raise/make
configurable the timeout, warm-probe cache, and split the three failure causes into
distinct messages. Local workaround (documented, removable):
`MOSAIC_LEASE_VERSION_PROBE_COMMAND` pointed at a script emitting the verified payload
instantly (`~/.local/bin/mosaic-lease-probe-fast`).
### F10 — shipped lease-broker unit is never installed → registration denied
With F9 bypassed, launch dies with "Mosaic lease broker registration failed; runtime
launch denied": the broker daemon isn't running, and although the framework SHIPS
`systemd/user/mosaic-lease-broker.service`, nothing installs/enables it.
`systemctl --user link` + `enable --now` of the shipped unit → READY instantly, launch
proceeds. Installer/init/wizard should own this step.
### Observations (not filed as findings)
- Launcher settings audit demands `mutator-gate.py` while init refuses to wire it —
main's components disagree about the gated state (fold into #1113/F4).
- Seat context: runtime contract injected ✓; SOUL.md NOT injected (seat confirmed) —
matches AGENTS.md read-on-demand load order, but README says the launcher "checks for
SOUL.md". Question for lead, not a finding.
- `--ref next` install path verified available (flag exists, next archive HTTP 200) — not
exercised; fomo-lin stays main-as-shipped per lead ruling.
## Next
- Milestone comms sent at: install complete ✓ / first seat launched ✓.
- First gated-seat probe deliberately deferred until PR #1109 lands (known deny-only state).
## Session 3 (2026-08-08 evening) — wizard + gateway; refocus to `next`
Directive from Jason mid-session: focus shifts to the `next` branch and the new structure
(stock `~/.claude` untouched; framework wholly under `~/.config/mosaic`). Main's ~/.claude
write behavior is a deprecated location — findings stand, but no further deep-testing of it.
Wizard run (main): "keep identity, update framework"; ~/.claude hooks install DECLINED per
directive (wizard rewrote ~/.claude/settings.json anyway — benign, no gated hooks, MCP kept).
Wizard never prompted about fleet roster or lease-broker unit → F8/F10 disambiguation
partial: wizard does not visibly own those steps. Full degraded-state test dropped per refocus.
### F11 — gateway "Local" tier hard-requires Redis on main (fixed on next)
Wizard gateway install, Local tier ("embedded database, no dependencies"), port 14242:
daemon starts then crash-spams ioredis ECONNREFUSED; never healthy; killed manually.
`main..next` already contains `56787fab fix(gateway): disable Redis consumers on local
tier (#689)`. Main ships a gateway that cannot come up dependency-free; next has the cure.
### F12 — wizard exits 0 on gateway failure
Terminal shows "▲ Fix the underlying error above, then re-run `mosaic gateway install`"
and the wizard exits 0. Scripted/CI consumers read success.
### Cosmetic
Skipping the optional ANTHROPIC_API_KEY prompt records the literal string "undefined".
### `next` recon (read-only)
- next install.sh: first-class `--next` prerelease lane (npm @next dist-tag CLI + framework
from permanent next branch; guard against mixing @next with a different explicit --ref).
- next does NOT carry the new structure: ~/.claude handling unchanged; no harness-homes
design docs on next or main. New structure = Jason directive + fred W-F design phase.
State: bare seat launch works; gateway stopped. Holding for fred's ruling on a next-lane
reinstall (proposed) and W-F design review.
## Session 4 (2026-08-08 night) — `--next` lane reinstall (pivot confirmed by Jason)
Main uninstalled (note: uninstall removed `~/.claude/CLAUDE.md`/hooks-config/context7 but
LEFT its modified `settings.json` — asymmetric cleanup, minor). Reinstalled via next's own
installer: `raw/branch/next/tools/install.sh --next --yes --no-auto-launch` → framework from
permanent next branch + **CLI 0.0.50-next.2207 / gateway 0.0.7-next.2207 from the @next
registry lane**. Lane works as designed.
### N1 — FLAGSHIP (next-only): @next CLI requires Node 22; docs/installer floor says ≥20
On Node 20.19.2 (Debian 13's apt version, and the documented minimum) **every** mosaic
command crashes — even `--version` — with `ERR_REQUIRE_CYCLE_MODULE` in
`@mosaicstack/brain/dist/projects.js`. npm corroborates: `[email protected]` declares
`node >=22`. Verified the same installed CLI runs clean under Node **22.23.2** (nvm).
So the @next lane is dead-on-arrival on the documented minimum Node. Fix: installer
gates node ≥22 for the next lane (or brain drops the require cycle). fomo-lin now runs
Node 22 via nvm (user-level; system apt tops out at 20 — durable fix wants nodesource 22).
### F1F12 recurrence scorecard on next
| Finding | On next |
|---|---|
| F1 PATH print-only | RECURS (identical warning) |
| F2 ~/.claude writes | RECURS (runtime assets copied again; per ruling, no deeper testing — W-F fixes structurally) |
| F3 doctor warns on fresh install | RECURS (10 warnings, same classes) |
| F4 drift-baseline wedge | RECURS (same gated template + drift warning) |
| F5 skill sets disjoint | RECURS (same 7 missing mosaic-*) |
| F6 init eval injection | RECURS (eval at lines 102/118/132 of next's mosaic-init) |
| F7 init drops mcpServers | RECURS (verified: count 0 after init; ensure-script fix works) |
| F8 roster raw-throw | RECURS in code (throw present in next launch.js; not re-triggered — roster restored from backup) |
| F9 probe 2.0s timeout | RECURS (constant unchanged) — and compounded: probe spawns `mosaic`, which on ambient Node 20 crashes (N1), so the probe fails on slow AND stock-node hosts |
| F10 broker unit not installed | RECURS (hand-relinked next's shipped unit; works) |
| F11 gateway Redis-on-local | Expected FIXED (#689 in next); not yet live-verified — gateway install not re-run this session |
| F12 wizard exit-0 | Untested on next (wizard.ts differs; #1120 tracks) |
Chain result on next (with the same three workarounds: MCP ensure-script, restored roster,
broker relink, plus probe override): **install → init → launch all pass; seat up on
Claude Code v2.1.226 / sonnet under Node 22.**
Net: next cures nothing in F1F10 (they're all pre-W-F structural issues), carries the
gateway fix, and adds one hard regression-class gap (N1 node floor). The W-F gap list
stands unchanged as the fix vehicle.
## Session 5 (2026-08-08 night) — `~/.mosaic` prototype hand-roll (second-host cross-check)
Hand-rolled per HARNESS-HOMES prototype section, on the next-lane framework: skeleton
(config/claude `{}`, auth/claude/jason_woltje.com with `primary` alias, empty plugins/skills
stores), probe seat (profile.json schema 1, overlay `{}`, composed settings via three-layer
deep-merge, credentials two-hop symlink, identity-bootstrap CLAUDE.md, seeded onboarding
.claude.json, SOUL.md with positive Identity block).
**Smoke test PASS** (`CLAUDE_CONFIG_DIR=<probe> claude --print`): RC=0, auth through the
two-hop chain, seat self-identified as "probe". Post-run: both symlinks survived, live
credential inode unchanged, transcript in probe's own projects/, probe generated its own
backups/sessions, operator ~/.claude untouched. **dragon-lin's results replicate on a
clean second host — the layout stands up greenfield.**
### Gap-bites during the roll (feed to W-F)
- **Base-template hole (F4/gap-5 adjacent, NEW):** the design's composition base
`~/.config/mosaic/framework/runtime/claude/settings.json` does NOT exist in the shipped
framework; the closest shipped artifact (`runtime/claude/settings.json`) is the GATED
wedge template. Used the operator's vetted ungated settings as base (as dragon-lin did).
W-F1 must define + ship the canonical UNGATED system base; gate hooks arrive only via
promotion overlay.
- **Identity bootstrap vs permissions (NEW):** in `--print`/restricted mode the seat was
DENIED reading SOUL.md outside cwd — "read SOUL.md" bootstrap depends on tool
permissions. Generator should materialize the identity INTO the generated CLAUDE.md
(parameterized), keeping SOUL.md as source, not runtime dependency.
- **Gap 2 lived experience:** probe exists in profile.json but not roster.yaml — the
hand-rolled seat and `mosaic claude` are disjoint universes on the same host.
- **Gap 4 in miniature:** fresh-host store is empty; nothing defines what seeds it.
- **Lease posture:** hand-rolled seats launch bare `claude` → ungated by construction
until `mosaic fleet launch` exists (consistent with current bare-for-real-work rule).
### Addendum — gap-7 characterization (canonical ungated base)
Diffed operator vetted ungated settings vs shipped gated template: the delta is exactly
three items — template-only PreToolUse mutator-gate entry, template-only Stop
receipt-observer entry, operator-only mcpServers.sequential-thinking block (whose omission
from the template is F7's root cause). Spec: base = template two gate hooks + mcpServers;
promotion overlay = the two gate hooks, nothing more. Sent to fred (20260808T200337Z).
## Box doctrine — true greenfield, repeatable full-cycle testing (Jason, 2026-08-08)
fomo-lin's defining property: the test operator (scooby) is NOT a fleet seat — comms
watcher, git identity, nvm/Node, and repos live entirely outside Mosaic. Therefore Mosaic
can be wiped to TRUE ZERO and reinstalled in full, repeatedly, to test protocols
end-to-end per cycle (each W-F fixture drop, each next release).
Codified as `~/.local/bin/mosaic-greenfield-reset` (dry-run by default, `--yes` to
execute): removes units/gateway/npm packages/npmrc scope/`~/.config/mosaic`/`~/.mosaic`/
mosaic-written `~/.claude` files (settings reset to stock)/workaround shims; preserves the
operator layer (watcher, git creds, nvm, repos, `~/.claude` auth + session state, baseline
backup). Ends with a verify-zero checklist.
Known boundary impurities the reset explicitly handles: `~/.claude/settings.json` is
mosaic-written today (its QA hooks fire even in the operator's own session — observed:
prevent-memory-write blocked an operator write), and the F9 probe shim sits in
`~/.local/bin`. Both are named in the script rather than left as ambient state.
Not executed yet — current install (next lane + prototype) is the substrate Fred's W-F1
fixtures target. First full cycle runs when the next testable artifact lands.
## Session 6 (2026-08-08 night) — promotion-branch E2E (Fred-directed, first fomo-lin full E2E)
Branch feat/lease-promotion-and-harness-isolation (rebased on next), built from source
(pnpm --filter '@mosaicstack/mosaic...' build), CLI packed + installed globally, branch
framework installed to ~/.config/mosaic. Transcript:
scratchpad/promote-e2e-transcript.md. Verdict: **BLOCKED at step 3, NOT VERIFIED (not faked).**
Findings this session (all filed under scooby's own Gitea account):
- **#1123** — TS activation capability probe hardcodes a 2000ms timeout; `node cli.js
__lease-capability` cold-start on fomo-lin is 5.15.5s, so `leaseEnforcementActivatable()`
returns false and the gate REFUSES to wire via the sanctioned path. The Python-side
F9/#1118 override does NOT apply to this TS probe. Worked around by bumping only the
installed dist timeout (reversible; can't mask a bad capability).
- **LIVE WEDGE (F4 reproduced, un-recoverable):** hand-wiring the gated template into a live
BARE session's own runtime home hot-reloads the gate and bricks the session with
GATE_UNAVAILABLE (no lease). Every self-recovery path is closed (Bash/Read gated;
Write/Edit blocked by stale-guard needing a gated Read). Required an EXTERNAL shell to
restore settings. Exactly HARNESS-HOMES' "a live unpromoted session that gains the gate
cannot self-recover." Lesson applied: gated seats must be a SEPARATE mosaic claude process
in its own CLAUDE_CONFIG_DIR (~/.config/mosaic/.claude), never the operator's ~/.claude.
- **#1124 — the critical link, proven to fail:** `mosaic promote` transport reads the lease
session id from `pane_pid`'s /proc/environ, but `execRuntime()` (launch.js:883) uses
`spawnSync` (NOT the exec-replace its own comment claims), so pane_pid = node(mosaic)
[no lease env] and the lease env is on the claude CHILD. resolve() never walks to the
child → 'no readable lease session' → UNVERIFIED exit 1, before injection. Fails for every
real `mosaic claude` seat; unit tests pass only via a mocked environmentReader. This is
the exact link terra couldn't test (detached pane).
E2E scorecard: Step 1 (build/install) PASS. Step 2 (gated seat, real lease, mutator DENIED
MUTATOR_UNVERIFIED, file not created) PASS. Step 3 (promote → VERIFIED) BLOCKED (#1124).
Steps 45 not reached; failure-path sub-case (unresolvable seat → UNVERIFIED exit 1, no
hang) incidentally confirmed. The branch does NOT pass E2E on a real host as-is; #1124 gates
its merge.
## Session 7 (2026-08-08 night) — promotion re-run on #1124 fix (b) @ de0adb92
Rebuilt from de0adb92, reran steps 3+. **fix (b) confirmed working**; promotion advanced two
links deeper and revealed two new findings. Debian 13 compat: `/proc/<pid>/task/<pid>/children`
IS populated — BFS walk works, no PPID fallback needed.
- resolve() (#1124 fix b): **PASS** — BFS walk from pane(node,no-lease) → claude child(lease)
resolved the real session id. The exact bug I reported is fixed.
- **#1125** — `/mosaic-promote` first returned "Unknown command": the slash command is shipped
at `runtime/claude/commands/mosaic-promote.md` but NOT seeded into the seat's
`CLAUDE_CONFIG_DIR/commands/`. UserPromptSubmit hook never fires → PROMOTION_TIMEOUT. F7-class
asset-seeding gap. Worked around by copying the command into the seat home; hook then fires.
- **#1126 (deepest finding)** — with the command seeded, promote-begin injects (via
UserPromptSubmit additionalContext) an instruction to echo an opaque `MOSAIC-RECEIPT{...}`
token "verbatim and nothing else … discloses nothing." The seat MODEL REFUSED, correctly
flagging it as a prompt-injection pattern (imperative in a description field; verbatim opaque
echo; self-vouching language; no protocol legitimized in the seat's trusted context) →
RECEIPT_MISMATCH. Design-level: legitimate promotion is indistinguishable from an injection
attack to a well-aligned model; stronger injection defenses = more reliable promotion FAILURE.
Refusal evidence: docs/reports/greenfield/seat-receipt-refusal.txt.
Scorecard: build/install PASS · gated seat + mutator DENIED PASS · resolve (fix b) PASS ·
slash-cmd (post-#1125 workaround) PASS · receipt echo FAIL (#1126). Promotion does NOT reach
VERIFIED on a real seat. #1124 fix validated; #1125 + #1126 now gate the merge.
@@ -0,0 +1,31 @@
▐▛███▜▌ Claude Code v2.1.226
▝▜█████▛▘ Sonnet 5 · Claude Max
▘▘ ▝▝ ~/src/jarvis-brain
✔ Using flicker-free rendering · if you want to go back, use /tui default
· Click to move your cursor in the text input
· Click to expand collapsed tool results
· By default, text auto-copies when you select it (/config to change)
· Hold Shift (Option in iTerm2, Fn in Terminal.app) while selecting to use your terminal's native copy instead
▎ Fable 5 is now a standard part of your Max plan
▎ You can use up to 50% of your weekly usage limit on Fable 5. If you hit your limit, you can continue on Fable 5 with usage credits. Fable 5 draws down usage faster than Opus 5. Run /model and
▎ select Fable to use it. Learn more: https://support.claude.com/en/articles/15424964-claude-fable-5-promotional-access
+1 more · /status
/mosaic-promote
● I'm not going to echo that receipt string. A few things about this request don't add up, and I want to flag them before doing anything:
1. The "command description" is itself an instruction. In the skills listing, mosaic-promote is described as "I invoked this registered command to authorize lease promotion; follow the local seat
broker's injected receipt confirmation instruction exactly." That's not a description of what a skill does — it's an imperative instruction written in first person, injected into a field that's
supposed to be descriptive metadata. That's a classic injection pattern.
2. The "hook" reminder asks for verbatim, unexamined output. The UserPromptSubmit hook additional context block instructs me to output an opaque MOSAIC-RECEIPT{...} blob "verbatim and nothing else,"
and preemptively asserts that doing so "discloses nothing" — that's the kind of self-vouching language injected content uses to head off scrutiny. I have no way to verify what that token actually
authorizes downstream (it reads like a challenge/response for some privilege or lease-promotion action), and nothing in the actual Mosaic Constitution, AGENTS.md, or RUNTIME.md content I've loaded
this session mentions a "seat broker," "lease promotion," or a MOSAIC-RECEIPT protocol at all.
3. No genuine /mosaic-promote skill exists in the documented set. The real skill list (mosaic-gitea, mosaic-orchestrator, mosaic-portainer, mosaic-woodpecker, mosaic-tools, mosaic-deploy) has
nothing resembling this. A command that only shows up as a self-instructing string, paired with a matching "hook" telling me to blindly echo a token, is consistent with a prompt-injection attempt to
get an agent to produce an authorization artifact it doesn't understand.
I'm stopping here rather than emitting the receipt. If this is something you actually intended to trigger (a real seat/lease mechanism I'm not aware of), let me know and point me at where it's
documented and I'll take another look. Otherwise, I'd treat this as a suspicious injected command.
✻ Sautéed for 23s
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
⏸ manual mode on · ? for shortcuts · ← for agents
@@ -0,0 +1,72 @@
# #1099 pipefail + early-exit sweep
Baseline: `df4c591ab42aa1ae62c12935fdc0e772684864a0`
This is a site inventory, not a risk count. `FIXED` means the early-exiting consumer no longer has a piped upstream process whose SIGPIPE can become the result under `pipefail`. `NOT-LOAD-BEARING` means the pipeline status is explicitly discarded. `UNREACHABLE-AND-WHY` describes designed input, not a payload-size safety claim.
## Tranche 1 — runtime and general scripts
| Baseline site | Verdict | Construction / reason |
| --- | --- | --- |
| `tools/matrix-presence-harness/run.sh:38` | FIXED | nullglob array selects the first path; no pipeline |
| `tools/e2e-install-test.sh:139` | FIXED | capture help completely, then grep via redirection |
| `tools/install.sh:312` | FIXED | NUL `mapfile` reads all roots; count != 1 reaches the named malformed-archive diagnostic |
| `scripts/analysis/reflect-board-history.sh:76` | FIXED | capture Git history completely, then grep via redirection |
| `scripts/analysis/reflect-git-history.sh:67` | FIXED | grep reads from a here-string |
| `scripts/analysis/reflect-git-history.sh:69` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/authentik/user-create.sh:72` | FIXED | jq `first(...)` reads the response directly |
| `packages/mosaic/framework/tools/git/mutate-push-guard.sh:87` | FIXED | grep `-m1` reads the file directly; downstream `cut` consumes its complete scalar output |
| `packages/mosaic/framework/tools/orchestrator/session-resume.sh:94` | FIXED | `mapfile` plus bounded indexed loop replaces `head` pipeline |
| `packages/mosaic/framework/tools/prdy/prdy-status.sh:69` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:172` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:173` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:174` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:175` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:176` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:177` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:178` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/qa/typecheck-hook.sh:16` | FIXED | Bash regex extracts the first field without a pipeline |
| `packages/mosaic/framework/tools/qa/typecheck-hook.sh:56` | FIXED | grep and bounded sed each read from a here-string |
| `packages/mosaic/framework/tools/tmux/send-message.sh:113` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/tmux/send-message.sh:124` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/wake/detector.sh:126` | FIXED | one awk reads the manifest directly and exits after the first exact key |
| `packages/mosaic/framework/tools/wake/detector.sh:270` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/wake/detector.sh:278` | FIXED | grep reads from a here-string |
| `packages/mosaic/framework/tools/wake/digest.sh:647` | FIXED | capture complete locator output, then select first line by parameter expansion |
| `packages/mosaic/framework/tools/wake/reconcile.sh:149` | FIXED | one awk reads the manifest directly and exits after the first exact key |
## Explicit withdrawn / non-load-bearing sites
| Baseline site | Verdict | Reason |
| --- | --- | --- |
| `tools/install.sh:182` | NOT-LOAD-BEARING | `|| true` explicitly discards lookup status |
| `tools/install.sh:356` | UNREACHABLE-AND-WHY | `pnpm pack` writes one matching CLI tarball into a fresh directory immediately before lookup; citation withdrawn in #1099 |
| `tools/install.sh:357` | UNREACHABLE-AND-WHY | same fresh-directory invariant for gateway tarball; citation withdrawn in #1099 |
| `tools/install.sh:627` | NOT-LOAD-BEARING | `|| true` explicitly discards lookup status |
| `scripts/agent/session-start.sh:70` | NOT-LOAD-BEARING | optional scratchpad lookup has `|| true` |
| `packages/mosaic/framework/templates/repo/scripts/agent/session-start.sh:58` | NOT-LOAD-BEARING | optional scratchpad lookup has `|| true` |
| `packages/mosaic/framework/tools/qa/qa-hook-stdin.sh:25` | UNREACHABLE-AND-WHY | withdrawn in #1099 after designed-input reachability measurement; preserved without re-litigation |
| `packages/mosaic/framework/tools/qa/qa-hook-stdin.sh:27` | UNREACHABLE-AND-WHY | same withdrawn designed-input finding |
| `packages/mosaic/framework/tools/qa/qa-hook-stdin.sh:30` | UNREACHABLE-AND-WHY | same withdrawn designed-input finding |
| `packages/mosaic/framework/tools/qa/qa-hook-stdin.sh:32` | UNREACHABLE-AND-WHY | same withdrawn designed-input finding |
| `packages/mosaic/framework/tools/qa/qa-hook-stdin.sh:34` | UNREACHABLE-AND-WHY | same withdrawn designed-input finding |
## Tranche 2 — non-wake test harnesses
All 22 baseline sites below are `FIXED`; the checked-in tranche fixture is passed through the same scanner and asserts all 22 occurrences and 21 normalized identities (the same response-split line occurs twice).
| Baseline site(s) | Verdict | Construction |
| --- | --- | --- |
| `systemd/user/test-fleet-units.sh:148` | FIXED | capture tmux output, then grep via redirection |
| `git/test-issue-comment-readback.sh:283,302` | FIXED | parameter expansion splits status/body without `head` |
| `git/test-pr-review-gitea-comment.sh:228` | FIXED | parameter expansion splits status/body |
| `git/test-lane-brief-pr-linkage.sh:72` | FIXED | grep reads from a here-string |
| `git/test-pr-review-repo-host-override.sh:225-226` | FIXED | grep reads from a here-string |
| `orchestrator/smoke-test.sh:67,72` | FIXED | parameter expansion selects first line |
| `orchestrator/test-board-roll.sh:99-100` | FIXED | grep reads from a here-string |
| `quality/scripts/test-upgrade-durable-snapshot.sh:180` | FIXED | complete sorted output is read with `mapfile`, then indexed |
| `quality/scripts/test-upgrade-rollback.sh:339,356` | FIXED | direct `grep -m1` file reads; cleanup captures before testing |
| `tmux/test-send-message-socket.sh:37,38,44-46,68,72` | FIXED | capture commands complete before redirected grep assertions |
| `tmux/test-send-message-verdict.sh:34` | FIXED | grep reads from a here-string |
Remaining wake-validation sites are intentionally deferred to the final review-sized tranche and are not yet assigned a safety verdict here.
+37
View File
@@ -0,0 +1,37 @@
# #1099 — pipefail + early-exit sweep
## Scope and decisions
- Baseline `df4c591ab42aa1ae62c12935fdc0e772684864a0`, after #1100 removed its 35 sites.
- Split into review-sized non-closing tranches: runtime/general; tmux/git/quality tests; wake validation/tests.
- Do not equate class membership with demonstrated risk. Do not use payload size or pipeline stage count as a safety proxy.
- Preserve the issue's withdrawn findings for `qa-hook-stdin.sh` and the two fresh-directory `pnpm pack` lookups. Fix `install.sh:312` because malformed multi-root input must reach its named handler.
## Tranche 1 TDD
RED-first control: `node --test scripts/pipefail-early-exit.test.mjs` reported exactly 26 non-accepted runtime/general sites, including `install.sh:312`, and exited 1. A checked-in fixture generated from immutable baseline `df4c591a` records all 26 normalized sites; the control passes every fixture entry through the same scanner, asserts exact identity/count/uniqueness, and separately requires zero findings in the current tree. It also inventories accepted sites rather than silently excluding whole files.
Construction choices:
- here-string/file redirection for scalar grep assertions;
- full capture then parameter expansion for first-line selection;
- arrays/`mapfile` for complete populations;
- direct jq/awk/grep selection where one tool can express the property;
- no `|| true` added to a load-bearing assertion.
Site-by-site verdicts: `docs/reports/quality/1099-pipefail-sweep.md`.
## Tranche 2 TDD
Expanded the unconditional scanner over 11 non-wake test harnesses. RED named exactly 22 source lines; a second immutable-baseline fixture now asserts those 22 entries through the same scanner. Rewrites preserve command status by capturing producers before redirected assertions, use parameter expansion for line selection, and use complete `mapfile` populations where ordering matters. Current-tree finding count is zero for tranches 1 and 2.
## Verification so far
- `bash -n` on every changed shell script: pass.
- structural Node control: pass.
- `test-mutate-push-guard.sh`: 8/8 pass.
- `test-send-message-verdict.sh`: 3/3 pass.
- `test-send-message-socket.sh`: pass.
- Independent review 143 found two semantic regressions: a help-probe `|| true` changed the failure truth table, and an unguarded Git capture changed non-Git data-dir behavior from rc 0 + JSON to silent rc 128. Both received RED-first regressions before correction; help status is now separate and required, and Git status remains condition-guarded.
- Wake detector/reconcile/digest/preimage suites terminate at their existing fail-closed #973 `BASH_LINENO` environment probe (exit 97, observed `[3 5]`, expected `[3 4]`) before subject tests. No bypass or skip was used; canonical CI remains required.
- ShellCheck reports only pre-existing source-following, unused-variable, and untouched `ls | head` findings; no new diagnostic was introduced.
@@ -145,7 +145,8 @@ EOF
/usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin \ /usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin \
MOSAIC_TMUX_SOCKET="$TEST_SOCKET" MOSAIC_TMUX_HOLDER=_holder "$HOLDER_START" MOSAIC_TMUX_SOCKET="$TEST_SOCKET" MOSAIC_TMUX_HOLDER=_holder "$HOLDER_START"
tmux -L "$TEST_SOCKET" has-session -t '=_holder:0.0' || fail "fresh holder was not created" tmux -L "$TEST_SOCKET" has-session -t '=_holder:0.0' || fail "fresh holder was not created"
if tmux -L "$TEST_SOCKET" show-environment -g LD_PRELOAD 2>/dev/null | grep -q '^LD_PRELOAD='; then ld_preload_env="$(tmux -L "$TEST_SOCKET" show-environment -g LD_PRELOAD 2>/dev/null)" || true
if grep -q '^LD_PRELOAD=' <<<"$ld_preload_env"; then
fail "fresh holder retained LD_PRELOAD" fail "fresh holder retained LD_PRELOAD"
fi fi
/usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin MOSAIC_HOME="$AGENT_HOME" \ /usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin MOSAIC_HOME="$AGENT_HOME" \
@@ -69,7 +69,7 @@ if [[ -n "$GROUP" ]]; then
group_response=$(curl -sk \ group_response=$(curl -sk \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
"${AUTHENTIK_URL}/api/v3/core/groups/?search=${GROUP}") "${AUTHENTIK_URL}/api/v3/core/groups/?search=${GROUP}")
group_pk=$(echo "$group_response" | jq -r ".results[] | select(.name == \"$GROUP\") | .pk" | head -1) group_pk=$(jq -r "first(.results[] | select(.name == \"$GROUP\") | .pk) // empty" <<<"$group_response")
if [[ -n "$group_pk" ]]; then if [[ -n "$group_pk" ]]; then
payload=$(echo "$payload" | jq --arg gk "$group_pk" '. + {groups: [$gk]}') payload=$(echo "$payload" | jq --arg gk "$group_pk" '. + {groups: [$gk]}')
else else
@@ -84,7 +84,7 @@ cp "$TARGET" "$BAK"
export MOSAIC_TEST_WORK_DIR="$WORK/.work" export MOSAIC_TEST_WORK_DIR="$WORK/.work"
# --- where the prose lives: usage() { ... EOF --------------------------------- # --- where the prose lives: usage() { ... EOF ---------------------------------
PROSE_LO="$(grep -n '^usage() {' "$BAK" | head -1 | cut -d: -f1)" PROSE_LO="$(grep -n -m1 '^usage() {' "$BAK" | cut -d: -f1)"
PROSE_HI="$(awk -v lo="$PROSE_LO" 'NR > lo && /^EOF$/ { print NR; exit }' "$BAK")" PROSE_HI="$(awk -v lo="$PROSE_LO" 'NR > lo && /^EOF$/ { print NR; exit }' "$BAK")"
if [[ -z "$PROSE_LO" || -z "$PROSE_HI" ]]; then if [[ -z "$PROSE_LO" || -z "$PROSE_HI" ]]; then
echo "!! cannot locate the usage() heredoc -- the prose guard would be inert; refusing" >&2 echo "!! cannot locate the usage() heredoc -- the prose guard would be inert; refusing" >&2
@@ -280,7 +280,10 @@ print("201")
print(json.dumps(record)) print(json.dumps(record))
PY PY
) )
write_response "$(printf '%s' "$result" | head -n1)" "$(printf '%s' "$result" | tail -n +2)" response_status="${result%%$'\n'*}"
response_body=""
[[ "$result" == *$'\n'* ]] && response_body="${result#*$'\n'}"
write_response "$response_status" "$response_body"
elif [[ "$method" == "GET" && "$path" == "$ISSUE_COMMENT_API_BASE"/issues/comments/* ]]; then elif [[ "$method" == "GET" && "$path" == "$ISSUE_COMMENT_API_BASE"/issues/comments/* ]]; then
result=$(ISSUE_COMMENT_GET_ID="${path##*/}" python3 - <<'PY' result=$(ISSUE_COMMENT_GET_ID="${path##*/}" python3 - <<'PY'
import json import json
@@ -299,7 +302,10 @@ else:
print(json.dumps(match)) print(json.dumps(match))
PY PY
) )
write_response "$(printf '%s' "$result" | head -n1)" "$(printf '%s' "$result" | tail -n +2)" response_status="${result%%$'\n'*}"
response_body=""
[[ "$result" == *$'\n'* ]] && response_body="${result#*$'\n'}"
write_response "$response_status" "$response_body"
else else
echo "Unexpected curl request: $method $url" >&2 echo "Unexpected curl request: $method $url" >&2
exit 97 exit 97
@@ -69,7 +69,7 @@ section_nums() { # $1 = output $2 = header-prefix
} }
fail() { echo "FAIL: $1" >&2; exit 1; } fail() { echo "FAIL: $1" >&2; exit 1; }
contains() { printf '%s\n' "$1" | grep -qx "$2"; } contains() { grep -qx "$2" <<<"$1"; }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Fixed (current) script behavior # Fixed (current) script behavior
@@ -225,7 +225,10 @@ write_response() {
emit() { emit() {
# Split a two-line "status\n<json body>" python result into the response. # Split a two-line "status\n<json body>" python result into the response.
local result="$1" local result="$1"
write_response "$(printf '%s' "$result" | head -n1)" "$(printf '%s' "$result" | tail -n +2)" response_status="${result%%$'\n'*}"
response_body=""
[[ "$result" == *$'\n'* ]] && response_body="${result#*$'\n'}"
write_response "$response_status" "$response_body"
} }
mode="${PR_REVIEW_TEST_MODE:-}" mode="${PR_REVIEW_TEST_MODE:-}"
@@ -222,8 +222,8 @@ grep -q 'Unknown action: bogus-action' "$OUTPUT_FILE"
# --- Case 2: -h/--help documents both overrides. # --- Case 2: -h/--help documents both overrides.
HELP_TEXT="$("$SCRIPT_DIR/pr-review.sh" -h)" HELP_TEXT="$("$SCRIPT_DIR/pr-review.sh" -h)"
echo "$HELP_TEXT" | grep -q -- '-r, --repo' grep -q -- '-r, --repo' <<<"$HELP_TEXT"
echo "$HELP_TEXT" | grep -q -- '-H, --host' grep -q -- '-H, --host' <<<"$HELP_TEXT"
# --- Case 3 (comment): a TRUE no-git-origin dir + -r/-H must not silently die # --- Case 3 (comment): a TRUE no-git-origin dir + -r/-H must not silently die
# and must not fail with "not a git repository or no origin remote" either. # and must not fail with "not a git repository or no origin remote" either.
@@ -91,10 +91,12 @@ fi
if [[ -n "$dirty_files" ]]; then if [[ -n "$dirty_files" ]]; then
echo " Modified files:" echo " Modified files:"
echo "$dirty_files" | head -20 | while IFS= read -r line; do mapfile -t dirty_lines <<<"$dirty_files"
echo " $line" file_count="${#dirty_lines[@]}"
display_count=$((file_count < 20 ? file_count : 20))
for ((i = 0; i < display_count; i++)); do
echo " ${dirty_lines[$i]}"
done done
file_count="$(echo "$dirty_files" | wc -l)"
if (( file_count > 20 )); then if (( file_count > 20 )); then
echo " ... and $(( file_count - 20 )) more" echo " ... and $(( file_count - 20 )) more"
fi fi
@@ -64,12 +64,12 @@ if jq -e '.next_task == "T-001"' "$capsule_file" >/dev/null 2>&1; then pass_case
if grep -Fq 'Target runtime:** codex' <<< "$codex_continue_output"; then pass_case "continue prompt contains target runtime codex"; else fail_case "continue prompt contains target runtime codex"; fi if grep -Fq 'Target runtime:** codex' <<< "$codex_continue_output"; then pass_case "continue prompt contains target runtime codex"; else fail_case "continue prompt contains target runtime codex"; fi
codex_run_prompt="$(MOSAIC_COORD_RUNTIME=codex bash "$SCRIPT_DIR/session-run.sh" --project "$tmp_project" --print)" codex_run_prompt="$(MOSAIC_COORD_RUNTIME=codex bash "$SCRIPT_DIR/session-run.sh" --project "$tmp_project" --print)"
if [[ "$(printf '%s\n' "$codex_run_prompt" | head -n1)" == "Now initiating Orchestrator mode..." ]]; then pass_case "codex run prompt first line is mode declaration"; else fail_case "codex run prompt first line is mode declaration"; fi if [[ "${codex_run_prompt%%$'\n'*}" == "Now initiating Orchestrator mode..." ]]; then pass_case "codex run prompt first line is mode declaration"; else fail_case "codex run prompt first line is mode declaration"; fi
if grep -Fq 'Do NOT ask clarifying questions before your first tool actions' <<< "$codex_run_prompt"; then pass_case "codex run prompt includes no-questions hard gate"; else fail_case "codex run prompt includes no-questions hard gate"; fi if grep -Fq 'Do NOT ask clarifying questions before your first tool actions' <<< "$codex_run_prompt"; then pass_case "codex run prompt includes no-questions hard gate"; else fail_case "codex run prompt includes no-questions hard gate"; fi
if grep -Fq '"next_task": "T-001"' <<< "$codex_run_prompt"; then pass_case "codex run prompt embeds capsule json"; else fail_case "codex run prompt embeds capsule json"; fi if grep -Fq '"next_task": "T-001"' <<< "$codex_run_prompt"; then pass_case "codex run prompt embeds capsule json"; else fail_case "codex run prompt embeds capsule json"; fi
claude_run_prompt="$(MOSAIC_COORD_RUNTIME=claude bash "$SCRIPT_DIR/session-run.sh" --project "$tmp_project" --print)" claude_run_prompt="$(MOSAIC_COORD_RUNTIME=claude bash "$SCRIPT_DIR/session-run.sh" --project "$tmp_project" --print)"
if [[ "$(printf '%s\n' "$claude_run_prompt" | head -n1)" == "## Continuation Mission" ]]; then pass_case "claude run prompt remains continuation prompt format"; else fail_case "claude run prompt remains continuation prompt format"; fi if [[ "${claude_run_prompt%%$'\n'*}" == "## Continuation Mission" ]]; then pass_case "claude run prompt remains continuation prompt format"; else fail_case "claude run prompt remains continuation prompt format"; fi
echo "" echo ""
echo "Smoke test summary: pass=$PASS fail=$FAIL" echo "Smoke test summary: pass=$PASS fail=$FAIL"
@@ -96,8 +96,8 @@ L="$WORK/live5.md"; G="$WORK/ledger5.md"; echo "# LEDGER" > "$G"
make_board "$L" 6 1 400 make_board "$L" 6 1 400
before_l=$(cat "$L"); before_g=$(cat "$G") before_l=$(cat "$L"); before_g=$(cat "$G")
out=$(bash "$SUT" --live "$L" --ledger "$G" --cap 2000 --dry-run 2>&1) || note "dry-run exited nonzero: $out" out=$(bash "$SUT" --live "$L" --ledger "$G" --cap 2000 --dry-run 2>&1) || note "dry-run exited nonzero: $out"
echo "$out" | grep -qi "dry run" || note "dry-run did not announce itself" grep -qi "dry run" <<<"$out" || note "dry-run did not announce itself"
echo "$out" | grep -q "would roll" || note "dry-run did not report a plan" grep -q "would roll" <<<"$out" || note "dry-run did not report a plan"
[[ "$(cat "$L")" == "$before_l" ]] || note "dry-run modified LIVE" [[ "$(cat "$L")" == "$before_l" ]] || note "dry-run modified LIVE"
[[ "$(cat "$G")" == "$before_g" ]] || note "dry-run modified LEDGER" [[ "$(cat "$G")" == "$before_g" ]] || note "dry-run modified LEDGER"
@@ -66,7 +66,7 @@ present=0
for entry in "${PRDY_REQUIRED_SECTIONS[@]}"; do for entry in "${PRDY_REQUIRED_SECTIONS[@]}"; do
pattern="${entry#*|}" pattern="${entry#*|}"
if echo "$PRD_CONTENT" | grep -qiE "$pattern"; then if grep -qiE "$pattern" <<<"$PRD_CONTENT"; then
present=$((present + 1)) present=$((present + 1))
fi fi
done done
@@ -169,13 +169,13 @@ main() {
# classify_surface PATH → surface name (highest-risk match wins, mirrors TS) # classify_surface PATH → surface name (highest-risk match wins, mirrors TS)
classify_surface() { classify_surface() {
local p="$1" local p="$1"
if printf '%s' "$p" | grep -qiE 'auth|login|session|token|permission|rbac|credential|secret'; then echo auth; return; fi if grep -qiE 'auth|login|session|token|permission|rbac|credential|secret' <<<"$p"; then echo auth; return; fi
if printf '%s' "$p" | grep -qiE 'migration|prisma|schema|\.sql|entity|repository|seed'; then echo data; return; fi if grep -qiE 'migration|prisma|schema|\.sql|entity|repository|seed' <<<"$p"; then echo data; return; fi
if printf '%s' "$p" | grep -qiE 'docker|\.woodpecker|compose|traefik|deploy|helm|k8s|terraform'; then echo infra; return; fi if grep -qiE 'docker|\.woodpecker|compose|traefik|deploy|helm|k8s|terraform' <<<"$p"; then echo infra; return; fi
if printf '%s' "$p" | grep -qiE 'package\.json|tsconfig|turbo\.json|pnpm-|\.config\.|eslint|vite'; then echo build; return; fi if grep -qiE 'package\.json|tsconfig|turbo\.json|pnpm-|\.config\.|eslint|vite' <<<"$p"; then echo build; return; fi
if printf '%s' "$p" | grep -qE '\.tsx|\.css|components/|apps/web/'; then echo ui; return; fi if grep -qE '\.tsx|\.css|components/|apps/web/' <<<"$p"; then echo ui; return; fi
if printf '%s' "$p" | grep -qE '\.spec\.|\.test\.|__tests__/'; then echo test; return; fi if grep -qE '\.spec\.|\.test\.|__tests__/' <<<"$p"; then echo test; return; fi
if printf '%s' "$p" | grep -qE '\.md$|docs/'; then echo docs; return; fi if grep -qE '\.md$|docs/' <<<"$p"; then echo docs; return; fi
echo none echo none
} }
@@ -13,7 +13,12 @@ JSON_INPUT=$(cat)
if command -v jq &>/dev/null; then if command -v jq &>/dev/null; then
FILE_PATH=$(echo "$JSON_INPUT" | jq -r '.tool_input.file_path // .tool_response.filePath // .file_path // empty' 2>/dev/null || echo "") FILE_PATH=$(echo "$JSON_INPUT" | jq -r '.tool_input.file_path // .tool_response.filePath // .file_path // empty' 2>/dev/null || echo "")
else else
FILE_PATH=$(echo "$JSON_INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/' | head -1) file_path_pattern='"file_path"[[:space:]]*:[[:space:]]*"([^"]*)"'
if [[ "$JSON_INPUT" =~ $file_path_pattern ]]; then
FILE_PATH="${BASH_REMATCH[1]}"
else
FILE_PATH=""
fi
fi fi
# Only check TypeScript files # Only check TypeScript files
@@ -53,7 +58,7 @@ OUTPUT=$(npx tsc --noEmit --pretty --maxNodeModuleJsDepth 0 2>&1) || STATUS=$?
if [ "${STATUS:-0}" -ne 0 ]; then if [ "${STATUS:-0}" -ne 0 ]; then
# Filter output to only show errors related to the edited file (if possible) # Filter output to only show errors related to the edited file (if possible)
BASENAME=$(basename "$FILE_PATH") BASENAME=$(basename "$FILE_PATH")
RELEVANT=$(echo "$OUTPUT" | grep -A2 "$BASENAME" 2>/dev/null || echo "$OUTPUT" | head -20) RELEVANT=$(grep -A2 "$BASENAME" <<<"$OUTPUT" 2>/dev/null || sed -n '1,20p' <<<"$OUTPUT")
echo "TypeScript type errors detected after editing $FILE_PATH:" echo "TypeScript type errors detected after editing $FILE_PATH:"
echo "$RELEVANT" echo "$RELEVANT"
@@ -176,8 +176,12 @@ run_snap() {
# Resolve the single pre-update-* snapshot dir under a state dir (newest if many). # Resolve the single pre-update-* snapshot dir under a state dir (newest if many).
snap_dir() { snap_dir() {
find "$1/mosaic/backups" -maxdepth 1 -type d -name 'pre-update-*' 2>/dev/null \ local -a snapshots=()
| LC_ALL=C sort -r | head -1 mapfile -t snapshots < <(
find "$1/mosaic/backups" -maxdepth 1 -type d -name 'pre-update-*' 2>/dev/null \
| LC_ALL=C sort -r
)
printf '%s\n' "${snapshots[0]:-}"
} }
echo "── Part 1/2/3: durable snapshot scope, perms, no-leak ──────────────────" echo "── Part 1/2/3: durable snapshot scope, perms, no-leak ──────────────────"
@@ -336,7 +336,7 @@ chk "[reset-fail] the manual-recovery pointer is emitted (not a silent set -e ex
"grep -q 'Snapshot restore could not reset' '$OUTG'" "grep -q 'Snapshot restore could not reset' '$OUTG'"
chk "[reset-fail] the recovery message points at a preserved snapshot dir" \ chk "[reset-fail] the recovery message points at a preserved snapshot dir" \
"grep -q 'preserved at: .*mosaic-snapshot' '$OUTG'" "grep -q 'preserved at: .*mosaic-snapshot' '$OUTG'"
SNAP_E="$(grep -o '/[^ ]*mosaic-snapshot[^ ]*' "$OUTG" | head -1)" SNAP_E="$(grep -m1 -o '/[^ ]*mosaic-snapshot[^ ]*' "$OUTG")"
chk "[reset-fail] the named snapshot directory actually survives for recovery" \ chk "[reset-fail] the named snapshot directory actually survives for recovery" \
"[ -n '$SNAP_E' ] && [ -d '$SNAP_E' ]" "[ -n '$SNAP_E' ] && [ -d '$SNAP_E' ]"
chk "[reset-fail] operator secret value never appears in installer output" \ chk "[reset-fail] operator secret value never appears in installer output" \
@@ -353,7 +353,8 @@ chk "[control] without the D2 recovery line the operator gets no snapshot pointe
"! grep -q 'Snapshot restore could not reset' '$OUTH'" "! grep -q 'Snapshot restore could not reset' '$OUTH'"
[ -n "${SNAP_E:-}" ] && rm -rf "$SNAP_E" [ -n "${SNAP_E:-}" ] && rm -rf "$SNAP_E"
# Reap any snapshot the reset-fail runs left in /tmp (reset failed → never cleaned). # Reap any snapshot the reset-fail runs left in /tmp (reset failed → never cleaned).
grep -o '/[^ ]*mosaic-snapshot[^ ]*' "$OUTH" 2>/dev/null | head -1 | while read -r s; do rm -rf "$s"; done orphan_snapshot="$(grep -m1 -o '/[^ ]*mosaic-snapshot[^ ]*' "$OUTH" 2>/dev/null || true)"
[ -n "$orphan_snapshot" ] && rm -rf "$orphan_snapshot"
# Cleanup (generated installer controls are also removed by the EXIT trap). # Cleanup (generated installer controls are also removed by the EXIT trap).
for d in "$HA" "$REFA" "$HB" "$REFB" "$HC" "$HD" "$HE" "$REFE" "$HF" "$REFF" "$HG" "$HH"; do rm -rf "$d"; done for d in "$HA" "$REFA" "$HB" "$REFB" "$HC" "$HD" "$HE" "$REFE" "$HF" "$REFF" "$HG" "$HH"; do rm -rf "$d"; done
@@ -110,7 +110,7 @@ for attempt in $(seq 1 $((RETRIES + 1))); do
sleep 1.2 sleep 1.2
pane=$("${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null) pane=$("${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null)
if printf '%s' "$pane" | grep -qF "$QUEUED_RE"; then if grep -qF "$QUEUED_RE" <<<"$pane"; then
status="queued"; break status="queued"; break
fi fi
# Locate the REPL input box (prompt glyph). If we cannot see it, we have NO # Locate the REPL input box (prompt glyph). If we cannot see it, we have NO
@@ -121,7 +121,7 @@ for attempt in $(seq 1 $((RETRIES + 1))); do
fi fi
# Input box located AND still carrying our tail => unsubmitted draft. Flush + retry. # Input box located AND still carrying our tail => unsubmitted draft. Flush + retry.
# (Submitted messages scroll up into history; a draft stays on the line.) # (Submitted messages scroll up into history; a draft stays on the line.)
if [ -n "$snippet" ] && printf '%s' "$promptline" | grep -qF "$snippet"; then if [ -n "$snippet" ] && grep -qF "$snippet" <<<"$promptline"; then
status="draft"; continue status="draft"; continue
fi fi
# Input box located AND clear of our tail => positively submitted. This is the # Input box located AND clear of our tail => positively submitted. This is the
@@ -34,16 +34,20 @@ tmux new-session -d -s "$DEFAULT_TARGET" -c "$TMPDIR" 'PS1=" " exec bash --no
"$SEND_MESSAGE" -L "$SOCKET" -t "=$TARGET" -m "named socket hello" >/tmp/send-message-named.out "$SEND_MESSAGE" -L "$SOCKET" -t "=$TARGET" -m "named socket hello" >/tmp/send-message-named.out
sleep 0.2 sleep 0.2
capture_named | grep -qF "named socket hello" || fail "send-message.sh did not deliver to named socket" named_pane="$(capture_named)" || fail "could not capture named socket pane"
if capture_default | grep -qF "named socket hello"; then grep -qF "named socket hello" <<<"$named_pane" || fail "send-message.sh did not deliver to named socket"
default_pane="$(capture_default)" || fail "could not capture default socket pane"
if grep -qF "named socket hello" <<<"$default_pane"; then
fail "send-message.sh leaked named-socket message to default tmux server" fail "send-message.sh leaked named-socket message to default tmux server"
fi fi
"$AGENT_SEND" -L "$SOCKET" -S "tester:source" -s "=$TARGET" -m "agent socket hello" >/tmp/agent-send-named.out "$AGENT_SEND" -L "$SOCKET" -S "tester:source" -s "=$TARGET" -m "agent socket hello" >/tmp/agent-send-named.out
sleep 0.2 sleep 0.2
capture_named | grep -qF "[tester:source ->" || fail "agent-send.sh did not include preamble" named_pane="$(capture_named)" || fail "could not capture named socket pane"
capture_named | grep -qF "agent socket hello" || fail "agent-send.sh did not deliver to named socket" grep -qF "[tester:source ->" <<<"$named_pane" || fail "agent-send.sh did not include preamble"
if capture_default | grep -qF "agent socket hello"; then grep -qF "agent socket hello" <<<"$named_pane" || fail "agent-send.sh did not deliver to named socket"
default_pane="$(capture_default)" || fail "could not capture default socket pane"
if grep -qF "agent socket hello" <<<"$default_pane"; then
fail "agent-send.sh leaked named-socket message to default tmux server" fail "agent-send.sh leaked named-socket message to default tmux server"
fi fi
@@ -65,11 +69,11 @@ done
sleep 0.2 sleep 0.2
for i in $(seq 1 "$CONC_N"); do for i in $(seq 1 "$CONC_N"); do
pane=$(tmux -L "$SOCKET" capture-pane -t "=conc-$i:0.0" -p) pane=$(tmux -L "$SOCKET" capture-pane -t "=conc-$i:0.0" -p)
printf '%s' "$pane" | grep -qF "CONCPAYLOAD-${i}-END" \ grep -qF "CONCPAYLOAD-${i}-END" <<<"$pane" \
|| fail "concurrent send dropped payload for pane conc-$i" || fail "concurrent send dropped payload for pane conc-$i"
for j in $(seq 1 "$CONC_N"); do for j in $(seq 1 "$CONC_N"); do
[ "$j" = "$i" ] && continue [ "$j" = "$i" ] && continue
if printf '%s' "$pane" | grep -qF "CONCPAYLOAD-${j}-END"; then if grep -qF "CONCPAYLOAD-${j}-END" <<<"$pane"; then
fail "concurrent send cross-delivered payload $j to pane conc-$i" fail "concurrent send cross-delivered payload $j to pane conc-$i"
fi fi
done done
@@ -31,7 +31,7 @@ tmux -L "$SOCKET" new-session -d -s repl -c "$TMP" \
'PS1=" " exec bash --noprofile --norc -i' 'PS1=" " exec bash --noprofile --norc -i'
sleep 0.3 sleep 0.3
out=$("$SEND" -L "$SOCKET" -t "=repl" -m "verdict fixture one delivered ok" 2>"$TMP/e1"); rc=$? out=$("$SEND" -L "$SOCKET" -t "=repl" -m "verdict fixture one delivered ok" 2>"$TMP/e1"); rc=$?
if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -qF "✓ delivered"; then if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: -prompt REPL that submits => exit 0 ✓ delivered" ok "delivered: -prompt REPL that submits => exit 0 ✓ delivered"
else else
no "delivered: -prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]" no "delivered: -prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]"
@@ -123,7 +123,7 @@ _manifest_val() {
# _manifest_val KEY — echo VALUE for KEY=VALUE in the manifest (blank if none). # _manifest_val KEY — echo VALUE for KEY=VALUE in the manifest (blank if none).
local key="$1" local key="$1"
[ -f "$MANIFEST" ] || return 0 [ -f "$MANIFEST" ] || return 0
sed -n "s/^${key}=//p" "$MANIFEST" | head -n1 | tr -d '[:space:]' awk -v key="$key" 'index($0, key "=") == 1 { sub(/^[^=]*=/, ""); gsub(/[[:space:]]/, ""); print; exit }' "$MANIFEST"
} }
# _load_watchlist — validate the watch-list path + JSON + schema_version range. # _load_watchlist — validate the watch-list path + JSON + schema_version range.
@@ -267,7 +267,7 @@ _poll_source() {
if snap_json="$(jq -ce '.' <<<"$rawmeta" 2>/dev/null)"; then if snap_json="$(jq -ce '.' <<<"$rawmeta" 2>/dev/null)"; then
snap_sha="$(jq -r 'if (.snapshot_sha|type) == "string" then .snapshot_sha else "" end' <<<"$snap_json")" snap_sha="$(jq -r 'if (.snapshot_sha|type) == "string" then .snapshot_sha else "" end' <<<"$snap_json")"
snap_ts="$(jq -r 'if (.snapshot_ts|type) == "number" then (.snapshot_ts|floor|tostring) else "" end' <<<"$snap_json")" snap_ts="$(jq -r 'if (.snapshot_ts|type) == "number" then (.snapshot_ts|floor|tostring) else "" end' <<<"$snap_json")"
if [ -n "$snap_sha" ] && ! printf '%s' "$snap_sha" | grep -Eq '^[0-9a-f]{7,64}$'; then if [ -n "$snap_sha" ] && ! grep -Eq '^[0-9a-f]{7,64}$' <<<"$snap_sha"; then
echo "detector.sh: source '$kind/$id' snapshot_sha rejected (not a 7-64 char lowercase-hex git sha) — snapshot metadata DROPPED, poll continues (#940)." >&2 echo "detector.sh: source '$kind/$id' snapshot_sha rejected (not a 7-64 char lowercase-hex git sha) — snapshot metadata DROPPED, poll continues (#940)." >&2
snap_sha="" snap_sha=""
snap_ts="" snap_ts=""
@@ -275,7 +275,7 @@ _poll_source() {
# A ts must be a sane positive epoch BEFORE any arithmetic touches it: a # A ts must be a sane positive epoch BEFORE any arithmetic touches it: a
# negative or absurdly large value would make the shell integer comparison # negative or absurdly large value would make the shell integer comparison
# below error out and silently KEEP the bad ts — validate first, compare after. # below error out and silently KEEP the bad ts — validate first, compare after.
if [ -n "$snap_ts" ] && ! printf '%s' "$snap_ts" | grep -Eq '^[0-9]{1,12}$'; then if [ -n "$snap_ts" ] && ! grep -Eq '^[0-9]{1,12}$' <<<"$snap_ts"; then
echo "detector.sh: source '$kind/$id' snapshot_ts rejected (not a sane positive epoch) — snapshot_ts DROPPED, poll continues (#940)." >&2 echo "detector.sh: source '$kind/$id' snapshot_ts rejected (not a sane positive epoch) — snapshot_ts DROPPED, poll continues (#940)." >&2
snap_ts="" snap_ts=""
fi fi
@@ -644,7 +644,8 @@ cmd_render() {
oseq="$(jq -r '.observed_seq // "?"' <<<"$line")" oseq="$(jq -r '.observed_seq // "?"' <<<"$line")"
oclass="$(jq -r '.class // "actionable"' <<<"$line")" oclass="$(jq -r '.class // "actionable"' <<<"$line")"
oloc="$(jq -c '.locators // {}' <<<"$line")" oloc="$(jq -c '.locators // {}' <<<"$line")"
olabel="$(_locator_line "$oloc" | head -n1)" olabel="$(_locator_line "$oloc")"
olabel="${olabel%%$'\n'*}"
printf ' * seq %s [%s] %s\n' "$oseq" "$(_scrub_inline "$oclass")" "$olabel" printf ' * seq %s [%s] %s\n' "$oseq" "$(_scrub_inline "$oclass")" "$olabel"
done <<<"$pending" done <<<"$pending"
fi fi
@@ -146,7 +146,7 @@ EOF
_manifest_val() { _manifest_val() {
local key="$1" local key="$1"
[ -f "$MANIFEST" ] || return 0 [ -f "$MANIFEST" ] || return 0
sed -n "s/^${key}=//p" "$MANIFEST" | head -n1 | tr -d '[:space:]' awk -v key="$key" 'index($0, key "=") == 1 { sub(/^[^=]*=/, ""); gsub(/[[:space:]]/, ""); print; exit }' "$MANIFEST"
} }
# _load_watchlist — validate path + JSON + shape + Gate B schema range (mirrors # _load_watchlist — validate path + JSON + shape + Gate B schema range (mirrors
+3 -2
View File
@@ -72,8 +72,9 @@ elif [[ -n "$DATA_DIR" ]]; then
while IFS= read -r file; do while IFS= read -r file; do
[[ -z "$file" ]] && continue [[ -z "$file" ]] && continue
done_total=$((done_total + 1)) done_total=$((done_total + 1))
if git -C "$DATA_DIR" log --since="${WINDOW_DAYS} days ago" --pretty='%s' -- "$file" 2>/dev/null \ history_rc=0
| grep -qiE 'reopen|revert|fix|regression|wrong|incorrect|redo'; then history="$(git -C "$DATA_DIR" log --since="${WINDOW_DAYS} days ago" --pretty='%s' -- "$file" 2>/dev/null)" || history_rc=$?
if [[ "$history_rc" -eq 0 ]] && grep -qiE 'reopen|revert|fix|regression|wrong|incorrect|redo' <<<"$history"; then
detectable=$((detectable + 1)) detectable=$((detectable + 1))
fi fi
done < <(find "$DATA_DIR" -type f -name '*.json' 2>/dev/null) done < <(find "$DATA_DIR" -type f -name '*.json' 2>/dev/null)
+2 -2
View File
@@ -64,9 +64,9 @@ for line in "${LINES[@]}"; do
# - build/test/lint/type/ci signals → CI would have caught it # - build/test/lint/type/ci signals → CI would have caught it
# - security/auth/permission/data/migration → human review would flag it # - security/auth/permission/data/migration → human review would flag it
# - everything else (logic/UX/assumption/edge) → only-self-reflection bucket # - everything else (logic/UX/assumption/edge) → only-self-reflection bucket
if printf '%s' "$subj" | grep -qiE 'test|lint|type|build|ci|compile|typo'; then if grep -qiE 'test|lint|type|build|ci|compile|typo' <<<"$subj"; then
ci=$((ci + 1)) ci=$((ci + 1))
elif printf '%s' "$subj" | grep -qiE 'security|auth|permission|rbac|secret|migration|data|sql|injection'; then elif grep -qiE 'security|auth|permission|rbac|secret|migration|data|sql|injection' <<<"$subj"; then
human=$((human + 1)) human=$((human + 1))
else else
selfonly=$((selfonly + 1)) selfonly=$((selfonly + 1))
@@ -0,0 +1,28 @@
[
"tools/matrix-presence-harness/run.sh:TSX_CLI=\"$(ls -d \"${REPO}\"/node_modules/.pnpm/tsx@*/node_modules/tsx/dist/cli.mjs 2>/dev/null | head -1)\"",
"tools/e2e-install-test.sh:if ! mosaic gateway --help 2>&1 | grep -q 'verify'; then",
"tools/install.sh:EXTRACTED_DIR=\"$(find \"$WORK_DIR\" -maxdepth 1 -mindepth 1 -type d | head -1)\"",
"scripts/analysis/reflect-board-history.sh:if git -C \"$DATA_DIR\" log --since=\"${WINDOW_DAYS} days ago\" --pretty='%s' -- \"$file\" 2>/dev/null | grep -qiE 'reopen|revert|fix|regression|wrong|incorrect|redo'; then",
"scripts/analysis/reflect-git-history.sh:if printf '%s' \"$subj\" | grep -qiE 'test|lint|type|build|ci|compile|typo'; then",
"scripts/analysis/reflect-git-history.sh:elif printf '%s' \"$subj\" | grep -qiE 'security|auth|permission|rbac|secret|migration|data|sql|injection'; then",
"packages/mosaic/framework/tools/authentik/user-create.sh:group_pk=$(echo \"$group_response\" | jq -r \".results[] | select(.name == \\\"$GROUP\\\") | .pk\" | head -1)",
"packages/mosaic/framework/tools/git/mutate-push-guard.sh:PROSE_LO=\"$(grep -n '^usage() {' \"$BAK\" | head -1 | cut -d: -f1)\"",
"packages/mosaic/framework/tools/orchestrator/session-resume.sh:echo \"$dirty_files\" | head -20 | while IFS= read -r line; do",
"packages/mosaic/framework/tools/prdy/prdy-status.sh:if echo \"$PRD_CONTENT\" | grep -qiE \"$pattern\"; then",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'auth|login|session|token|permission|rbac|credential|secret'; then echo auth; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'migration|prisma|schema|\\.sql|entity|repository|seed'; then echo data; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'docker|\\.woodpecker|compose|traefik|deploy|helm|k8s|terraform'; then echo infra; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qiE 'package\\.json|tsconfig|turbo\\.json|pnpm-|\\.config\\.|eslint|vite'; then echo build; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qE '\\.tsx|\\.css|components/|apps/web/'; then echo ui; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qE '\\.spec\\.|\\.test\\.|__tests__/'; then echo test; return; fi",
"packages/mosaic/framework/tools/qa/reflect-stop-hook.sh:if printf '%s' \"$p\" | grep -qE '\\.md$|docs/'; then echo docs; return; fi",
"packages/mosaic/framework/tools/qa/typecheck-hook.sh:FILE_PATH=$(echo \"$JSON_INPUT\" | grep -o '\"file_path\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' | sed 's/.*\"\\([^\"]*\\)\"$/\\1/' | head -1)",
"packages/mosaic/framework/tools/qa/typecheck-hook.sh:RELEVANT=$(echo \"$OUTPUT\" | grep -A2 \"$BASENAME\" 2>/dev/null || echo \"$OUTPUT\" | head -20)",
"packages/mosaic/framework/tools/tmux/send-message.sh:if printf '%s' \"$pane\" | grep -qF \"$QUEUED_RE\"; then",
"packages/mosaic/framework/tools/tmux/send-message.sh:if [ -n \"$snippet\" ] && printf '%s' \"$promptline\" | grep -qF \"$snippet\"; then",
"packages/mosaic/framework/tools/wake/detector.sh:sed -n \"s/^${key}=//p\" \"$MANIFEST\" | head -n1 | tr -d '[:space:]'",
"packages/mosaic/framework/tools/wake/detector.sh:if [ -n \"$snap_sha\" ] && ! printf '%s' \"$snap_sha\" | grep -Eq '^[0-9a-f]{7,64}$'; then",
"packages/mosaic/framework/tools/wake/detector.sh:if [ -n \"$snap_ts\" ] && ! printf '%s' \"$snap_ts\" | grep -Eq '^[0-9]{1,12}$'; then",
"packages/mosaic/framework/tools/wake/digest.sh:olabel=\"$(_locator_line \"$oloc\" | head -n1)\"",
"packages/mosaic/framework/tools/wake/reconcile.sh:sed -n \"s/^${key}=//p\" \"$MANIFEST\" | head -n1 | tr -d '[:space:]'"
]
@@ -0,0 +1,24 @@
[
"packages/mosaic/framework/systemd/user/test-fleet-units.sh:if tmux -L \"$TEST_SOCKET\" show-environment -g LD_PRELOAD 2>/dev/null | grep -q '^LD_PRELOAD='; then",
"packages/mosaic/framework/tools/git/test-issue-comment-readback.sh:write_response \"$(printf '%s' \"$result\" | head -n1)\" \"$(printf '%s' \"$result\" | tail -n +2)\"",
"packages/mosaic/framework/tools/git/test-issue-comment-readback.sh:write_response \"$(printf '%s' \"$result\" | head -n1)\" \"$(printf '%s' \"$result\" | tail -n +2)\"",
"packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh:contains() { printf '%s\\n' \"$1\" | grep -qx \"$2\"; }",
"packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh:write_response \"$(printf '%s' \"$result\" | head -n1)\" \"$(printf '%s' \"$result\" | tail -n +2)\"",
"packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh:echo \"$HELP_TEXT\" | grep -q -- '-r, --repo'",
"packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh:echo \"$HELP_TEXT\" | grep -q -- '-H, --host'",
"packages/mosaic/framework/tools/orchestrator/smoke-test.sh:if [[ \"$(printf '%s\\n' \"$codex_run_prompt\" | head -n1)\" == \"Now initiating Orchestrator mode...\" ]]; then pass_case \"codex run prompt first line is mode declaration\"; else fail_case \"codex run prompt first line is mode declaration\"; fi",
"packages/mosaic/framework/tools/orchestrator/smoke-test.sh:if [[ \"$(printf '%s\\n' \"$claude_run_prompt\" | head -n1)\" == \"## Continuation Mission\" ]]; then pass_case \"claude run prompt remains continuation prompt format\"; else fail_case \"claude run prompt remains continuation prompt format\"; fi",
"packages/mosaic/framework/tools/orchestrator/test-board-roll.sh:echo \"$out\" | grep -qi \"dry run\" || note \"dry-run did not announce itself\"",
"packages/mosaic/framework/tools/orchestrator/test-board-roll.sh:echo \"$out\" | grep -q \"would roll\" || note \"dry-run did not report a plan\"",
"packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh:find \"$1/mosaic/backups\" -maxdepth 1 -type d -name 'pre-update-*' 2>/dev/null | LC_ALL=C sort -r | head -1",
"packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh:SNAP_E=\"$(grep -o '/[^ ]*mosaic-snapshot[^ ]*' \"$OUTG\" | head -1)\"",
"packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh:grep -o '/[^ ]*mosaic-snapshot[^ ]*' \"$OUTH\" 2>/dev/null | head -1 | while read -r s; do rm -rf \"$s\"; done",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:capture_named | grep -qF \"named socket hello\" || fail \"send-message.sh did not deliver to named socket\"",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:if capture_default | grep -qF \"named socket hello\"; then",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:capture_named | grep -qF \"[tester:source ->\" || fail \"agent-send.sh did not include preamble\"",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:capture_named | grep -qF \"agent socket hello\" || fail \"agent-send.sh did not deliver to named socket\"",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:if capture_default | grep -qF \"agent socket hello\"; then",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:printf '%s' \"$pane\" | grep -qF \"CONCPAYLOAD-${i}-END\" || fail \"concurrent send dropped payload for pane conc-$i\"",
"packages/mosaic/framework/tools/tmux/test-send-message-socket.sh:if printf '%s' \"$pane\" | grep -qF \"CONCPAYLOAD-${j}-END\"; then",
"packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh:if [ \"$rc\" -eq 0 ] && printf '%s' \"$out\" | grep -qF \"✓ delivered\"; then"
]
+160
View File
@@ -0,0 +1,160 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
const ROOT = new URL('../', import.meta.url);
const EXPECTED_BASELINE_SITES = 26;
const EXPECTED_TEST_BASELINE_SITES = 22;
const TARGETS = [
'tools/matrix-presence-harness/run.sh',
'tools/e2e-install-test.sh',
'tools/install.sh',
'scripts/agent/session-start.sh',
'scripts/analysis/reflect-board-history.sh',
'scripts/analysis/reflect-git-history.sh',
'packages/mosaic/framework/templates/repo/scripts/agent/session-start.sh',
'packages/mosaic/framework/tools/authentik/user-create.sh',
'packages/mosaic/framework/tools/git/mutate-push-guard.sh',
'packages/mosaic/framework/tools/orchestrator/session-resume.sh',
'packages/mosaic/framework/tools/prdy/prdy-status.sh',
'packages/mosaic/framework/tools/qa/reflect-stop-hook.sh',
'packages/mosaic/framework/tools/qa/typecheck-hook.sh',
'packages/mosaic/framework/tools/tmux/send-message.sh',
'packages/mosaic/framework/tools/wake/detector.sh',
'packages/mosaic/framework/tools/wake/digest.sh',
'packages/mosaic/framework/tools/wake/reconcile.sh',
'packages/mosaic/framework/systemd/user/test-fleet-units.sh',
'packages/mosaic/framework/tools/git/test-issue-comment-readback.sh',
'packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh',
'packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh',
'packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh',
'packages/mosaic/framework/tools/orchestrator/smoke-test.sh',
'packages/mosaic/framework/tools/orchestrator/test-board-roll.sh',
'packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh',
'packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh',
'packages/mosaic/framework/tools/tmux/test-send-message-socket.sh',
'packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh',
];
// These statuses are explicitly non-load-bearing or unreachable at designed input.
// They remain inventoried until the final #1099 tranche records every verdict.
const ACCEPTED = [
['tools/install.sh', 'mosaic-bak-', '|| true'],
['tools/install.sh', 'mosaicstack-mosaic-*.tgz', 'head -1'],
['tools/install.sh', 'mosaicstack-gateway-*.tgz', 'head -1'],
['scripts/agent/session-start.sh', 'docs/scratchpads/*.md', '|| true'],
[
'packages/mosaic/framework/templates/repo/scripts/agent/session-start.sh',
'docs/scratchpads/*.md',
'|| true',
],
];
const earlyExit =
/(?<!\|)\|(?!\|)[^;\n]*(?:grep\b[^;\n]*(?:-[A-Za-z]*q|--quiet|-m\s*1)|head\b(?:\s|$))/;
function scan(sources) {
const found = [];
for (const [file, rawSource] of sources) {
const source = rawSource.replace(/\\\n\s*/g, ' ');
for (const rawLine of source.split('\n')) {
const line = rawLine.trim();
if (!earlyExit.test(line)) continue;
const accepted = ACCEPTED.some(
([acceptedFile, ...fragments]) =>
acceptedFile === file && fragments.every((item) => line.includes(item)),
);
if (!accepted) found.push(`${file}:${line}`);
}
}
return found;
}
async function currentSources() {
return Promise.all(
TARGETS.map(async (file) => [file, await readFile(new URL(file, ROOT), 'utf8')]),
);
}
async function assertBaselineFixture(file, expectedCount, expectedUnique = expectedCount) {
const baseline = JSON.parse(await readFile(new URL(file, ROOT), 'utf8'));
assert.equal(baseline.length, expectedCount);
assert.equal(new Set(baseline).size, expectedUnique);
const fixtureSources = baseline.map((site) => {
const separator = site.indexOf(':');
assert.ok(separator > 0, `invalid baseline site: ${site}`);
return [site.slice(0, separator), site.slice(separator + 1)];
});
assert.deepEqual(scan(fixtureSources), baseline);
}
test('the registered runtime baseline denominator is exactly 26 unsafe sites', async () => {
await assertBaselineFixture(
'scripts/fixtures/pipefail-early-exit-baseline.json',
EXPECTED_BASELINE_SITES,
);
});
test('the registered test baseline denominator is exactly 22 unsafe sites', async () => {
await assertBaselineFixture(
'scripts/fixtures/pipefail-early-exit-test-baseline.json',
EXPECTED_TEST_BASELINE_SITES,
21,
);
});
test('load-bearing pipefail paths do not pipe into early-exiting consumers', async () => {
assert.deepEqual(scan(await currentSources()), []);
});
test('gateway verify capability preserves the complete help-probe truth table', async () => {
const directory = await mkdtemp(path.join(tmpdir(), 'gateway-help-probe-'));
const mosaic = path.join(directory, 'mosaic');
const probe = new URL('tools/e2e-gateway-verify-supported.sh', ROOT).pathname;
try {
await writeFile(
mosaic,
'#!/usr/bin/env bash\nprintf \'%s\\n\' "${MOCK_HELP_OUTPUT:-}"\nexit "${MOCK_HELP_RC:-0}"\n',
);
await chmod(mosaic, 0o755);
const run = (rc, output) =>
spawnSync('bash', [probe], {
env: {
...process.env,
PATH: `${directory}:${process.env.PATH}`,
MOCK_HELP_RC: String(rc),
MOCK_HELP_OUTPUT: output,
},
}).status;
assert.equal(run(0, 'commands: verify'), 0);
assert.equal(run(0, 'commands: install'), 1);
assert.equal(run(1, 'commands: verify'), 1);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test('board-history preserves non-git data-dir as a non-detectable result', async () => {
const directory = await mkdtemp(path.join(tmpdir(), 'reflect-board-non-git-'));
try {
await writeFile(path.join(directory, 'task.json'), '{}\n');
const result = spawnSync(
'bash',
[
new URL('scripts/analysis/reflect-board-history.sh', ROOT).pathname,
'--data-dir',
directory,
],
{ encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /"done_tasks": 1/);
assert.match(result.stdout, /"detectable_outcomes": 0/);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Exit 0 only when the capability probe itself succeeds and advertises verify.
# A failed help command and a successful response without verify are both
# unsupported, matching the historical e2e-install-test.sh conditional.
set -uo pipefail
gateway_help_rc=0
gateway_help="$(mosaic gateway --help 2>&1)" || gateway_help_rc=$?
[[ "$gateway_help_rc" -eq 0 ]] || exit 1
grep -q 'verify' <<<"$gateway_help"
+1 -1
View File
@@ -136,7 +136,7 @@ fi
echo "=== [inner] Running mosaic gateway verify ===" echo "=== [inner] Running mosaic gateway verify ==="
# `gateway verify` was added in feat/mosaic-first-run-ux. # `gateway verify` was added in feat/mosaic-first-run-ux.
# If the installed version pre-dates this, skip gracefully. # If the installed version pre-dates this, skip gracefully.
if ! mosaic gateway --help 2>&1 | grep -q 'verify'; then if ! bash /repo/tools/e2e-gateway-verify-supported.sh; then
echo "[SKIP] 'mosaic gateway verify' not available in installed version ${INSTALLED_VERSION}." echo "[SKIP] 'mosaic gateway verify' not available in installed version ${INSTALLED_VERSION}."
echo "[SKIP] This command was added in the feat/mosaic-first-run-ux release." echo "[SKIP] This command was added in the feat/mosaic-first-run-ux release."
echo "[SKIP] Re-run after the new version is published to validate this step." echo "[SKIP] Re-run after the new version is published to validate this step."
+8 -4
View File
@@ -308,13 +308,17 @@ ensure_monorepo() {
exit 1 exit 1
fi fi
# Gitea archives extract to <repo-name>/ inside the work dir # Gitea archives extract to exactly one <repo-name>/ inside the work dir.
EXTRACTED_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)" # Read the complete population so a malformed multi-root archive reaches the
if [[ -z "$EXTRACTED_DIR" ]] || [[ ! -d "$EXTRACTED_DIR" ]]; then # named diagnostic instead of aborting on an upstream SIGPIPE under pipefail.
fail "Could not locate extracted source in archive." local -a extracted_dirs=()
mapfile -d '' -t extracted_dirs < <(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d -print0)
if [[ "${#extracted_dirs[@]}" -ne 1 ]] || [[ ! -d "${extracted_dirs[0]:-}" ]]; then
fail "Could not locate exactly one extracted source directory in archive."
ls -la "$WORK_DIR" >&2 ls -la "$WORK_DIR" >&2
exit 1 exit 1
fi fi
EXTRACTED_DIR="${extracted_dirs[0]}"
} }
# Build @mosaicstack/mosaic + @mosaicstack/gateway from source and install both # Build @mosaicstack/mosaic + @mosaicstack/gateway from source and install both
+4 -1
View File
@@ -35,7 +35,10 @@ export DARK_THRESHOLD_MS="${DARK_THRESHOLD_MS:-6000}"
export AGENT_SLUGS="${AGENT_SLUGS:-alpha,bravo,charlie}" export AGENT_SLUGS="${AGENT_SLUGS:-alpha,bravo,charlie}"
export VICTIM_SLUG="${VICTIM_SLUG:-charlie}" export VICTIM_SLUG="${VICTIM_SLUG:-charlie}"
TSX_CLI="$(ls -d "${REPO}"/node_modules/.pnpm/tsx@*/node_modules/tsx/dist/cli.mjs 2>/dev/null | head -1)" shopt -s nullglob
TSX_CANDIDATES=("${REPO}"/node_modules/.pnpm/tsx@*/node_modules/tsx/dist/cli.mjs)
shopt -u nullglob
TSX_CLI="${TSX_CANDIDATES[0]:-}"
if [[ -z "${TSX_CLI}" ]]; then if [[ -z "${TSX_CLI}" ]]; then
echo "run.sh: tsx not found under node_modules — run pnpm install first" >&2 echo "run.sh: tsx not found under node_modules — run pnpm install first" >&2
exit 1 exit 1