Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7081b58a4a | ||
|
|
717ebd1fb2 | ||
|
|
9032b05703 | ||
|
|
b79336a8c1 |
@@ -4,6 +4,14 @@ pnpm-lock.yaml
|
||||
**/node_modules
|
||||
**/drizzle
|
||||
**/.next
|
||||
# Python build/test artifacts — same category as node_modules/dist/.next above.
|
||||
# Prettier must never scan generated trees; without these a local venv poisons
|
||||
# `pnpm format:check` with thousands of third-party files.
|
||||
**/venv
|
||||
**/__pycache__
|
||||
**/.mypy_cache
|
||||
**/.pytest_cache
|
||||
**/htmlcov
|
||||
.claude/
|
||||
docs/tess/TASKS.md
|
||||
docs/scratchpads/
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# mos-remediation — LIVE BOARD (keep < 8 KB)
|
||||
|
||||
**Phase:** PLANNING — adversarial task decomposition IN FLIGHT.
|
||||
**Updated:** 2026-07-31 (mos-remediation orchestrator; seat active on `mosaic-fleet`).
|
||||
|
||||
## Head
|
||||
|
||||
- Mission charter + 15 decisions + 4-build plan: PERSISTED (`docs/remediation/MISSION.md`).
|
||||
- HOLD lifted for this workstream (Jason 2026-07-31). Nothing implemented yet — planning first.
|
||||
- Orchestrator seat `mos-remediation` is LIVE and owns the mission. Residency attestation: PASS.
|
||||
- TASK-0 (env + push mission package) IN PROGRESS — checkout deps repaired, gates being verified honestly.
|
||||
- TASK-1 (adversarial decomp) DISPATCHED for real, both planners on GUARANTEED-CLEAN context.
|
||||
|
||||
## In-flight
|
||||
|
||||
| Task | Owner | State |
|
||||
| ---------------------------------------------------- | --------------- | --------------------------------------------- |
|
||||
| TASK-0 env repair + push `remediation/mission-setup` | mos-remediation | IN PROGRESS — deps installed; gates verifying |
|
||||
| Decomp (robustness side) → `DECOMP-OPUS.md` | planner-opus | WORKING (clean session) |
|
||||
| Decomp (pragmatic side) → `DECOMP-SOL.md` | planner-sol | WORKING (reset to 0.0% ctx before dispatch) |
|
||||
| Reconcile both decomps → `docs/remediation/TASKS.md` | mos-remediation | BLOCKED on the two decomps |
|
||||
|
||||
## Fleet seats
|
||||
|
||||
- mos-remediation — project orchestrator (Claude, /src/mosaic-stack, socket `mosaic-fleet`) — ACTIVE
|
||||
- planner-opus — adversarial planner (robustness), Opus 5, socket `default` — WORKING
|
||||
- planner-sol — adversarial planner (pragmatic), gpt-5.6-sol, socket `default` — WORKING
|
||||
- rev-974 — mosaicstack reviewer identity (id 16, write:repository) — idle, on call
|
||||
- Mos (mos-claude) — lead coordinator, socket `default` — relay path to Jason
|
||||
|
||||
## Gate status
|
||||
|
||||
- Delivery gates active: author≠reviewer, diff-blind pre-registered checks, CI-green, merged-PR completion.
|
||||
- Freeze: LIFTED for this workstream only.
|
||||
- Git identity: `MOSAIC_GIT_IDENTITY=mos-dt-0` INTERIM. Mos ruled gate-16 HOLDS (author≠reviewer is what
|
||||
gate-16 protects; rev-974 reviews, mos-dt-0 never self-reviews). Dedicated identity TRACKED, Mos provisions.
|
||||
- Remote control: native `/remote-control` NOT wired in this runtime. Path is **Mos-relay**
|
||||
(Jason ↔ mos-claude via Discord ↔ mos-remediation via agent-send). Not a blocker.
|
||||
|
||||
## Sequencing (from MISSION.md)
|
||||
|
||||
1. Spine + choke-point service (MACP wiring @ mosaic_orchestrator.py::run_single_task) + PG/Redis
|
||||
2. Rotation daemon (finish Mission Control Plane, reuse packages/coord)
|
||||
3. Comms service (envelope→service→PG/Redis→adapters)
|
||||
4. Hygiene + conformance harness
|
||||
Cross-cutting retirements: flat-file tracking, 3 MACP islands, silent MOSAIC BYPASS.
|
||||
|
||||
## Dogfood evidence captured this session (live failure classes, not hypotheticals)
|
||||
|
||||
- **D-1 / P-ACTIVATION + hygiene — committed `.npmrc` hard-pins `store-dir=/root/.local/share/pnpm/store`.**
|
||||
Correct for the CI container (runs as root), fatal for EVERY non-root local checkout: `EACCES` on
|
||||
`/root/.local/share/pnpm/store/v10/server/server.json`. A committed config that only works on one
|
||||
runtime is exactly the activation-skew class. Fix candidate: make store-dir env-overridable, not hardcoded.
|
||||
- **D-2 / hygiene — husky `prepare` fails `EPERM` copying into root-owned `.husky/_/`.** Repo working
|
||||
tree has root-owned dirs (`.husky/`, repo root) under a non-root agent. Worked around with the
|
||||
intended `HUSKY=0` escape hatch (does NOT disable the existing pre-commit/pre-push hooks).
|
||||
- **D-3 / P-FLEET-001 — the seats running this mission are UNMANAGED.** `mos-remediation`, `rev-974`,
|
||||
`planner-opus`, `planner-sol` appear in NO roster (`~/.config/mosaic/fleet/roster.yaml`, `agents/`).
|
||||
Planners run on socket `default`; the roster declares `mosaic-fleet`. This is the exact
|
||||
"one roster-owned socket/host + quarantine unmanaged + stale GC" failure P-FLEET-001 indicts —
|
||||
observed on the remediation mission's own fleet. Prerequisite for INBOX identity-addressing.
|
||||
- **D-4 / P-LIFECYCLE + hygiene — a dispatched agent silently IGNORED an in-message context reset.**
|
||||
planner-sol was at 64.3%/372k; the brief asked it to reset first; it began work on dirty context anyway.
|
||||
Only an out-of-band `/new` driven by the orchestrator guaranteed clean state. Confirms the postmortem
|
||||
thesis: **instructions are not enforcement.** Reset must be a mechanical pre-dispatch step, not a request.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- 2026-07-31 — Mission set up by Mos post-postmortem (15/15 decided). Dogfood posture active.
|
||||
- 2026-07-31 — Mos: stale `.mosaic/orchestrator/mission.json` is RESIDUE of the disabled Python
|
||||
orchestrator rail that this plan RETIRES. Do NOT invest in it; do NOT build on that rail. The 0/0
|
||||
milestone banner is cosmetic. (Supersedes any plan to repair it.)
|
||||
- 2026-07-31 — Mos: planners must be dispatched with GUARANTEED clean context, not requested-clean.
|
||||
Prior default-socket planner sessions predate this mission; dirty context is the indicted hygiene.
|
||||
- 2026-07-31 — mos-remediation: worker briefs forbid all git ops and restrict each worker to a single
|
||||
named output file, so two planners can share one checkout without a branch race (M2-era incident doctrine).
|
||||
@@ -0,0 +1,44 @@
|
||||
# mos-remediation — Orchestrator Kickstart / Compaction-Survival Resume
|
||||
|
||||
**You are `mos-remediation`, the project orchestrator for the Mosaic Stack remediation, launched in `/src/mosaic-stack`.**
|
||||
This file is your fail-closed resume procedure. Read it on EVERY fresh/cleared session and on the FIRST turn
|
||||
after any compaction. This mission's whole point is that manual compaction-survival is fragile — so follow this
|
||||
mechanically until Build 3 (rotation) makes it automatic.
|
||||
|
||||
## On resume (do in order, before any orchestration action)
|
||||
|
||||
1. `cd /src/mosaic-stack` and confirm you are on the remediation working branch.
|
||||
2. Read `docs/remediation/MISSION.md` — the charter (goal, 4 builds, 15 decisions, sequencing, directives).
|
||||
3. Read `docs/remediation/BOARD.md` — the LIVE state: current phase, in-flight tasks, fleet seat assignments,
|
||||
gate status. This is your single source of in-flight truth (kept small).
|
||||
4. Read the discussion checkpoint for full rationale if needed:
|
||||
`../jarvis-brain/docs/scratchpads/postmortem/REMEDIATION-DISCUSSION-STATE.md` (or the jarvis-brain repo path).
|
||||
5. **Residency attestation (fail-closed):** restate from the reloaded files — (a) the goal in one line, (b) the
|
||||
current build/phase, (c) the BOARD head (in-flight tasks + who owns them). If you cannot, HALT and re-read.
|
||||
Do NOT act on memory alone; a compaction may have dropped context silently.
|
||||
|
||||
## Standing invariants (never violate)
|
||||
|
||||
- **North star:** deterministic-right-answer → code/gate; LLM only for judgment.
|
||||
- **Delivery gates:** author≠reviewer; PRE-REGISTERED diff-blind checks committed before reading the diff;
|
||||
CI terminal-green; completion = merged PR + closed issue. rev-974 = the mosaicstack reviewer identity.
|
||||
- **Dogfooding:** every fix validated against its live seed case (MISSION.md lists them).
|
||||
- **Tracking → DB** (hard cutover); do NOT re-invest in flat-file tracking. jarvis-brain PDA is off-limits.
|
||||
- **Git identity:** export `MOSAIC_GIT_IDENTITY=<your-seat>` so wrappers author correctly and survive respawn.
|
||||
|
||||
## After every significant event
|
||||
|
||||
Overwrite stale lines in `BOARD.md`, keep it < 8 KB, commit + push. The board IS your checkpoint until the
|
||||
DB-backed rotation daemon (Build 3) exists. Persist typed state (phase, tasks, owners, gates) — never the transcript.
|
||||
|
||||
## Fleet
|
||||
|
||||
- Adversarial planners: `planner-opus` (robustness), `planner-sol` (pragmatic) — dispatch for task decomposition; reconcile their oppositional decomps.
|
||||
- Coders/reviewers: dispatch per roster + delivery gates. Comms: `~/.config/mosaic/tools/tmux/agent-send.sh`
|
||||
(`-L <socket> -s <dst> -S <yourhost>:<yourseat> --class <class>`); always pass `-S`.
|
||||
- Lead coordinator: Mos (`mos-claude`). Escalate only on the Constitution's escalation triggers.
|
||||
|
||||
## Remote control
|
||||
|
||||
On first startup, activate remote control for this session (`/remote-control`) so Jason can reach/drive you while
|
||||
away. If the command is unavailable in this runtime, report it to Mos and continue — it is not a blocker.
|
||||
@@ -0,0 +1,98 @@
|
||||
# MACP wiring investigation
|
||||
|
||||
**Scope:** `/src/mosaic-stack` inspected at HEAD `b79336a8c11e2a4646a47ff8d295a226e0c71404`; read-only. Existing dirty/untracked state was not touched.
|
||||
|
||||
## Verdict
|
||||
|
||||
**(c) STRANDED.** `packages/macp` is exported, unit-tested, and registered as a CLI command group, but no production dispatch/execution code invokes its credential resolver, gate runner, or event emitter.
|
||||
A separate MACP-named OpenClaw/orchestrator rail exists, but it redefines task/result types and gate/event logic instead of importing `@mosaicstack/macp`; direct `mosaic yolo|claude|codex|opencode|pi` also bypasses it.
|
||||
|
||||
## 1. Production call sites vs tests
|
||||
|
||||
### Production references to `@mosaicstack/macp`
|
||||
|
||||
| Surface | Evidence | Actual use |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Unified CLI | `packages/mosaic/src/cli.ts:8,385` | Imports and registers `registerMacpCommand`; no task/gate/event execution. |
|
||||
| Forge | `packages/forge/src/types.ts:1,17,68,79` | **Type-only** imports of `GateEntry` and `TaskResult`. Pipeline calls an injected abstract executor at `packages/forge/src/pipeline-runner.ts:189-190,299-300`, not MACP. |
|
||||
| Mosaic package metadata | `packages/mosaic/package.json:36`; `packages/mosaic/src/runtime/update-checker.ts:172` | Dependency/update inventory only. |
|
||||
| Agent | No match under production `packages/agent/src/**` | No MACP import/call. |
|
||||
| Coord | No match under production `packages/coord/src/**`; dependency list is only `@mosaicstack/types` at `packages/coord/package.json:25-27` | No MACP import/call. |
|
||||
| Plugins | No `@mosaicstack/macp` import under `plugins/**` | No package use; the MACP-named plugin is an independent implementation (below). |
|
||||
|
||||
**Repository-wide production call-site search result:** excluding `packages/macp/**`, tests, worktrees, and build output, there are **zero** calls to `runGate`, `runGates`, `emitEvent`, `appendEvent`, or `resolveCredentials`.
|
||||
|
||||
### `packages/macp` implementation is internally connected only
|
||||
|
||||
- Public exports: `packages/macp/src/index.ts:1-48` exports Task/GateEntry/MACPEvent/TaskResult, credential resolution, `runGate(s)`, risk-floor, and event emission.
|
||||
- Gate runner calls its own event emitter: `packages/macp/src/gate-runner.ts:187-236`.
|
||||
- Event persistence implementation appends NDJSON to a caller-supplied path: `packages/macp/src/event-emitter.ts:11-27`.
|
||||
- There is **no exported programmatic `submit` implementation** in `packages/macp/src/index.ts:1-48`; only the CLI placeholder named `submit`.
|
||||
|
||||
### Test-only invocations
|
||||
|
||||
- Gate runner: `packages/macp/__tests__/gate-runner.test.ts:96-242` invokes `runGate/runGates`.
|
||||
- Event ledger: `packages/macp/__tests__/event-emitter.test.ts:46-133` invokes `appendEvent/emitEvent` against temporary `events.ndjson` files.
|
||||
- Credential resolver: `packages/macp/__tests__/credential-resolver.test.ts` exercises resolver behavior.
|
||||
- CLI tests only verify command registration: `packages/macp/src/cli.spec.ts:37-73`; `packages/mosaic/src/cli-smoke.spec.ts:8` imports registration.
|
||||
|
||||
## 2. Gate on the live dispatch path
|
||||
|
||||
### Direct Mosaic runtime launch bypasses MACP
|
||||
|
||||
- Runtime commands dispatch directly to harness launch: `packages/mosaic/src/commands/launch.ts:730-801`.
|
||||
- Claude/Pi go through the lease broker, then spawn the runtime: `packages/mosaic/src/commands/launch.ts:817-843`.
|
||||
- Commander wiring sends `mosaic yolo <runtime>` and direct runtime commands to `launchRuntime`: `packages/mosaic/src/commands/launch.ts:1102-1157,1165-1167`.
|
||||
- None of those ranges imports/calls `@mosaicstack/macp`, `runGates`, or `emitEvent`.
|
||||
|
||||
**Result:** a direct `mosaic yolo`, `mosaic claude/codex/opencode/pi`, or underlying exec does not create a typed MACP Task, run the package gate-runner, or append a package MACPEvent.
|
||||
|
||||
### Coord bypasses MACP
|
||||
|
||||
- Coord reads/updates `docs/TASKS.md`: `packages/coord/src/runner.ts:6,306-386`; parser/writer is `packages/coord/src/tasks-file.ts:326-377`.
|
||||
- Coord launches a child process directly: `packages/coord/src/runner.ts:397-427`.
|
||||
- Mission state is its own `.mosaic/orchestrator/mission.json`/`next-task.json`: `packages/coord/src/mission.ts:8-12`; `packages/coord/src/runner.ts:15-16,355-384`.
|
||||
|
||||
**Result:** Coord task execution has no MACP Task validation, package gate runner, or event append.
|
||||
|
||||
### Forge bypasses MACP execution
|
||||
|
||||
- Forge defines its own `ForgeTask` and abstract `TaskExecutor`: `packages/forge/src/types.ts:48-80`.
|
||||
- The production CLI injects a **stub executor** that immediately reports completion with empty gates: `packages/forge/src/cli.ts:13-31,167,185`.
|
||||
|
||||
**Result:** even `mosaic forge run` does not execute MACP gates or persist MACP events.
|
||||
|
||||
### Separate MACP-named rail is not `packages/macp`
|
||||
|
||||
- OpenClaw plugin registers an ACP backend named `macp`: `plugins/macp/src/index.ts:1-18,72-102`.
|
||||
- It locally redefines `OrchestratorTask`, `TaskResult`, and gate-result shapes instead of importing package types: `plugins/macp/src/macp-runtime.ts:43-77`.
|
||||
- It appends directly to `.mosaic/orchestrator/tasks.json`, triggers an external controller, and polls `results/<task>.json`: `plugins/macp/src/macp-runtime.ts:290-329,437-483`.
|
||||
- The controller independently implements `append_event`, `emit_event`, shell execution, gate execution, and results: `packages/mosaic/framework/tools/orchestrator-matrix/controller/mosaic_orchestrator.py:29-91,126-276`.
|
||||
- Its gate loop runs raw string gates after worker success: `mosaic_orchestrator.py:213-235`; it does not support the package's structured `GateEntry`/AI-review behavior.
|
||||
- Current checkout disables this controller: `.mosaic/orchestrator/config.json:2` (`"enabled": false`).
|
||||
- Plugin references `tools/macp/dispatcher/pi_runner.ts` at `plugins/macp/src/macp-runtime.ts:85-91`, but `tools/macp/` does not exist in this checkout.
|
||||
|
||||
**Result:** there is a parallel, optionally enabled MACP-shaped rail, not package integration. It cannot make `packages/macp` the enforced path.
|
||||
|
||||
## 3. Event ledger status
|
||||
|
||||
- Package persistence exists only as a library primitive: `packages/macp/src/event-emitter.ts:11-27` appends JSON lines to an arbitrary `eventsPath`.
|
||||
- Package event emission is reached only from package `runGates`: `packages/macp/src/gate-runner.ts:204-236`.
|
||||
- No production caller invokes package `runGates/emitEvent/appendEvent`; therefore no runtime destination path is configured for the package ledger.
|
||||
- Test-only ledgers use temp paths: `packages/macp/__tests__/event-emitter.test.ts:35-133`; gate tests use temp `events.ndjson`: `packages/macp/__tests__/gate-runner.test.ts:171-242`.
|
||||
- The separate Python controller writes `.mosaic/orchestrator/events.ndjson`: `mosaic_orchestrator.py:129-133,159-161,219-235`; the Mosaic Framework plugin only **reads** that file for context at `plugins/mosaic-framework/src/index.ts:279-316,430-438`.
|
||||
- In this checkout, `.mosaic/orchestrator/events.ndjson` is absent and the controller is disabled (`.mosaic/orchestrator/config.json:2`).
|
||||
|
||||
**Conclusion:** `MACPEvent` from `packages/macp` is defined/tested but not emitted or persisted by live production call sites. The similarly shaped Python ledger is a duplicate island.
|
||||
|
||||
## 4. Coord link
|
||||
|
||||
- `packages/coord` has no `@mosaicstack/macp` dependency/import: `packages/coord/package.json:25-27`; no matches in `packages/coord/src/**`.
|
||||
- Coord's task model is Markdown `docs/TASKS.md` plus mission/session JSON: `packages/coord/src/tasks-file.ts:1-10,257-377`; `packages/coord/src/mission.ts:8-12`; `packages/coord/src/runner.ts:306-427`.
|
||||
- It does not consume `.mosaic/orchestrator/events.ndjson`, MACP Task, MACPEvent, GateEntry, or TaskResult.
|
||||
|
||||
**Conclusion:** Coord and `packages/macp` are disconnected islands.
|
||||
|
||||
## Shortest wiring gap
|
||||
|
||||
**Single integration point:** replace the duplicated execution/gate/event block in `mosaic_orchestrator.py::run_single_task` (`:126-276`) with one production Node `TaskExecutor` backed by `@mosaicstack/macp` (typed Task validation + `resolveCredentials` + `runGates` + `emitEvent`), and make Coord/Forge/OpenClaw submit through that executor. This queue/controller choke point is where `yolo|acp|exec` worker outcomes can be gated and journaled before completion is recorded.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Mosaic Stack Remediation — Mission Charter
|
||||
|
||||
**Owner:** project orchestrator `mos-remediation` (Claude, launched in `/src/mosaic-stack`).
|
||||
**Origin:** 2026-07-16..31 fleet lifecycle postmortem. **Status:** PLANNING (task decomposition).
|
||||
**HOLD lifted** for this workstream by Jason, 2026-07-31 — "begin full mosaic fleet operation on this."
|
||||
|
||||
## Goal
|
||||
|
||||
Convert the 15 accepted postmortem remediation proposals into a working, **dogfooded** implementation.
|
||||
**North star:** anything with a deterministic right answer moves OUT of the LLM into a deterministic
|
||||
gate/program; the LLM handles only genuine judgment.
|
||||
|
||||
## Decision record (authoritative, immutable)
|
||||
|
||||
- **15/15 proposals decided: 13 accept, 2 modify (P-AUTHORITY-001, P-INBOX-001), 0 reject.**
|
||||
- Site + `annotations.json`: `jarvis-brain/docs/postmortem-spec/site/` (committed, origin/main).
|
||||
- Discussion checkpoint (rich rationale per proposal): `jarvis-brain/docs/scratchpads/postmortem/REMEDIATION-DISCUSSION-STATE.md`.
|
||||
- Postmortem report: mosaicstack/stack PR #107 (merged 88f4ee04).
|
||||
- MACP wiring scout (verdict c=STRANDED): `/tmp/macp-wiring-investigation.md` (copy into this dir — see TODO).
|
||||
|
||||
## The plan — 15 proposals collapse to 4 builds + hygiene
|
||||
|
||||
| Build | Absorbs | What it is |
|
||||
| ------------------------------------------------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **1. One choke-point service** (mechanical enforcer) | MISSION, STATE, AUDIT, WRAPPER, QUEUE | Deterministic program every task/data mutation flows through. **Wire the stranded `@mosaicstack/macp`** in at `mosaic_orchestrator.py::run_single_task` — typed tasks, gate-runner, event ledger, credential binding, tri-state write outcomes. |
|
||||
| **2. One durable spine + hot path** | (storage under everything) | **PG system-of-record + Redis hot queue** (transactional-outbox). Mission/tasks/state-claims/audit-ledger/comms-inbox all land here. |
|
||||
| **3. Rotation lifecycle** (finish the Mission Control Plane) | LIFECYCLE, CONTRACT, GUIDE, RECOVERY | Coordinator daemon: contract-hash binding, compaction-detected → rotate-not-compact, checkpoint→fresh-session→rehydrate, broker-independent recovery. Deterministic, not an LLM. Reuse `packages/coord`; existing PRD at `docs/mission-control/`. |
|
||||
| **4. Comms service** | AUTHORITY, INBOX (+ versioning roadmap) | Envelope (comms/v1) → sole-path service → PG/Redis → pluggable adapters (tmux→Matrix/Discord/Slack/Telegram). Version the protocol, not participants. |
|
||||
| **+ Hygiene & proof** | FLEET, WORKFLOW, CONFORMANCE | One roster-owned socket/host + stale GC; allowlist auto-sync; the conformance harness that fault-injects the failure classes and proves builds 1–4 hold. |
|
||||
|
||||
## The finding that sets the cost
|
||||
|
||||
**Built-but-unwired disease.** `@mosaicstack/macp` is stranded (nothing calls it); `packages/coord` primitives
|
||||
exist; the Mission Control PRD exists; PG + Redis already run in-stack. Three duplicate MACP islands, an
|
||||
orphaned context loader, a fail-open bypass. **Work = wire + consolidate + retire, NOT greenfield. "Finish, don't re-spec."**
|
||||
|
||||
## Sequencing (skeleton — adversarial decomposition refines this)
|
||||
|
||||
1. **Spine + choke-point service** (builds 1+2) — foundation; unlocks MISSION/STATE/AUDIT/WRAPPER/QUEUE at one integration point.
|
||||
2. **Rotation daemon** (build 3) on that spine — the drift fix proper.
|
||||
3. **Comms service** (build 4) — envelope → service → PG/Redis → adapters; retire direct-tmux.
|
||||
4. **Hygiene + conformance** (build 5) — fleet convergence, allowlist sync, dogfood harness.
|
||||
|
||||
- **Cross-cutting retirements:** flat-file orchestration tracking (hard cutover to DB), the 3 duplicate MACP islands, the silent `MOSAIC BYPASS`.
|
||||
|
||||
## Standing directives (Jason, 2026-07-31)
|
||||
|
||||
- **Dogfooding:** validate EACH fix against the live fleet failure that motivated it. Seed acceptance tests:
|
||||
Pi brick (RECOVERY), scout-bounce (INBOX/FLEET), gate-6 inert + #1019 recursion (QUEUE), identity drift
|
||||
(WRAPPER), auto-sync sweep (WORKFLOW), #1018 stale-consumed (INBOX). The fleet is its own test bed.
|
||||
- **Orchestration tracking → DB**, hard cutover ("rip off the bandaid"), NO flat-file interim. jarvis-brain
|
||||
PDA flat-files untouched. Current flat-file tracking runs as-is/unhardened until DB tracking is real, then one clean replace.
|
||||
|
||||
## The 15 decisions (one-line; full rationale in the checkpoint)
|
||||
|
||||
1. **P-ACTIVATION-001** accept — transactional CLI+hooks+broker+version release; block launch on skew, fail-SAFE.
|
||||
2. **P-AUTHORITY-001** MODIFY — structured authenticated inbox; envelope carries comms-PROTOCOL version; version the protocol not participants; N-version window.
|
||||
3. **P-LIFECYCLE-001** accept — rotation not recursive compaction; pre-empt at token threshold; enforcer = deterministic coordinator; = finish Mission Control Plane.
|
||||
4. **P-MISSION-001** accept — bind lanes to mission+task ledger; convention exists, ENFORCEMENT is the gap; mission+tasks → DB spine (hard cutover).
|
||||
5. **P-QUEUE-001** accept — repair queue transport + exit-asserting non-null-case tests (gate-6 was INERT fleet-wide; #1019 fix recursed the same bug).
|
||||
6. **P-STATE-001** accept — typed claims (source/confidence/TTL) not prose blob; MACP typed record; integrity fail-closed HMAC; don't fork a 4th island.
|
||||
7. **P-AUDIT-001** accept — MACPEvent lifecycle ledger; EXTEND enum to lifecycle events; runtime-neutral (executor-emitted); retire duplicate Python ledger.
|
||||
8. **P-WRAPPER-001** accept — identity derives from seat name + survives respawn; tri-state write outcomes MANDATORY; name safe target metadata.
|
||||
9. **P-CONTRACT-001** accept — bind session to contract hash; re-anchor on policy-change OR compaction-detected; stale generation loses authority MECHANICALLY.
|
||||
10. **P-INBOX-001** MODIFY — sole-path comms SERVICE; PG durable SoR + Redis hot queue (outbox, reconciliation sweeper); pluggable adapters; protocol-first, PG-first-then-Redis.
|
||||
11. **P-RECOVERY-001** accept — broker-independent bootstrap recovery; honest capability labeling; break-glass LOUD+AUDITED+TEMPORARY not silent permanent bypass.
|
||||
12. **P-GUIDE-001** accept — delete `/compact and continue` from orchestrator path (keep for ephemeral); removal = substitution (wire rotation trigger).
|
||||
13. **P-FLEET-001** accept — one roster-owned socket/host; quarantine unmanaged; stale-session GC; prerequisite for INBOX identity-addressing.
|
||||
14. **P-WORKFLOW-001** accept — auto-sync ALLOWLIST not denylist; worktree/lease isolation for agent docs/source; DB-tracking obviates the flat-file-sweep criterion.
|
||||
15. **P-CONFORMANCE-001** accept — fleet lifecycle harness on REAL runtime artifacts + fault injection; the 100-rotations-lossless bar is a test; target the DB substrate.
|
||||
|
||||
## Fleet operating model
|
||||
|
||||
- **Project orchestrator** `mos-remediation` (this seat) owns the mission; coordinates under Mos (lead).
|
||||
- **Adversarial task decomposition:** `planner-opus` (robustness) + `planner-sol` (pragmatic) each decompose
|
||||
the plan independently; orchestrator reconciles into `TASKS.md`/DB tasks. Oppositional by design.
|
||||
- **Delivery gates (non-negotiable):** author≠reviewer, PRE-REGISTERED diff-blind acceptance checks committed
|
||||
before reading the diff, CI terminal-green, completion = merged PR + closed issue. rev-974 = mosaicstack reviewer.
|
||||
- **Compaction survival:** see `KICKSTART.md` in this dir — the resume procedure. Persist typed state, not transcript.
|
||||
@@ -0,0 +1,92 @@
|
||||
# orchestrator/ tools
|
||||
|
||||
Helper scripts for r0 coordinator / orchestrator sessions — mission lifecycle,
|
||||
session health, continuation, and board maintenance. See
|
||||
`framework/guides/ORCHESTRATOR-PROTOCOL.md` for the surrounding process.
|
||||
|
||||
| Script | Purpose |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `mission-init.sh` | Initialize a new orchestration mission (manifest, scratchpad, TASKS.md). |
|
||||
| `mission-status.sh` | Show the mission progress dashboard. |
|
||||
| `session-run.sh` | Generate continuation context and launch the target runtime. |
|
||||
| `session-resume.sh` | Crash recovery for dead orchestrator sessions. |
|
||||
| `session-status.sh` | Check agent session health. |
|
||||
| `continue-prompt.sh` | Generate the continuation prompt for the next session. |
|
||||
| `board-roll.sh` | Keep a LIVE orchestration board under its byte cap by rolling the oldest entries to its LEDGER. |
|
||||
| `smoke-test.sh` | Behavior smoke checks for the coord continue/run workflows. |
|
||||
| `test-board-roll.sh` | Regression harness for `board-roll.sh`. |
|
||||
| `_lib.sh` | Shared functions sourced by the above (state files, TASKS.md parsing, locks). |
|
||||
|
||||
## board-roll.sh
|
||||
|
||||
Coordinator boards (`MOS-ORCHESTRATION-BOARD-LIVE.md`, `MS-LEAD-BOARD-LIVE.md`)
|
||||
follow a **"< 8 KB LIVE"** discipline: the LIVE board is the only file loaded on
|
||||
resume, so it must stay small, and history lives in an append-only LEDGER. When a
|
||||
board write would push LIVE over its cap, coordinators otherwise hand-trim and
|
||||
retry every time — an observed 38 ABORT-OVER-CAP cycles in one 24 h window.
|
||||
`board-roll.sh` automates that trim mechanically and reversibly: the audit trail
|
||||
is moved to the LEDGER instead of being hand-deleted.
|
||||
|
||||
### Contract (conservative — it never guesses what is safe to move)
|
||||
|
||||
The LIVE board opts in by wrapping its aging archival ticks in an explicit roll
|
||||
zone. Everything **outside** the markers (title, protocol blockquote, curated
|
||||
always-current `##` sections) is pinned and never touched:
|
||||
|
||||
```markdown
|
||||
# MOS ORCHESTRATION BOARD — LIVE state
|
||||
|
||||
> protocol blockquote … (pinned)
|
||||
|
||||
## 🟦 Curated always-current section (pinned)
|
||||
|
||||
…
|
||||
|
||||
<!-- BOARD-ROLL:START -->
|
||||
|
||||
### 2026-07-22 (mid²²) — newest tick, stays longest
|
||||
|
||||
…
|
||||
|
||||
### 2026-07-20 (dawn) — oldest tick, rolled first
|
||||
|
||||
…
|
||||
|
||||
<!-- BOARD-ROLL:END -->
|
||||
```
|
||||
|
||||
Inside the zone, entries are delimited by a heading marker (default `### `) and
|
||||
are assumed **newest-first (top) → oldest-last (bottom)**. `board-roll.sh` moves
|
||||
whole oldest (bottom-most) entry blocks out of the zone and appends them verbatim
|
||||
to the LEDGER, one at a time, until LIVE is back under the cap or the zone is
|
||||
empty. If the board has no markers, it exits `3` and changes nothing — adding the
|
||||
markers is a deliberate opt-in by the board owner.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
board-roll.sh --live <LIVE.md> --ledger <LEDGER.md> [options]
|
||||
|
||||
--live <path> LIVE board file (required)
|
||||
--ledger <path> append-only LEDGER file (required; created if absent)
|
||||
--cap <bytes> size ceiling for LIVE (default 8192)
|
||||
--marker <prefix> entry-heading prefix inside the roll zone (default "### ")
|
||||
--dry-run report what would move; change nothing
|
||||
-h, --help show help and exit 0
|
||||
```
|
||||
|
||||
Only **one** roll zone is supported. If a board carries more than one
|
||||
`BOARD-ROLL:START`/`END` pair, `board-roll.sh` refuses (exit `3`, zero changes)
|
||||
rather than span first-START..last-END and relocate the curated content between
|
||||
the zones — consolidate the ticks into a single zone instead.
|
||||
|
||||
Exit codes: `0` LIVE under cap (already, or after rolling) — on `--dry-run`, a
|
||||
plan exists or nothing to do · `2` usage / argument / IO error · `3` cannot meet
|
||||
the cap (no markers, **more than one marker pair**, or the pinned sections alone
|
||||
exceed the cap and need a manual trim).
|
||||
|
||||
Writes are atomic (temp file + `mv`, LEDGER first) so a failure never leaves a
|
||||
board half-written; line endings are normalized to LF on rewrite. `--dry-run`
|
||||
first is recommended when wiring it into a board update protocol.
|
||||
|
||||
Run the regression suite with `bash test-board-roll.sh`.
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# board-roll.sh — keep a LIVE orchestration board under its byte cap by rolling
|
||||
# the oldest archival entries out to its append-only LEDGER.
|
||||
#
|
||||
# WHY: coordinator boards (MOS-ORCHESTRATION-BOARD-LIVE.md, MS-LEAD-BOARD-LIVE.md)
|
||||
# enforce a "< 8 KB LIVE" discipline via a self-guard that ABORTs the board write
|
||||
# when the file exceeds the cap. In practice the LIVE board keeps bumping the cap,
|
||||
# so coordinators hand-trim + retry every time (observed: 38 ABORT-OVER-CAP cycles
|
||||
# in a 24h window on one coordinator). This automates that trim, mechanically and
|
||||
# reversibly, so the audit trail is preserved in the LEDGER instead of hand-deleted.
|
||||
#
|
||||
# CONTRACT (conservative by design — it NEVER guesses what is safe to move):
|
||||
# The LIVE board must declare an explicit ROLL ZONE with HTML-comment markers:
|
||||
#
|
||||
# <!-- BOARD-ROLL:START -->
|
||||
# ### 2026-07-22 (newest tick — stays longest)
|
||||
# ...
|
||||
# ### 2026-07-19 (oldest tick — rolled first)
|
||||
# ...
|
||||
# <!-- BOARD-ROLL:END -->
|
||||
#
|
||||
# Everything OUTSIDE the markers (title, protocol blockquote, curated always-current
|
||||
# `##` sections) is PINNED and never touched. Inside the zone, entries are delimited
|
||||
# by a heading marker (default `### `) and are assumed newest-first (top) → oldest-last
|
||||
# (bottom), matching board convention. board-roll moves whole oldest (bottom-most)
|
||||
# entry blocks out of the zone and APPENDS them verbatim to the LEDGER, one block at a
|
||||
# time, until the LIVE file is back under the cap or the zone is empty.
|
||||
#
|
||||
# If no markers are present, it exits 3 without changing anything (safe default —
|
||||
# adding the markers is a deliberate opt-in by the board owner).
|
||||
#
|
||||
# USAGE:
|
||||
# board-roll.sh --live <LIVE.md> --ledger <LEDGER.md> [options]
|
||||
#
|
||||
# OPTIONS:
|
||||
# --live <path> LIVE board file (required)
|
||||
# --ledger <path> append-only LEDGER file (required; created if absent)
|
||||
# --cap <bytes> size ceiling for LIVE (default 8192)
|
||||
# --marker <prefix> entry-heading prefix inside the roll zone (default "### ")
|
||||
# --dry-run report what would move + resulting size; change nothing
|
||||
# -h, --help print usage and exit 0
|
||||
#
|
||||
# EXIT CODES:
|
||||
# 0 LIVE is under cap (already, or after rolling); on --dry-run, 0 = a plan exists
|
||||
# (or nothing to do)
|
||||
# 2 usage / argument / IO error (bad flag, missing file, unwritable target)
|
||||
# 3 cannot satisfy the cap: no roll markers present, MORE THAN ONE marker pair
|
||||
# (multiple zones are refused, not guessed), OR the zone was emptied and LIVE
|
||||
# is still over cap (curated pinned sections need a manual trim)
|
||||
#
|
||||
# NOTE: line endings are normalized to LF on rewrite (boards are LF markdown); a
|
||||
# trailing newline is always ensured. Writes are atomic (temp file + mv) so a
|
||||
# failure never leaves LIVE or LEDGER half-written.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
START_MARK='<!-- BOARD-ROLL:START -->'
|
||||
END_MARK='<!-- BOARD-ROLL:END -->'
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: board-roll.sh --live <LIVE.md> --ledger <LEDGER.md> [options]
|
||||
|
||||
Roll the oldest entries out of a LIVE orchestration board into its LEDGER
|
||||
until the LIVE file is under a byte cap. Conservative: only content inside
|
||||
explicit <!-- BOARD-ROLL:START -->/<!-- BOARD-ROLL:END --> markers is moved.
|
||||
|
||||
Options:
|
||||
--live <path> LIVE board file (required)
|
||||
--ledger <path> append-only LEDGER file (required; created if absent)
|
||||
--cap <bytes> size ceiling for LIVE (default 8192)
|
||||
--marker <prefix> entry-heading prefix inside the roll zone (default "### ")
|
||||
--dry-run report what would move; change nothing
|
||||
-h, --help show this help and exit 0
|
||||
|
||||
Exit: 0 under cap (or dry-run plan) · 2 usage/IO error · 3 cannot meet cap
|
||||
(no markers, or pinned sections alone exceed the cap).
|
||||
EOF
|
||||
}
|
||||
|
||||
die() { echo "board-roll: $*" >&2; exit 2; }
|
||||
|
||||
LIVE=""; LEDGER=""; CAP=8192; MARKER='### '; DRYRUN=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--live) LIVE="${2:-}"; shift 2 || die "--live needs a value" ;;
|
||||
--ledger) LEDGER="${2:-}"; shift 2 || die "--ledger needs a value" ;;
|
||||
--cap) CAP="${2:-}"; shift 2 || die "--cap needs a value" ;;
|
||||
--marker) MARKER="${2:-}"; shift 2 || die "--marker needs a value" ;;
|
||||
--dry-run) DRYRUN=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) usage >&2; die "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$LIVE" ]] || { usage >&2; die "--live is required"; }
|
||||
[[ -n "$LEDGER" ]] || { usage >&2; die "--ledger is required"; }
|
||||
[[ -f "$LIVE" ]] || die "LIVE file not found: $LIVE"
|
||||
[[ "$CAP" =~ ^[0-9]+$ ]] || die "--cap must be a non-negative integer, got: $CAP"
|
||||
|
||||
# --- read LIVE into a line array (newlines stripped; re-added on write) ---------
|
||||
mapfile -t LINES < "$LIVE"
|
||||
|
||||
# byte size of an array rendered as LF-terminated text
|
||||
render_size() {
|
||||
if [[ $# -eq 0 ]]; then printf 0; return; fi
|
||||
printf '%s\n' "$@" | wc -c
|
||||
}
|
||||
|
||||
orig_size=$(render_size "${LINES[@]}")
|
||||
|
||||
# --- already under cap → nothing to do -----------------------------------------
|
||||
if (( orig_size < CAP )); then
|
||||
echo "board-roll: LIVE is ${orig_size}B (< cap ${CAP}B) — nothing to roll."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- locate the roll-zone markers ----------------------------------------------
|
||||
# Exactly ONE marker pair is supported. If a board carries more than one START or
|
||||
# END marker we REFUSE (exit 3, zero changes) rather than guess: a naive
|
||||
# first-START..last-END span would swallow the curated content and the intermediate
|
||||
# markers sitting between two intended zones and silently relocate that pinned text
|
||||
# to the LEDGER — the exact data-loss this tool exists to prevent. Refusing matches
|
||||
# the "no markers = exit 3" conservative posture.
|
||||
start_idx=-1; end_idx=-1; start_count=0; end_count=0
|
||||
for i in "${!LINES[@]}"; do
|
||||
if [[ "${LINES[$i]}" == "$START_MARK" ]]; then
|
||||
if (( start_count == 0 )); then start_idx=$i; fi
|
||||
start_count=$(( start_count + 1 ))
|
||||
fi
|
||||
if [[ "${LINES[$i]}" == "$END_MARK" ]]; then
|
||||
end_idx=$i
|
||||
end_count=$(( end_count + 1 ))
|
||||
fi
|
||||
done
|
||||
if (( start_count > 1 || end_count > 1 )); then
|
||||
echo "board-roll: LIVE is ${orig_size}B (>= cap ${CAP}B) but has ${start_count} START / ${end_count} END" >&2
|
||||
echo " markers — only a SINGLE '$START_MARK' … '$END_MARK' roll zone is supported." >&2
|
||||
echo " Multiple zones are refused (not guessed) so content between zones is never relocated." >&2
|
||||
echo " Consolidate the archival ticks into one zone, or trim manually." >&2
|
||||
exit 3
|
||||
fi
|
||||
if (( start_idx < 0 || end_idx < 0 || end_idx <= start_idx )); then
|
||||
echo "board-roll: LIVE is ${orig_size}B (>= cap ${CAP}B) but no usable roll zone" >&2
|
||||
echo " (need '$START_MARK' then '$END_MARK'). Add the markers around the" >&2
|
||||
echo " archival tick section to opt this board into automatic rolling." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# preamble = lines [0 .. start_idx] (inclusive of START marker)
|
||||
# zone = lines (start_idx .. end_idx) (exclusive of both markers)
|
||||
# footer = lines [end_idx .. end] (inclusive of END marker)
|
||||
preamble=(); zone=(); footer=()
|
||||
for i in "${!LINES[@]}"; do
|
||||
if (( i <= start_idx )); then preamble+=("${LINES[$i]}")
|
||||
elif (( i < end_idx )); then zone+=("${LINES[$i]}")
|
||||
else footer+=("${LINES[$i]}")
|
||||
fi
|
||||
done
|
||||
|
||||
# --- split the zone into a fixed head + entry blocks ----------------------------
|
||||
# zone_head = any zone lines before the first entry marker (kept, never rolled).
|
||||
# blocks[k] = newline-joined text of entry k (marker line .. line before next marker).
|
||||
zone_head=(); declare -a block_start=()
|
||||
first_block=-1
|
||||
for i in "${!zone[@]}"; do
|
||||
if [[ "${zone[$i]}" == "$MARKER"* ]]; then
|
||||
[[ $first_block -eq -1 ]] && first_block=$i
|
||||
block_start+=("$i")
|
||||
fi
|
||||
done
|
||||
if (( first_block == -1 )); then
|
||||
echo "board-roll: LIVE is ${orig_size}B (>= cap ${CAP}B) but the roll zone has no" >&2
|
||||
echo " '${MARKER}' entries to move. Trim the pinned sections manually." >&2
|
||||
exit 3
|
||||
fi
|
||||
for (( i=0; i<first_block; i++ )); do zone_head+=("${zone[$i]}"); done
|
||||
|
||||
nblocks=${#block_start[@]}
|
||||
# block k spans zone[ block_start[k] .. (block_start[k+1]-1 or end-of-zone) ]
|
||||
block_text() { # $1 = block index → prints the block's lines, LF-joined (no trailing)
|
||||
local k=$1 s e
|
||||
s=${block_start[$k]}
|
||||
if (( k+1 < nblocks )); then e=$(( block_start[$((k+1))] - 1 )); else e=$(( ${#zone[@]} - 1 )); fi
|
||||
local out=()
|
||||
for (( j=s; j<=e; j++ )); do out+=("${zone[$j]}"); done
|
||||
printf '%s\n' "${out[@]}"
|
||||
}
|
||||
|
||||
# --- greedily roll oldest (bottom-most) blocks until under cap ------------------
|
||||
# keep = number of newest blocks retained; start with all, drop from the bottom.
|
||||
keep=$nblocks # blocks [keep .. nblocks-1] are the oldest set that gets moved
|
||||
current_size=$orig_size
|
||||
build_live_size() { # size of LIVE if we keep blocks [0 .. keep-1]
|
||||
local acc=("${preamble[@]}" "${zone_head[@]}")
|
||||
local k s e j
|
||||
for (( k=0; k<keep; k++ )); do
|
||||
s=${block_start[$k]}
|
||||
if (( k+1 < nblocks )); then e=$(( block_start[$((k+1))] - 1 )); else e=$(( ${#zone[@]} - 1 )); fi
|
||||
for (( j=s; j<=e; j++ )); do acc+=("${zone[$j]}"); done
|
||||
done
|
||||
acc+=("${footer[@]}")
|
||||
render_size "${acc[@]}"
|
||||
}
|
||||
while (( current_size >= CAP && keep > 0 )); do
|
||||
keep=$(( keep - 1 ))
|
||||
current_size=$(build_live_size)
|
||||
done
|
||||
|
||||
moved_count=$(( nblocks - keep ))
|
||||
if (( moved_count == 0 )); then
|
||||
# zone had entries but none movable brought us under (shouldn't happen: keep hits 0)
|
||||
echo "board-roll: could not reduce LIVE below cap (${current_size}B >= ${CAP}B)." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# --- dry-run report -------------------------------------------------------------
|
||||
plan_headers() {
|
||||
local k
|
||||
for (( k=keep; k<nblocks; k++ )); do
|
||||
# first line of each moved block
|
||||
printf ' %s\n' "${zone[${block_start[$k]}]}"
|
||||
done
|
||||
}
|
||||
if (( DRYRUN )); then
|
||||
echo "board-roll: DRY RUN"
|
||||
echo " LIVE now: ${orig_size}B (cap ${CAP}B) — over by $(( orig_size - CAP ))B"
|
||||
echo " would roll: ${moved_count} of ${nblocks} entr$([[ $moved_count -eq 1 ]] && echo y || echo ies) (oldest first):"
|
||||
plan_headers
|
||||
echo " LIVE after: ${current_size}B"
|
||||
if (( current_size >= CAP )); then
|
||||
echo " WARNING: still >= cap after emptying the zone; pinned sections need a manual trim." >&2
|
||||
exit 3
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- commit the roll atomically -------------------------------------------------
|
||||
live_tmp="$(mktemp "${LIVE}.roll.XXXXXX")" || die "cannot create temp next to LIVE"
|
||||
ledger_tmp=""
|
||||
# shellcheck disable=SC2329 # invoked indirectly via `trap cleanup EXIT`
|
||||
cleanup() { rm -f "$live_tmp" "$ledger_tmp" 2>/dev/null || true; }
|
||||
trap cleanup EXIT
|
||||
|
||||
# new LIVE = preamble + zone_head + kept blocks + footer
|
||||
{
|
||||
printf '%s\n' "${preamble[@]}" "${zone_head[@]}"
|
||||
for (( k=0; k<keep; k++ )); do block_text "$k"; done
|
||||
printf '%s\n' "${footer[@]}"
|
||||
} > "$live_tmp"
|
||||
|
||||
# LEDGER gets the moved blocks appended verbatim, in original top→bottom order,
|
||||
# under a provenance separator. LEDGER is append-only, so we only ever add at EOF.
|
||||
ledger_tmp="$(mktemp "${LEDGER}.roll.XXXXXX")" || die "cannot create temp next to LEDGER"
|
||||
if [[ -f "$LEDGER" ]]; then cat "$LEDGER" > "$ledger_tmp"; fi
|
||||
# ensure a trailing newline on existing content before appending
|
||||
if [[ -s "$ledger_tmp" && -n "$(tail -c1 "$ledger_tmp")" ]]; then printf '\n' >> "$ledger_tmp"; fi
|
||||
{
|
||||
printf '\n<!-- board-roll: %d entr%s rolled from %s -->\n' \
|
||||
"$moved_count" "$([[ $moved_count -eq 1 ]] && echo y || echo ies)" "$(basename "$LIVE")"
|
||||
for (( k=keep; k<nblocks; k++ )); do block_text "$k"; done
|
||||
} >> "$ledger_tmp"
|
||||
|
||||
# atomic swap (both, LEDGER first so a crash never drops content that left LIVE)
|
||||
mv "$ledger_tmp" "$LEDGER"; ledger_tmp=""
|
||||
mv "$live_tmp" "$LIVE"; live_tmp=""
|
||||
trap - EXIT
|
||||
|
||||
# read the real on-disk size back (truthful, not the predicted value)
|
||||
final_size=$(wc -c < "$LIVE")
|
||||
echo "board-roll: rolled ${moved_count} entr$([[ $moved_count -eq 1 ]] && echo y || echo ies) to $(basename "$LEDGER"); LIVE ${orig_size}B → ${final_size}B (cap ${CAP}B)."
|
||||
if (( final_size >= CAP )); then
|
||||
echo "board-roll: still >= cap after rolling all zone entries; pinned sections need a manual trim." >&2
|
||||
exit 3
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression harness for board-roll.sh — rolling oldest LIVE-board entries to LEDGER.
|
||||
#
|
||||
# Asserts:
|
||||
# 1. Under cap → no-op, exit 0, files unchanged.
|
||||
# 2. Over cap, no roll markers → exit 3, LIVE unchanged (never guesses).
|
||||
# 3. Over cap, markers present → rolls the fewest oldest entries to get under cap,
|
||||
# LIVE ends under cap, pinned preamble/footer + newest entries preserved.
|
||||
# 4. Rolled blocks land in the LEDGER verbatim, oldest set in original order.
|
||||
# 5. --dry-run changes nothing and reports a plan.
|
||||
# 6. Zone emptied but pinned sections alone exceed cap → exit 3.
|
||||
# 7. --help exits 0 and prints usage; an unknown flag exits nonzero (#701 discipline).
|
||||
# 8. More than one marker pair → exit 3, unchanged; curated content between the two
|
||||
# zones is never relocated to the LEDGER (rev0 #868 regression).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SUT="$SCRIPT_DIR/board-roll.sh"
|
||||
|
||||
fail=0
|
||||
note() { echo "FAIL: $*" >&2; fail=1; }
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# builds a LIVE board: pinned preamble + roll zone with N dated entries (newest first),
|
||||
# each entry padded to be individually large so the cap math is predictable.
|
||||
make_board() { # $1 file $2 n_entries $3 with_markers(1/0) $4 pad_bytes
|
||||
local f=$1 n=$2 markers=$3 pad=$4 i padtxt
|
||||
padtxt=$(head -c "$pad" < /dev/zero | tr '\0' 'x')
|
||||
{
|
||||
echo "# BOARD — LIVE"
|
||||
echo "> pinned protocol blockquote, never rolled."
|
||||
echo
|
||||
echo "## Curated always-current section (pinned)"
|
||||
echo "- this stays no matter what"
|
||||
echo
|
||||
[[ "$markers" == 1 ]] && echo '<!-- BOARD-ROLL:START -->'
|
||||
# newest first (i=n .. 1); oldest (i=1) ends at the bottom
|
||||
for (( i=n; i>=1; i-- )); do
|
||||
echo "### 2026-07-$(printf '%02d' $i) tick number $i"
|
||||
echo "- detail $i $padtxt"
|
||||
echo
|
||||
done
|
||||
[[ "$markers" == 1 ]] && echo '<!-- BOARD-ROLL:END -->'
|
||||
} > "$f"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── 1. under cap → no-op ───────────────────────────────────────────────────────
|
||||
L="$WORK/live1.md"; G="$WORK/ledger1.md"; : > "$G"
|
||||
make_board "$L" 2 1 10
|
||||
before=$(cat "$L")
|
||||
if ! out=$(bash "$SUT" --live "$L" --ledger "$G" --cap 100000 2>&1); then
|
||||
note "under-cap should exit 0 (got nonzero): $out"
|
||||
fi
|
||||
[[ "$(cat "$L")" == "$before" ]] || note "under-cap modified LIVE"
|
||||
[[ -s "$G" ]] && note "under-cap wrote to LEDGER"
|
||||
|
||||
# ── 2. over cap, no markers → exit 3, unchanged ────────────────────────────────
|
||||
L="$WORK/live2.md"; G="$WORK/ledger2.md"; : > "$G"
|
||||
make_board "$L" 6 0 400
|
||||
before=$(cat "$L")
|
||||
set +e; bash "$SUT" --live "$L" --ledger "$G" --cap 800 >/dev/null 2>&1; rc=$?; set -e
|
||||
[[ "$rc" -eq 3 ]] || note "no-markers over-cap should exit 3 (got $rc)"
|
||||
[[ "$(cat "$L")" == "$before" ]] || note "no-markers run modified LIVE (must never guess)"
|
||||
|
||||
# ── 3+4. over cap with markers → rolls oldest, LIVE under cap, LEDGER gets them ─
|
||||
L="$WORK/live3.md"; G="$WORK/ledger3.md"; echo "# LEDGER" > "$G"
|
||||
make_board "$L" 6 1 400 # 6 entries, each ~>400B
|
||||
big=$(wc -c < "$L")
|
||||
[[ "$big" -ge 2000 ]] || note "fixture too small to test rolling ($big B)"
|
||||
if ! out=$(bash "$SUT" --live "$L" --ledger "$G" --cap 2000 2>&1); then
|
||||
note "marker roll should exit 0 when it can get under cap: $out"
|
||||
fi
|
||||
after=$(wc -c < "$L")
|
||||
[[ "$after" -lt 2000 ]] || note "LIVE still >= cap after roll ($after B)"
|
||||
# pinned content survives
|
||||
grep -q "Curated always-current section" "$L" || note "roll dropped pinned section"
|
||||
grep -q 'BOARD-ROLL:START' "$L" || note "roll dropped START marker"
|
||||
grep -q 'BOARD-ROLL:END' "$L" || note "roll dropped END marker"
|
||||
# newest entry (07-06) stays; oldest (07-01) is the first to leave
|
||||
grep -q "### 2026-07-06 tick number 6" "$L" || note "roll dropped the newest entry"
|
||||
grep -q "### 2026-07-01 tick number 1" "$L" && note "oldest entry not rolled out of LIVE"
|
||||
# oldest went to LEDGER
|
||||
grep -q "### 2026-07-01 tick number 1" "$G" || note "oldest entry not appended to LEDGER"
|
||||
grep -q "board-roll:.*rolled from live3.md" "$G" || note "LEDGER missing provenance separator"
|
||||
# a rolled entry must not be duplicated (present in exactly one of LIVE/LEDGER)
|
||||
if grep -q "### 2026-07-01 tick number 1" "$L"; then note "rolled entry duplicated in LIVE"; fi
|
||||
# LEDGER original content preserved
|
||||
grep -q "^# LEDGER" "$G" || note "roll clobbered existing LEDGER content"
|
||||
|
||||
# ── 5. --dry-run changes nothing ───────────────────────────────────────────────
|
||||
L="$WORK/live5.md"; G="$WORK/ledger5.md"; echo "# LEDGER" > "$G"
|
||||
make_board "$L" 6 1 400
|
||||
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"
|
||||
echo "$out" | grep -qi "dry run" || note "dry-run did not announce itself"
|
||||
echo "$out" | grep -q "would roll" || note "dry-run did not report a plan"
|
||||
[[ "$(cat "$L")" == "$before_l" ]] || note "dry-run modified LIVE"
|
||||
[[ "$(cat "$G")" == "$before_g" ]] || note "dry-run modified LEDGER"
|
||||
|
||||
# ── 6. zone emptied, pinned alone over cap → exit 3 ────────────────────────────
|
||||
# cap 120 is below the pinned preamble+footer size (~180B), so even after rolling
|
||||
# every zone entry the LIVE file stays over cap → must report the unsatisfiable case.
|
||||
L="$WORK/live6.md"; G="$WORK/ledger6.md"; echo "# LEDGER" > "$G"
|
||||
make_board "$L" 3 1 50
|
||||
set +e; bash "$SUT" --live "$L" --ledger "$G" --cap 120 >/dev/null 2>&1; rc=$?; set -e
|
||||
[[ "$rc" -eq 3 ]] || note "unsatisfiable cap should exit 3 (got $rc)"
|
||||
|
||||
# ── 7. help exits 0, unknown flag exits nonzero (#701) ─────────────────────────
|
||||
if ! out=$(bash "$SUT" --help 2>&1); then note "--help exited nonzero"; fi
|
||||
[[ "$out" == Usage:* ]] || note "--help did not print usage"
|
||||
bash "$SUT" -h >/dev/null 2>&1 || note "-h exited nonzero"
|
||||
if bash "$SUT" --not-a-real-flag >/dev/null 2>&1; then note "unknown flag was accepted"; fi
|
||||
if bash "$SUT" --live "$WORK/live3.md" >/dev/null 2>&1; then note "missing --ledger was accepted"; fi
|
||||
|
||||
# ── 8. multiple marker pairs → exit 3, unchanged (no cross-zone relocation) ─────
|
||||
# Two separately-marked zones with a curated pinned section BETWEEN them. A naive
|
||||
# first-START..last-END span would sweep that curated section (and the intermediate
|
||||
# markers) into the LEDGER. board-roll must refuse (exit 3) and touch nothing.
|
||||
L="$WORK/live8.md"; G="$WORK/ledger8.md"; echo "# LEDGER" > "$G"
|
||||
pad8=$(head -c 300 < /dev/zero | tr '\0' 'x')
|
||||
{
|
||||
echo "# BOARD — LIVE"
|
||||
echo "> pinned protocol blockquote"
|
||||
echo
|
||||
echo '<!-- BOARD-ROLL:START -->'
|
||||
echo "### 2026-07-10 zone-A newest"
|
||||
echo "- detail A2 $pad8"
|
||||
echo "### 2026-07-09 zone-A oldest"
|
||||
echo "- detail A1 $pad8"
|
||||
echo '<!-- BOARD-ROLL:END -->'
|
||||
echo
|
||||
echo "## Curated-between-zones (pinned — must never move)"
|
||||
echo "- CANARY-BETWEEN keep me"
|
||||
echo
|
||||
echo '<!-- BOARD-ROLL:START -->'
|
||||
echo "### 2026-07-08 zone-B newest"
|
||||
echo "- detail B2 $pad8"
|
||||
echo "### 2026-07-07 zone-B oldest"
|
||||
echo "- detail B1 $pad8"
|
||||
echo '<!-- BOARD-ROLL:END -->'
|
||||
} > "$L"
|
||||
before8=$(cat "$L")
|
||||
set +e; bash "$SUT" --live "$L" --ledger "$G" --cap 80 >/dev/null 2>&1; rc=$?; set -e
|
||||
[[ "$rc" -eq 3 ]] || note "multi-pair board should exit 3 (got $rc)"
|
||||
[[ "$(cat "$L")" == "$before8" ]] || note "multi-pair run modified LIVE (must never guess across zones)"
|
||||
grep -q "CANARY-BETWEEN keep me" "$L" || note "multi-pair run relocated curated between-zones content"
|
||||
grep -q "CANARY-BETWEEN" "$G" && note "curated between-zones content leaked into LEDGER"
|
||||
|
||||
if [[ "$fail" -eq 0 ]]; then
|
||||
echo "board-roll regression passed (8 groups)"
|
||||
fi
|
||||
exit "$fail"
|
||||
Reference in New Issue
Block a user