Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8764f8664e | ||
|
|
b820d6f3d6 | ||
|
|
be6da9d028 | ||
|
|
f4e3ef4ac2 | ||
|
|
b7c7357a2f | ||
|
|
f0b38f3fee | ||
|
|
6362baf7cc | ||
|
|
898e24472a | ||
|
|
605c09089c | ||
|
|
b0f7d26dd9 | ||
|
|
3a1203b2f8 | ||
|
|
df4c591ab4 |
@@ -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 F6–F10 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.55–2.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).
|
||||||
|
|
||||||
|
### F1–F12 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 F1–F10 (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.1–5.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 4–5 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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user