qualia-framework 6.9.0 → 6.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,94 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  > Note: git tags for historical versions were not retained; commit references are approximate
9
9
  > and dates reflect commit history rather than npm publish timestamps.
10
10
 
11
+ ## [6.14.0] - 2026-06-20 (v7 kernel, step 4 — R3: the cross-artifact analyze gate)
12
+
13
+ Spec-Kit's most-copied feature, ported. Qualia validated each artifact in isolation — `plan-contract.js` proves the contract is internally well-formed, `harness-eval` scores the built phase — but **nothing diffed scope ↔ plan**. That's exactly where a junior's idea silently loses intent: the scope asks for X, the plan quietly drops it, and no deterministic check notices. This adds that check, between plan and build.
14
+
15
+ ### Added — `bin/analyze-gate.js` (deterministic, zero-LLM, zero-dependency)
16
+ - Diffs the plan contract against **intent**: scope acceptance criteria (`.planning/phase-{N}-context.md` `## Acceptance Criteria`) and the CONTEXT.md glossary. Pure keyword/token coverage — same inputs → same output.
17
+ - **Four checks:** (1) *uncovered scope AC* — a scope requirement whose key terms don't appear in the contract (HIGH; the plan dropped it); (2) *orphan success criterion* — a contract success criterion no task covers (MEDIUM); (3) *glossary violation* — the plan uses a term CONTEXT.md lists under `Avoid:` (MEDIUM; a genuine spec↔plan contradiction); (4) *scope-reduction language* in task actions/ACs (HIGH; reuses `plan-contract.findScopeReductionPhrases`).
18
+ - CLI `analyze-gate.js <phase>` auto-discovers contract + scope + CONTEXT.md; `--json` for machine output. Exit 0 = clean, 1 = findings, 2 = invocation error. Library exports (`analyze`, `coverage`, `parseScopeAcceptanceCriteria`, `parseGlossaryBannedTerms`) for reuse.
19
+
20
+ ### Changed — wired into `qualia-build` as the plan→build gate (§1a), profile-aware
21
+ - Runs `analyze-gate.js {N}` before any build. **strict** profile → a HIGH finding is a stop (route to `/qualia-plan --gaps` or `/qualia-scope`); **standard** → surface + proceed with an explicit ack and a logged waiver. **No scope file = scope-coverage skipped, not a failure** — scope-less phases and `/qualia-feature` trivia still build.
22
+ - Deliberately **not** a `state.js` hard precondition: scope files are optional, so gating the `built` transition there would brick builds for projects that never ran `/qualia-scope`. The gate lives at the skill seam and degrades gracefully.
23
+ - Shipped into installed `bin/` via `runtime-manifest.js`.
24
+
25
+ ### Tests
26
+ - `tests/analyze-gate.test.sh` (new, 21 cases): clean pass, under-covered scope AC, orphan success criterion, glossary violation, no-scope skip, missing-contract error, `coverage()` overlap/disjoint units, AC-parser label-strip + section-boundary. Registered in `run-all.sh` (now 12 suites). `lib.test.sh` trust-score install set carries `analyze-gate.js`.
27
+
28
+ ## [6.13.0] - 2026-06-20 (v7 kernel, step 3 — enforce what you already declare: 3 missing primitives)
29
+
30
+ The v7 brief's loudest meta-theme is **"enforce, don't just declare"** — every static contract Qualia already computes should have a runtime gate. This batch wires the three primitives the research flagged as genuinely missing (R1, R2 + the slop-gate quick win), turning prose instructions and plan-time checks into deterministic, hook-governed guardrails. No new dependencies; all 11 suites green.
31
+
32
+ ### Added — R1: runtime plan-contract file-scope guard (`hooks/task-write-guard.js`)
33
+ - `plan-contract.js` proves file-disjointness across parallel tasks at **plan** time, but nothing stopped a builder writing outside its declared set at **run** time — the documented #1 cross-wave-conflict + AI-entropy vector. New PreToolUse `Edit|Write` hook closes it.
34
+ - **Scoped:** a no-op unless a build is in flight (≥1 `RUNNING` entry in `.agent-status/` — the R2 signal), so it never interferes with the orchestrator, verifier, or ordinary editing. During a build it **blocks any write to a path not declared by some task** in the active phase contract (`files_modify ∪ files_create`). `.planning/` and `.agent-status/` are always writable. **Fails open** on any error; OWNER escape `QUALIA_ALLOW_OUTSIDE_CONTRACT=1`.
35
+ - **Honest limitation (documented in-file):** Claude Code gives a stateless hook no task identity, so it enforces "declared by SOME task" not "by THIS task" — plan-time disjointness + the builder's `<wave_context>` prompt cover the residual gap. Registered in `install.js` (`QUALIA_HOOK_SET` + the `Edit|Write` block); hook count 14 → 15.
36
+
37
+ ### Added — R2: machine-readable per-task status + wave fan-in barrier (`bin/agent-status.js`)
38
+ - `contract-runner.js` only checked exit codes; wave completion relied on the orchestrator LLM **reading** each builder's "DONE/BLOCKED/PARTIAL" prose. New helper persists each builder's outcome to `.agent-status/<task>.json` (`RUNNING | DONE | BLOCKED | PARTIAL` + commit hash) — the parallel-worktrees convention.
39
+ - CLI: `write` / `read` / `list` / `clear`, plus `barrier <contract.json> [--wave W]` which **exits 0 ⇔ every expected task is DONE** (a pollable barrier, not "did the model notice"). Task ids validated against `^T\d+$` (rejects path traversal). `buildActive()` export is the signal R1 keys off.
40
+ - `qualia-build`: builders now write `RUNNING` at start + `DONE/BLOCKED/PARTIAL` at end; the orchestrator runs the barrier after each wave (a `BLOCKED`/`PARTIAL` task holds the wave) and clears scratch at completion. Shipped into installed `bin/` via `runtime-manifest.js`.
41
+
42
+ ### Added — slop-detect wired into the verify + ship gates (quick win)
43
+ - The anti-slop scanner (`bin/slop-detect.mjs`) already existed and exits non-zero on CRITICAL design tells; this adds the **gate wiring** (same role as `migration-guard`/`branch-guard`). `pre-deploy-gate.js` re-runs it at `vercel --prod` time as the hard, non-bypassable block (CRITICAL → deploy refused). Skipped silently when the scanner isn't installed (brownfield/older installs); OWNER-only `QUALIA_SKIP_SLOP=1` escape mirrors `QUALIA_SKIP_LINT`. Surfaced in `qualia-verify` (CRITICAL = verification FAIL) and `qualia-ship` Quality Gates.
44
+
45
+ ### Tests
46
+ - `tests/agent-status.test.sh` (new, 24 cases): round-trip, validation/traversal rejection, wave + phase barriers, BLOCKED-holds, list/clear, `buildActive`. Registered in `run-all.sh`.
47
+ - `tests/hooks.test.sh`: +10 `task-write-guard` cases (idle no-op, declared allow, undeclared block, `files_create`, framework-path exemptions, escape hatch, absolute paths, quiet-after-DONE, fail-open) and +4 `pre-deploy-gate` anti-slop cases (gradient block, clean pass, OWNER-only skip, graceful skip when absent).
48
+ - Hook-count assertions bumped 14 → 15 (`install-smoke`, `bin`); `runner.js` install test corrected to 15; `lib.test.sh` trust-score install set carries `agent-status.js`.
49
+
50
+ ## [6.12.0] - 2026-06-20 (v7 kernel, step 2 — lifecycle: build → operate, the forced handoff is gone)
51
+
52
+ The v7.0-defining change the redesign brief called out as #1 (Section 6, "the change you raised"): a project that has **launched** should stop being a milestone-journey dragged to a Handoff, and become an **update stream**. This is the v7 thesis in miniature — a behavior hard-coded in prose ("the final milestone is always Handoff, never negotiable") is now a **branch on explicit state**.
53
+
54
+ ### Added — a `lifecycle: "build" | "operate"` dimension to the state machine
55
+ - **`bin/state.js`:** new projects default to `lifecycle: "build"` (unchanged behavior). New `state.js launch [--deployed-url U] [--source erp|manual]` flips a project to `"operate"`, stamping `launched_at` + `launch_source` (idempotent — relaunching is a no-op). This is the discrete, **ERP-drivable** "is_live" event (the ERP can call it when it detects a project is live), so "launched" is state, not a milestone the team must mislabel as handoff.
56
+ - **The forced-handoff funnel is now lifecycle-gated:** `checkPreconditions` requires `HANDOFF.md` for `handed_off` **only in `build` mode**. An `operate` project can complete without ever producing a handoff. (Build mode is byte-for-byte unchanged — verified by a regression test.)
57
+ - **`nextCommand` is lifecycle-aware:** in `operate`, a final-phase `verified(pass)` routes to `/qualia-update` (not `/qualia-polish → ship → handoff`); a `verified(pass)` on the last phase increments `lifetime.updates_completed` (the operate analogue of closing a milestone). `cmdCheck` now surfaces `lifecycle` + `launched_at`.
58
+ - **`/qualia-update` skill (new):** the operate-mode counterpart to `/qualia-milestone` — a lean plan → build → verify → ship loop with no milestone/handoff machinery. Added to `ACTIVE_SKILLS`.
59
+ - **ERP contract:** `bin/report-payload.js` now sends `lifecycle` (+ `launched_at`/`launch_source` when set) so the ERP counts updates vs milestones and stops expecting a handoff for a live product. `templates/journey.md` rule 3 demoted from "the final milestone is always Handoff, never negotiable" to a **build-mode convention**.
60
+ - **Tests:** 7 new `state.test.sh` cases (build default; launch→operate stamping + routing; idempotent launch; operate verified→`/qualia-update` + `updates_completed` bump; **build mode still requires HANDOFF.md**; operate mode allows `handed_off` without it). All 10 suites green (89 state cases).
61
+
62
+ > Verification evidence (6.10.0) stays fully in force in operate mode — an update's contract still runs and its evidence must be clean to PASS. Lifecycle relaxes the *handoff* requirement, never the *evidence* requirement.
63
+
64
+ ## [6.11.0] - 2026-06-20 (main-push: accountability instead of a block)
65
+
66
+ Owner policy change: an employee pushing to `main`/`master` is **no longer blocked** — it is **counted**. The framework records each employee main-push locally (per-employee running total) and reports it to the ERP as a policy-event the OWNER can see, plus a visible on-push notice. OWNER pushes are unaffected and silent.
67
+
68
+ ### Changed — `hooks/branch-guard.js` is now allow-and-record, never block
69
+ - Was: `fail()`/exit 2 (BLOCK) on any non-OWNER push to a protected branch. Now: detects the same protected-branch pushes (current branch **and** `<src>:main` refspec), records an `employee_main_push` event to `~/.claude/.main-push-events.json` (`{counts:{<actor>:{total}}, events:[…]}`, mode 0600), enqueues it to the ERP `/api/v1/policy-events` endpoint via `erp-retry.js` (idempotent, `client_report_id` = `QS-MAINPUSH-<actor>-<count>`), prints a non-blocking NOTICE, and **exits 0**. Mirrors the existing `fawzi-approval-guard` model. The hook no longer blocks on missing/malformed config either — it never blocks.
70
+ - **ERP side:** `docs/erp-contract.md` now documents the `employee_main_push` event type (stored by `(type, actor_code)` for a per-employee tally, `branch` field replaces `sample`).
71
+ - Docs updated to the new reality (`EMPLOYEE-QUICKSTART.md`, `docs/qualia-manual.html`, installer comments + the push hook's status message now reads "Recording branch activity").
72
+ - Tests rewritten (`tests/hooks.test.sh`): employee main-push → exit 0 + recorded + count increments + notice; refspec `:main` → recorded; feature-branch / OWNER / missing-config → allowed, not recorded. All 10 suites green.
73
+
74
+ > Trade-off (by owner decision): `branch-guard` is no longer a hard gate on `main`; "main is always deployable" now rests on review discipline + the visible per-employee tally rather than a block. `git-guardrails` still blocks genuinely destructive pushes (force-push to main, branch -D).
75
+
76
+ ## [6.10.0] - 2026-06-20 (v7 kernel, step 1 — verification evidence is no longer optional)
77
+
78
+ Closes the headline finding of the v7 redesign brief: **the verification gate could PASS with zero evidence.** This was real — verified on disk against 6.9.2. The fix is the first incremental step of the v7 "invariants in an enforceable kernel, not prose" thesis, shipped on the 6.x line (main stays deployable).
79
+
80
+ ### Fixed — a phase can no longer reach `verified(pass)` without machine evidence
81
+ - **Root cause:** the enforcement machinery already existed (`contract-runner.js`, `state.js checkMachineEvidence`), but it was *bypassable by omission*. `checkMachineEvidence` only engages when `phase-N-contract.json` exists (`state.js:1024` returned `{ok:true}` when absent), and nothing forced a contract — the `planned` precondition checked the plan file but never the contract. So the common no-contract path fell through to the prose verifier.
82
+ - **Structural fix (`bin/state.js`):** the `planned` precondition now requires a valid `phase-N-contract.json` (exists + parseable + non-empty `tasks[]`), failing `MISSING_CONTRACT`/`INVALID_CONTRACT` otherwise. Because every planned phase now has a contract, `checkMachineEvidence` always engages at the `verified` gate — machine evidence (`evidence/phase-N-contract-run.json` with `ok:true`) becomes mandatory for PASS. "I built it" is no longer sufficient; "the contract ran clean" is required. (Implements the 2026-05-22 harness audit's Finding 3.)
83
+ - **Defense-in-depth (`agents/verifier.md`):** the design-rubric instruction "Default to 3 unless evidence supports otherwise" — which *contradicted* `rules/grounding.md` ("Score without evidence = 0") — is replaced. An unevidenced dimension is now `INSUFFICIENT EVIDENCE` and scores 1 (FAIL). A verifier that runs no checks and writes "3" across the board now produces a FAIL, and `state.js:983` already refuses PASS on that literal.
84
+ - **Tests (`tests/state.test.sh`):** `make_valid_plan` now emits a contract + passing evidence by default so happy-path setups satisfy the new preconditions; the two cases that exercise *absence* (missing-evidence guard, `--require-contract` missing) strip them explicitly. All 10 suites green.
85
+
86
+ ### Removed — dead `qualia-discuss` skill directory
87
+ - The skill was already retired in `bin/command-surface.js` (folded into `/qualia-scope`), but its 222-line `SKILL.md` still shipped in the tarball. Deleted the directory; `RETIRED_SKILLS` continues to clean it from older installs.
88
+
89
+ ## [6.9.2] - 2026-06-20 (docs — visual field manual)
90
+
91
+ ### Added — `docs/qualia-manual.html`
92
+ - A self-contained, single-file introductory **Field Manual** in the Qualia house style (dark + teal `#00FFD1`), shipped in the npm package (`files`). It is the human-friendly visual companion to `docs/EMPLOYEE-QUICKSTART.md`: what the framework is, the plan→build→verify→ship→report loop, employee-mode install, the full command map grouped by purpose, a first-project walkthrough, the five hard rules, the credentials-and-who-issues-them table, and the `/qualia` "when you're lost" escape hatch. Content is grounded in the existing quickstart + skill set — nothing invented. Interactive but dependency-free: sticky scroll-spy nav, click-to-copy command chips, reveal-on-scroll, mobile-responsive, skip-link for a11y. `docs/EMPLOYEE-QUICKSTART.md` now links to it.
93
+
94
+ ## [6.9.1] - 2026-06-16 (fix — stale update banner)
95
+
96
+ ### Fixed — session-start showed a false "update available" banner after catching up
97
+ - `hooks/session-start.js` rendered the cached `.qualia-update-available.json` notice on every session without checking it against the installed version. Because `auto-update.js` only clears that file on its next (throttled) run, the window between an install and that run showed a stale "Current 6.8.1 → Latest 6.9.0" banner even though 6.9.0 was already installed. `maybeRenderUpdateBanner` now validates the notice against `.qualia-config.json` and self-clears (skips render) when the installed version is at or past the advertised `latest`. Regression covered by two new cases in `tests/hooks.test.sh` (stale self-clears; genuine notice preserved).
98
+
11
99
  ## [6.9.0] - 2026-06-13 (employee on-ramps — stack presets, employee-mode install, shared knowledge pull)
12
100
 
13
101
  Implements Part A of the 2026-06-13 restructure plan: turn the framework from an owner-only tool into something a new employee can take idea→deployed without tribal knowledge. **Additive only** — no skill, agent, hook, or rule was rewritten; these are on-ramps built *around* the existing loop.
@@ -176,7 +176,7 @@ If exit code is 1 (critical findings present), the phase FAILS. Quote the findin
176
176
 
177
177
  ### Step B — Design rubric scoring (9 dimensions)
178
178
 
179
- Apply `qualia-design/design-rubric.md`. Score 1-5 per dimension WITH evidence on the next line. Default to 3 unless evidence supports otherwise.
179
+ Apply `qualia-design/design-rubric.md`. Score 1-5 per dimension WITH cited `file:line` evidence in the Evidence column. **Do NOT default to a passing score.** A dimension you cannot back with evidence is `INSUFFICIENT EVIDENCE` and scores **1 (FAIL)** — per `rules/grounding.md` ("Score without evidence = 0"). A verifier that runs no checks and writes "3" across the board must produce a FAIL, not a PASS. Never write a score you cannot cite.
180
180
 
181
181
  Scoped by phase scope:
182
182
  - Component-only phase → score Typography, Color cohesion, States, Motion intent, Microcopy, Container depth, and Visual system & graphics when the component owns a primary visual (skip Layout originality and Spatial rhythm when those are page-level concerns)
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+ // agent-status.js — machine-readable per-task build status + fan-in barrier.
3
+ //
4
+ // The gap this closes: `qualia-build` spawns one subagent per task and the
5
+ // orchestrator LLM has to *read* each builder's "DONE/BLOCKED/PARTIAL" prose to
6
+ // know a wave finished. That's not a barrier — it's the model noticing. This
7
+ // helper persists each builder's outcome to `.agent-status/<task>.json` so wave
8
+ // completion gates on a deterministic, pollable signal (the parallel-worktrees
9
+ // convention) and drives a live wave view.
10
+ //
11
+ // Builder convention (see skills/qualia-build):
12
+ // 1. At task start: agent-status.js write <task> RUNNING --phase N --wave W
13
+ // 2. At task end: agent-status.js write <task> DONE --commit <hash>
14
+ // (or BLOCKED / PARTIAL with a --note)
15
+ // Orchestrator after spawning a wave:
16
+ // agent-status.js barrier <contract.json> --wave W # exit 0 ⇔ all DONE
17
+ //
18
+ // Zero npm dependencies. Library + tiny CLI.
19
+
20
+ const fs = require("fs");
21
+ const path = require("path");
22
+ const pc = require("./plan-contract.js");
23
+
24
+ const STATUSES = new Set(["RUNNING", "DONE", "BLOCKED", "PARTIAL"]);
25
+ const STATUS_DIR = ".agent-status";
26
+
27
+ function statusDir(root) {
28
+ return path.join(root, STATUS_DIR);
29
+ }
30
+
31
+ // Task ids match ^T\d+$ (plan-contract schema). Reject anything else so a bad
32
+ // arg can't write outside .agent-status/ via a traversal in the filename.
33
+ function isTaskId(task) {
34
+ return typeof task === "string" && /^T\d+$/.test(task);
35
+ }
36
+
37
+ function statusFile(root, task) {
38
+ return path.join(statusDir(root), `${task}.json`);
39
+ }
40
+
41
+ function writeStatus(root, entry) {
42
+ if (!isTaskId(entry.task)) throw new Error(`invalid task id: ${entry.task} (must match ^T\\d+$)`);
43
+ const status = String(entry.status || "").toUpperCase();
44
+ if (!STATUSES.has(status)) throw new Error(`invalid status: ${entry.status} (must be ${[...STATUSES].join("|")})`);
45
+ const dir = statusDir(root);
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ const record = {
48
+ task: entry.task,
49
+ status,
50
+ commit: entry.commit || null,
51
+ note: entry.note || null,
52
+ phase: entry.phase != null ? Number(entry.phase) : null,
53
+ wave: entry.wave != null ? Number(entry.wave) : null,
54
+ updated_at: entry.now || new Date().toISOString(),
55
+ };
56
+ fs.writeFileSync(statusFile(root, entry.task), JSON.stringify(record, null, 2) + "\n");
57
+ return record;
58
+ }
59
+
60
+ function readStatus(root, task) {
61
+ try {
62
+ return JSON.parse(fs.readFileSync(statusFile(root, task), "utf8"));
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function listStatuses(root) {
69
+ const dir = statusDir(root);
70
+ if (!fs.existsSync(dir)) return [];
71
+ const out = [];
72
+ for (const f of fs.readdirSync(dir)) {
73
+ if (!f.endsWith(".json")) continue;
74
+ try {
75
+ out.push(JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")));
76
+ } catch {}
77
+ }
78
+ out.sort((a, b) => String(a.task).localeCompare(String(b.task), undefined, { numeric: true }));
79
+ return out;
80
+ }
81
+
82
+ function clearStatuses(root) {
83
+ const dir = statusDir(root);
84
+ if (!fs.existsSync(dir)) return 0;
85
+ let n = 0;
86
+ for (const f of fs.readdirSync(dir)) {
87
+ if (f.endsWith(".json")) { fs.unlinkSync(path.join(dir, f)); n++; }
88
+ }
89
+ try { fs.rmdirSync(dir); } catch {}
90
+ return n;
91
+ }
92
+
93
+ // True when a build is in flight: at least one task is RUNNING. R1's pre-write
94
+ // guard keys off this so it only enforces file-discipline during a build.
95
+ function buildActive(root) {
96
+ return listStatuses(root).some((s) => s.status === "RUNNING");
97
+ }
98
+
99
+ function expectedTaskIds(contract, wave) {
100
+ const tasks = (contract && contract.tasks) || [];
101
+ const filtered = wave != null ? tasks.filter((t) => Number(t.wave) === Number(wave)) : tasks;
102
+ return filtered.map((t) => t.id);
103
+ }
104
+
105
+ // Fan-in barrier: compare the persisted statuses against the task ids the
106
+ // contract expects (optionally scoped to one wave). ok ⇔ every expected task is
107
+ // DONE. Anything else (missing/running/blocked/partial) holds the barrier.
108
+ function barrier(root, contract, opts = {}) {
109
+ const expected = expectedTaskIds(contract, opts.wave);
110
+ const byTask = new Map(listStatuses(root).map((s) => [s.task, s]));
111
+ const tasks = expected.map((id) => {
112
+ const s = byTask.get(id);
113
+ return { task: id, status: s ? s.status : "MISSING", commit: s ? s.commit : null, note: s ? s.note : null };
114
+ });
115
+ const count = (st) => tasks.filter((t) => t.status === st).length;
116
+ const done = count("DONE");
117
+ return {
118
+ ok: expected.length > 0 && done === expected.length,
119
+ wave: opts.wave != null ? Number(opts.wave) : null,
120
+ expected: expected.length,
121
+ done,
122
+ blocked: count("BLOCKED"),
123
+ partial: count("PARTIAL"),
124
+ running: count("RUNNING"),
125
+ missing: count("MISSING"),
126
+ tasks,
127
+ };
128
+ }
129
+
130
+ // ── CLI ───────────────────────────────────────────────────────────────
131
+ function parseFlags(argv, start) {
132
+ const flags = { _: [] };
133
+ for (let i = start; i < argv.length; i++) {
134
+ const a = argv[i];
135
+ if (a === "--json") flags.json = true;
136
+ else if (a === "--cwd") flags.cwd = argv[++i];
137
+ else if (a.startsWith("--cwd=")) flags.cwd = a.slice(6);
138
+ else if (a === "--wave") flags.wave = argv[++i];
139
+ else if (a.startsWith("--wave=")) flags.wave = a.slice(7);
140
+ else if (a === "--commit") flags.commit = argv[++i];
141
+ else if (a.startsWith("--commit=")) flags.commit = a.slice(9);
142
+ else if (a === "--note") flags.note = argv[++i];
143
+ else if (a.startsWith("--note=")) flags.note = a.slice(7);
144
+ else if (a === "--phase") flags.phase = argv[++i];
145
+ else if (a.startsWith("--phase=")) flags.phase = a.slice(8);
146
+ else flags._.push(a);
147
+ }
148
+ return flags;
149
+ }
150
+
151
+ function usage() {
152
+ console.error([
153
+ "Usage:",
154
+ " agent-status.js write <task> <status> [--commit H] [--note N] [--phase P] [--wave W] [--cwd DIR]",
155
+ " agent-status.js read <task> [--cwd DIR] [--json]",
156
+ " agent-status.js list [--cwd DIR] [--json]",
157
+ " agent-status.js barrier <contract.json> [--wave W] [--cwd DIR] [--json]",
158
+ " agent-status.js clear [--cwd DIR]",
159
+ "",
160
+ "status ∈ RUNNING | DONE | BLOCKED | PARTIAL",
161
+ "barrier exits 0 ⇔ every expected task is DONE.",
162
+ ].join("\n"));
163
+ }
164
+
165
+ function main(argv) {
166
+ const cmd = argv[2];
167
+ if (!cmd || cmd === "-h" || cmd === "--help") { usage(); return 2; }
168
+ const flags = parseFlags(argv, 3);
169
+ const root = path.resolve(flags.cwd || process.cwd());
170
+
171
+ if (cmd === "write") {
172
+ const [task, status] = flags._;
173
+ if (!task || !status) { usage(); return 2; }
174
+ try {
175
+ const rec = writeStatus(root, {
176
+ task, status, commit: flags.commit, note: flags.note, phase: flags.phase, wave: flags.wave,
177
+ });
178
+ if (flags.json) console.log(JSON.stringify(rec));
179
+ else console.log(`${rec.task} ${rec.status}${rec.commit ? ` @ ${rec.commit}` : ""}`);
180
+ return 0;
181
+ } catch (e) {
182
+ console.error(`ERROR: ${e.message}`);
183
+ return 2;
184
+ }
185
+ }
186
+
187
+ if (cmd === "read") {
188
+ const [task] = flags._;
189
+ if (!task) { usage(); return 2; }
190
+ const rec = readStatus(root, task);
191
+ if (!rec) { console.error(`no status for ${task}`); return 1; }
192
+ if (flags.json) console.log(JSON.stringify(rec));
193
+ else console.log(`${rec.task} ${rec.status}${rec.commit ? ` @ ${rec.commit}` : ""}${rec.note ? ` — ${rec.note}` : ""}`);
194
+ return 0;
195
+ }
196
+
197
+ if (cmd === "list") {
198
+ const all = listStatuses(root);
199
+ if (flags.json) { console.log(JSON.stringify(all, null, 2)); return 0; }
200
+ if (all.length === 0) { console.log("(no agent statuses)"); return 0; }
201
+ for (const s of all) console.log(`${s.task} ${s.status.padEnd(8)}${s.commit ? ` @ ${s.commit}` : ""}${s.note ? ` — ${s.note}` : ""}`);
202
+ return 0;
203
+ }
204
+
205
+ if (cmd === "barrier") {
206
+ const [contractPath] = flags._;
207
+ if (!contractPath) { usage(); return 2; }
208
+ const loaded = pc.readContractFile(contractPath);
209
+ if (!loaded.ok) {
210
+ if (flags.json) console.log(JSON.stringify({ ok: false, ...loaded }));
211
+ else console.error(`${loaded.error}: ${loaded.message}`);
212
+ return 2;
213
+ }
214
+ const result = barrier(root, loaded.contract, { wave: flags.wave });
215
+ if (flags.json) { console.log(JSON.stringify(result, null, 2)); return result.ok ? 0 : 1; }
216
+ const scope = result.wave != null ? `wave ${result.wave}` : "phase";
217
+ if (result.ok) {
218
+ console.log(`BARRIER PASS (${scope}): ${result.done}/${result.expected} DONE`);
219
+ } else {
220
+ console.error(`BARRIER HOLD (${scope}): ${result.done}/${result.expected} DONE` +
221
+ ` (running=${result.running} blocked=${result.blocked} partial=${result.partial} missing=${result.missing})`);
222
+ for (const t of result.tasks) if (t.status !== "DONE") console.error(` - ${t.task}: ${t.status}${t.note ? ` — ${t.note}` : ""}`);
223
+ }
224
+ return result.ok ? 0 : 1;
225
+ }
226
+
227
+ if (cmd === "clear") {
228
+ const n = clearStatuses(root);
229
+ console.log(`cleared ${n} status file(s)`);
230
+ return 0;
231
+ }
232
+
233
+ usage();
234
+ return 2;
235
+ }
236
+
237
+ module.exports = {
238
+ STATUSES,
239
+ STATUS_DIR,
240
+ writeStatus,
241
+ readStatus,
242
+ listStatuses,
243
+ clearStatuses,
244
+ buildActive,
245
+ expectedTaskIds,
246
+ barrier,
247
+ };
248
+
249
+ if (require.main === module) {
250
+ process.exit(main(process.argv));
251
+ }