qualia-framework 6.14.0 → 7.0.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.
Files changed (72) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +316 -0
  3. package/CLAUDE.md +3 -1
  4. package/agents/roadmapper.md +16 -14
  5. package/bin/agent-status.js +24 -11
  6. package/bin/batch-plan.js +111 -0
  7. package/bin/branch-hygiene.js +135 -0
  8. package/bin/command-surface.js +2 -0
  9. package/bin/compile-instructions.js +82 -0
  10. package/bin/design-tokens.js +131 -0
  11. package/bin/erp-event.js +177 -0
  12. package/bin/erp-retry.js +12 -1
  13. package/bin/eval-runner.js +218 -0
  14. package/bin/host-adapters.js +84 -12
  15. package/bin/install.js +44 -13
  16. package/bin/knowledge-flush.js +6 -3
  17. package/bin/last-report.js +207 -0
  18. package/bin/project-sync.js +315 -0
  19. package/bin/recall.js +172 -0
  20. package/bin/repo-map.js +188 -0
  21. package/bin/runtime-manifest.js +12 -0
  22. package/bin/state.js +112 -1
  23. package/bin/vault-access.js +82 -0
  24. package/bin/verify-panel.js +294 -0
  25. package/bin/wave-plan.js +211 -0
  26. package/docs/erp-contract.md +180 -0
  27. package/mcp/memory-mcp/server.js +257 -0
  28. package/package.json +6 -3
  29. package/qualia-design/design-dials.md +72 -0
  30. package/qualia-design/design-reference.md +24 -0
  31. package/rules/access.md +42 -0
  32. package/rules/codex-goal.md +28 -26
  33. package/rules/infrastructure.md +1 -1
  34. package/skills/qualia/SKILL.md +6 -0
  35. package/skills/qualia-build/SKILL.md +43 -9
  36. package/skills/qualia-eval/SKILL.md +83 -0
  37. package/skills/qualia-feature/SKILL.md +20 -4
  38. package/skills/qualia-fix/SKILL.md +13 -1
  39. package/skills/qualia-map/SKILL.md +15 -0
  40. package/skills/qualia-milestone/SKILL.md +12 -6
  41. package/skills/qualia-new/REFERENCE.md +6 -4
  42. package/skills/qualia-new/SKILL.md +41 -15
  43. package/skills/qualia-plan/SKILL.md +2 -2
  44. package/skills/qualia-polish/SKILL.md +3 -2
  45. package/skills/qualia-recall/SKILL.md +76 -0
  46. package/skills/qualia-report/SKILL.md +10 -0
  47. package/skills/qualia-scope/SKILL.md +3 -3
  48. package/skills/qualia-ship/SKILL.md +34 -4
  49. package/skills/qualia-update/SKILL.md +4 -0
  50. package/skills/qualia-verify/SKILL.md +53 -24
  51. package/templates/DESIGN.md +15 -0
  52. package/templates/instructions.md +32 -0
  53. package/templates/journey.md +1 -1
  54. package/templates/project-discovery.md +30 -23
  55. package/templates/requirements.md +7 -7
  56. package/tests/agent-status.test.sh +15 -0
  57. package/tests/batch-plan.test.sh +56 -0
  58. package/tests/branch-hygiene.test.sh +93 -0
  59. package/tests/design-tokens.test.sh +53 -0
  60. package/tests/erp-event.test.sh +78 -0
  61. package/tests/eval-runner.test.sh +147 -0
  62. package/tests/instructions.test.sh +109 -0
  63. package/tests/last-report.test.sh +156 -0
  64. package/tests/lib.test.sh +29 -4
  65. package/tests/project-sync.test.sh +175 -0
  66. package/tests/recall.test.sh +91 -0
  67. package/tests/repo-map.test.sh +70 -0
  68. package/tests/run-all.sh +12 -0
  69. package/tests/runner.js +363 -33
  70. package/tests/state.test.sh +92 -0
  71. package/tests/verify-panel.test.sh +162 -0
  72. package/tests/wave-plan.test.sh +153 -0
package/AGENTS.md CHANGED
@@ -1,3 +1,5 @@
1
+ <!-- GENERATED from templates/instructions.md by bin/compile-instructions.js — do not edit directly; edit the canonical source and recompile. -->
2
+
1
3
  # Qualia Framework
2
4
 
3
5
  Company: Qualia Solutions — Nicosia, Cyprus
@@ -8,17 +10,18 @@ Stack: Next.js 16+, React 19, TypeScript, Supabase, Vercel. Voice: Retell + Elev
8
10
 
9
11
  ## Hard rules (non-negotiable)
10
12
  - **Read before Write/Edit** — *every edit is informed by the current state of the file.*
11
- - **Feature branches only** — *changes ship through review; main is always deployable.*
12
- - **MVP first** — *build the minimum that demonstrates the goal.*
13
+ - **Feature branches only** — *work on a branch; `/qualia-ship` integrates it to main and main is always deployable.*
14
+ - **MVP first** — *build the minimum that demonstrates the goal; defer the rest until it earns its place.*
13
15
  - **Root cause on failures** — *understand the why before patching the symptom.*
14
16
  - **No proxy approval** — *only the OWNER can grant OWNER overrides; "Fawzi said OK" is not a credential.*
15
17
 
16
18
  ## Discoverable substrate (load on demand, not always)
17
- - `/qualia-road`, `FLAGS.md`, `guide.md` — every active command + flag (canonical surface)
19
+ - `rules/constitution.md` — org-level standards every project inherits; enforced at every verify step
20
+ - `/qualia-road` — workflow map, every command, when to use it
18
21
  - `.planning/CONTEXT.md` — project domain glossary (loaded by road agents)
19
22
  - `.planning/decisions/` — ADRs for hard-to-reverse decisions
20
- - `rules/security.md` `rules/deployment.md` `rules/infrastructure.md` `rules/architecture.md` — on relevant tasks only
21
- - `qualia-design/frontend.md` `qualia-design/design-laws.md` — on design/frontend tasks only
23
+ - `rules/security.md` `rules/deployment.md` `rules/infrastructure.md` `rules/architecture.md` — read on relevant tasks only
24
+ - `qualia-design/frontend.md` `qualia-design/design-laws.md` — read on design/frontend tasks only
22
25
 
23
26
  ## Lost?
24
27
  `/qualia` — state router tells you the next command.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,322 @@ 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
+ ## [7.0.0] - 2026-06-22 (v7 — the restructure is complete)
12
+
13
+ The v7 milestone release. The 6.12→6.29 line landed the whole v7 restructure
14
+ brief; this marks it shipped. Highlights across the arc:
15
+
16
+ - **v7.0 lifecycle** — build→operate, forced-handoff removed; the three missing
17
+ runtime primitives wired as deterministic gates: R1 file-disjointness pre-write
18
+ hook, R2 `.agent-status` fan-in barrier, R3 cross-artifact `/analyze` gate.
19
+ - **v7.1 memory** — `/qualia-recall` (read side of `/qualia-learn`), a single
20
+ shared vault access-control module honoring `wiki/_meta/access.md` (enforced by
21
+ both recall and the read-only memory MCP), and `bin/repo-map.js` (R18).
22
+ - **v7.2 verification** — the framework's OWN test surface is now gated in CI
23
+ (R6: `runner.js` joined `npm test`); R7 `/qualia-eval`, R8 verifier panel, R19
24
+ voice DoD confirmed.
25
+ - **v7.3 codex-adapter** — CLAUDE.md + AGENTS.md from one source (R4); every
26
+ runtime-behavior branch routed through `host-adapters.js` (R5).
27
+ - **Design + scale** — tunable design dials + ban→do-instead pre-flight (R9),
28
+ per-client token registry (R10), `/qualia-build --batch` migration lane (R20).
29
+ - **ERP event integration (R14/R12)** — one signed, append-only `framework_events`
30
+ log: the framework signs and emits lifecycle events; the ERP verifies (HMAC
31
+ byte-matched across repos) and renders a live run-tree. Live in production.
32
+
33
+ Test surface at release: 24 shell suites + 179 cross-platform node tests, both
34
+ gated by `npm test`. No functional change in this bump — it tags the milestone.
35
+
36
+ ## [6.29.0] - 2026-06-22 (R14 client — the framework now emits signed events to the ERP)
37
+
38
+ The ERP side of R14 (the `framework_events` table + `/api/v1/events` +
39
+ `/api/v1/policy-events`) shipped in the `qualia-erp` repo and is live in
40
+ production. This is the framework EMIT side that closes the loop.
41
+
42
+ ### Added — `bin/erp-event.js`: signed lifecycle-event emitter
43
+ - Builds a `FrameworkEvent` envelope and queues it for `POST /api/v1/events`.
44
+ HMAC-SHA256 keyed by the install's `qlt_` token over `${id}.${timestamp}.${body}`
45
+ — **byte-identical** to the ERP's `lib/framework-events.ts` verifier (tested as
46
+ a cross-repo contract), so signatures verify with no extra secret. Headers
47
+ carry id/timestamp/signature (Standard-Webhooks shape); the event is the body.
48
+ - Non-blocking + graceful: ERP disabled or no API key ⇒ silent no-op; unsigned
49
+ posts still authenticate via Bearer (recorded `signature_valid=false`).
50
+ `emit <action> [--target type:ref] [--project uuid] [--meta k=v] [--json]
51
+ [--dry-run]`, plus an `emitEvent()` export for in-process callers.
52
+
53
+ ### Changed
54
+ - `bin/erp-retry.js`: the retry queue now carries optional per-item `headers`
55
+ (so the signed-event envelope rides the existing idempotent retry rails);
56
+ Auth/Content-* stay framework-owned and non-overridable.
57
+ - `/qualia-verify`: emits `verify_pass` / `verify_fail` after the state
58
+ transition — the first live lifecycle signal into the ERP run-tree. Other
59
+ points (`session_started`, `phase_planned`, `build_wave_started`) documented.
60
+ - `docs/erp-contract.md`: new "POST /api/v1/events" section (envelope, headers,
61
+ signing).
62
+ - `tests/erp-event.test.sh` (14 cases incl. the cross-repo signature match);
63
+ run-all now **24 suites**, all green (+179 node).
64
+
65
+ ## [6.28.0] - 2026-06-22 (R20 — the migration lane for /qualia-build)
66
+
67
+ ### Added — `bin/batch-plan.js` + `/qualia-build --batch`
68
+ - The wave model fits a phase; it doesn't fit a 50–500-file codemod. `--batch` is
69
+ a map-reduce over a flat file list: `batch-plan.js` splits it into
70
+ FILE-DISJOINT batches (each a worktree-isolated worker with tiny context that
71
+ opens its own commit) grouped into concurrency-capped waves, fanned in through
72
+ ONE staging branch. Deterministic split (dedup, complete, disjoint, waves ≤
73
+ max-workers); the orchestration lives in the skill.
74
+ - `--from FILE`/stdin, `--batch-size`, `--max-workers`, `--staging`, `--json`.
75
+ Zero-dep. `/qualia-build --batch` documents the full orchestration (staging
76
+ branch → per-wave worktree workers → fan-in → test once → ship), including
77
+ "log any dropped batch — no silent partial migrations."
78
+ - `tests/batch-plan.test.sh` (15); run-all now **23 suites**, all green (+179 node).
79
+
80
+ ## [6.27.0] - 2026-06-22 (R9 + R10 — design dials, ban→do-instead pre-flight, per-client token registry)
81
+
82
+ Two framework-area design enhancements: steer generation away from slop upfront,
83
+ and ground every builder in a real per-client design system.
84
+
85
+ ### Added — R9: tunable design dials + generation-time pre-flight
86
+ - `qualia-design/design-dials.md`: DESIGN_VARIANCE / MOTION_INTENSITY /
87
+ VISUAL_DENSITY (LOW|MED|HIGH) with concrete mappings + register/project-type
88
+ gating, plus a ban→do-instead table mirroring slop-detect's rules WITH the
89
+ positive alternative (avoids the pink-elephant backfire). The generation-time
90
+ half of the anti-slop contract; slop-detect remains the post-hoc gate.
91
+ - `templates/DESIGN.md` §0 declares the dials; `/qualia-polish` Stage 0 resolves
92
+ them and cites the bans before editing.
93
+
94
+ ### Added — R10: per-client design-token registry
95
+ - `bin/design-tokens.js`: compiles a client brand spec (`design-tokens.json`)
96
+ into a CSS-variable registry (`tokens.css`). `/qualia-new` scaffolds it;
97
+ builders reference `var(--…)`, never a literal hex. The shadcn-registry
98
+ pattern — first-party brand data is the escape from design monoculture.
99
+ - `tests/design-tokens.test.sh` (14) + the dials contract test in lib.test.sh;
100
+ run-all now **22 suites**, all green (+ 179 node).
101
+
102
+ ## [6.26.0] - 2026-06-22 (R18 — a cheap symbol map for brownfield onboarding)
103
+
104
+ ### Added — `bin/repo-map.js`, wired into `/qualia-map`
105
+ - Aider's repo-map idea, in the framework's zero-dep idiom: a deterministic
106
+ symbol map of a codebase (top-level exports/functions/classes/types per file,
107
+ busiest files first) via language-aware regexes for JS/TS, Python, Go, Rust,
108
+ Ruby, Java, PHP. NO tree-sitter native dependency — the brief named tree-sitter
109
+ but that fights the framework's zero-dep posture; regex extraction gets the
110
+ "what's in this repo and where" signal at zero cost.
111
+ - `/qualia-map` now runs it BEFORE the mapper agents (`repo-map.json`), so they
112
+ onboard from a structural index instead of reading the tree blind — cheaper,
113
+ better-grounded brownfield scans. `--json`, `--max-files`; skips
114
+ node_modules/dist/.git/etc.; exit 2 on bad dir. Registered in
115
+ runtime-manifest + lib install set; `tests/repo-map.test.sh` (14 cases).
116
+ run-all now **21 suites**.
117
+
118
+ ## [6.25.0] - 2026-06-21 (v7.3 codex-adapter — the last runtime branch moves into the seam)
119
+
120
+ v7.3 is complete. R4 (emit AGENTS.md alongside CLAUDE.md from one canonical
121
+ source) was already shipped via `bin/compile-instructions.js` — both files
122
+ compile from `templates/instructions.md` through the host adapter, with a
123
+ `--check` drift guard gated by the `instructions` suite. This release finishes R5.
124
+
125
+ ### Changed — R5: agent-CLI invocation is now an adapter fact (no more stray runtime branch)
126
+ - `host-adapters.js` was already the real seam (declarative `HOSTS`, per-host
127
+ paths/naming/instruction-file). The one genuine `if (codex)` behavior branch
128
+ still living outside it was in `knowledge-flush.js`: it hardcoded
129
+ `codex`/`claude` as the CLI and `exec`/`-p` as the invocation. Those are
130
+ per-host FACTS — moved into the adapter: `agentCli` + `agentExecPrefix`
131
+ (declarative) → `adapter().agentExec(prompt)` (computed argv), plus
132
+ `hostForHome(home)` so the home→host mapping has one owner.
133
+ - `knowledge-flush.js` now asks the adapter instead of branching. When a third
134
+ runtime arrives it's one `HOSTS` entry, not a grep-and-patch. lib.test.sh
135
+ covers the new facts.
136
+ - Left intentionally: the `qualiaHome()` install-root idiom repeated across
137
+ bins/hooks treats both hosts IDENTICALLY (it's home resolution, not a behavior
138
+ branch) — sweeping 20 files for a no-op-difference helper is high-risk,
139
+ low-value, and would touch hot paths. Noted, not done.
140
+
141
+ ## [6.24.0] - 2026-06-21 (v7.2 verification — gate the framework's own test surface)
142
+
143
+ v7.2 verification is complete. R7 (`/qualia-eval` lane), R8 (verify-panel +
144
+ adversarial skeptics), and R19 (voice-agent archetype DoD) shipped earlier and
145
+ are confirmed green. This release closes R6.
146
+
147
+ ### Fixed — R6: the framework's own checks can no longer silently regress
148
+ - The cross-platform `tests/runner.js` harness (179 node:test cases, incl. the
149
+ memory-MCP suite) was **never run by `npm test`** — only the shell suite was.
150
+ That is exactly why it silently drifted to 21 failures: nothing ran it. Now
151
+ `npm test` runs BOTH (`test:shell && test:node`), so CI (`run: npm test`)
152
+ gates the full surface — 20 shell suites + 179 node tests. Added a `test:node`
153
+ script. Verified CI-safe (179/179 with an empty HOME, no install required).
154
+ - This is R6's real intent for a deterministic/offline framework: a machine gate
155
+ on the framework's OWN checks. The LLM-prompt-level eval of skills is out of
156
+ scope for offline CI; `/qualia-eval` (R7) already provides that lane for the AI
157
+ artifacts projects build.
158
+
159
+ ## [6.23.0] - 2026-06-21 (v7.1 memory loop — recall, vault access control, harness health)
160
+
161
+ Completes the v7.1 memory work: the read side now exists, the vault has real
162
+ access control, and the cross-platform test harness is green again.
163
+
164
+ ### Added — `/qualia-recall` + `bin/recall.js`: the read side of memory
165
+ - Kills the `/qualia-recall` ghost (referenced in `agents/researcher.md` and
166
+ `skills/qualia-research` but never built). `recall.js` unifies BOTH stores in
167
+ one ranked digest: the knowledge layer (delegates to `knowledge.js` so new
168
+ files stay discoverable) + the qualia-memory vault (grep over `wiki/`).
169
+ `--json`, `--scope all|knowledge|vault`, `--max`. Zero deps. Registered in
170
+ `command-surface` ACTIVE_SKILLS + `runtime-manifest`.
171
+
172
+ ### Added — vault access control (`bin/vault-access.js`, `rules/access.md`)
173
+ - The vault's `wiki/_meta/access.md` manifest named `/qualia-recall` as an
174
+ enforcer, referenced an enforcing rule at `~/.claude/rules/access.md`, and
175
+ expected readers to honor OWNER_ONLY / CONDITIONAL paths — but **none of those
176
+ existed**. Now: `vault-access.js` is the single implementation (parse manifest,
177
+ resolve role, deny non-OWNER reads of OWNER_ONLY/CONDITIONAL paths,
178
+ fail-closed on unknown role); `rules/access.md` is the always-on enforcing
179
+ rule; **both** `recall.js` and the always-on memory MCP enforce it through that
180
+ one module so the security rule can't drift. No-manifest ⇒ no filtering (the
181
+ wiki is curated-by-design; sensitive stores live outside `wiki/`).
182
+
183
+ ### Fixed — test harness health
184
+ - `tests/runner.js` (cross-platform node:test harness) had drifted to 154/21;
185
+ re-synced to current bin/hook contracts (MISSING_CONTRACT/evidence,
186
+ SUSPICIOUS_NAME, set-erp-key stdin, branch-guard v6.10 allowed+recorded) with
187
+ no assertions weakened. Now **179**, with the new memory-MCP access cases.
188
+ - `tests/recall.test.sh` (18 cases) added; `run-all` now **20 suites**, all green.
189
+
190
+ ### Fixed — vault docs (qualia-memory repo)
191
+ - `CLAUDE.md` named a non-existent `flush.py` for session capture (the real
192
+ mechanism is the claude-memory-compiler SessionEnd hook) and carried a stale
193
+ `/home/qualia-new/` path. Corrected. No framework write-back leg was built:
194
+ the vault's session loop is external and already exists; the framework's
195
+ knowledge tier is separate by design.
196
+
197
+ ## [6.22.0] - 2026-06-21 (session continuity + ERP project-sync — built by two parallel worktree agents)
198
+
199
+ Two independent continuity features, built concurrently in isolated git worktrees and integrated together.
200
+
201
+ ### Added — B1: `/qualia` surfaces the latest session report at session start (`bin/last-report.js`)
202
+ - Finds the newest `.planning/reports/report-*.md` (filename date desc, mtime tiebreak) and extracts a tight digest: `{ found, file, date, summary, next, age_days }` — summary from the report's `## What Was Done`, next-step from `## Next Steps`, markdown-flattened and capped. `--json`, `--cwd`, `--now ISO` (deterministic age); exit 0 found / 1 none / 2 bad input.
203
+ - Wired into the `/qualia` router "Get State" step: when a project is loaded, the router prints the last-session digest at the TOP of its output, so the operator — or a teammate picking the project up — instantly sees where work was left off. `tests/last-report.test.sh` (28 assertions).
204
+
205
+ ### Added — B2: full project-sync reconciliation payload for the ERP (`bin/project-sync.js`)
206
+ - A single deterministic snapshot the ERP can reconcile a whole project from: identity + lifecycle/launched_at, `milestones[]` (closed/current/future + per-milestone REQ-ID completion + phases/tasks/deployed_url), current position, `task_rollup`, `accountability` (offroad), `integration` (the trunk-merge model), and a `schema_version`. **Composes** `project-snapshot.js` (reuses its builders) rather than duplicating or bloating that stable endpoint. `--json`/`--write`/`--pretty`; read-only; graceful on missing JOURNEY/REQUIREMENTS. `tests/project-sync.test.sh` (38 assertions).
207
+ - `docs/erp-contract.md`: new "Project Sync Payload" section — every field, the server-side reconciliation steps, and the PR/merge model (branch → main at ship → deploy; main-push accountability). Explicit **Framework-emits vs ERP-backend-ingests** split.
208
+ - **Backend remains (out of this repo):** a `POST /api/v1/project-sync` endpoint + server reconciliation (upsert milestones by num, completion from REQ counts, roll up phases/tasks, store offroad, encode the merge model). The framework emits + documents; it does not POST yet (the ERP team mirrors `project-snapshot.js`'s upload plumbing once the endpoint exists).
209
+
210
+ run-all now 19 suites; both bins in the manifest + install-set; all suites green.
211
+
212
+ ## [6.21.0] - 2026-06-21 (work-unit goals on both runtimes — Codex /goal + the Claude Code equivalent)
213
+
214
+ Every defined unit of work should declare one objective + one budget, so it stays anchored and the operator sees burn-vs-budget. The framework had this for Codex (`/goal`) but `rules/codex-goal.md` explicitly told Claude Code to "skip — no equivalent surface." Claude Code DOES have an equivalent (the session task-list + turn budget); this wires it up and broadens goal-setting to every work-unit skill.
215
+
216
+ ### Changed — `rules/codex-goal.md` is now a both-runtimes "work-unit goal" rule
217
+ - One shared helper (`codex-goal.js {scope}`, via the host-adapter-rendered `${QUALIA_BIN}`) produces the objective + token budget from STATE.md/ROADMAP.md.
218
+ - **Codex** path unchanged: native `/goal` / `update_goal`.
219
+ - **Claude Code** path (new): set the goal via the harness **task-list** (a tracked task titled with the objective, in_progress→completed) + state the budget in the banner. Same discipline — one named objective + budget per unit — native surface on each runtime.
220
+
221
+ ### Changed — goal-setting wired into every work-unit skill
222
+ - Existing blocks in `/qualia-plan`, `/qualia-build`, `/qualia-feature` relabeled from "Codex goal (Codex runtime only)" to runtime-neutral **"Set the work-unit goal."**
223
+ - Added to `/qualia-fix` (scope `quick`/`feature`) and `/qualia-update` (scope `feature` — it runs its own lean loop without `/qualia-plan`, so it needs its own goal). `/qualia-milestone` deliberately omitted — it routes into `/qualia-plan`, which sets the goal (no double-set).
224
+
225
+ No bin or schema change; all 17 suites pass.
226
+
227
+ ## [6.20.0] - 2026-06-21 (scope integrity — the roadmap finishes the project, and the team can't drift off it)
228
+
229
+ The deepest fix this cycle. Teams were drifting off-plan — inventing milestones, building features with no link to the roadmap — and the root cause was upstream: **`/qualia-new` under-scoped the project** (a v1 slice capped at 5 milestones, overflow dumped into an unplanned "v2"), so the agreed work literally wasn't in the arc and the team was *forced* to improvise. Two layers: make genesis cover the whole project, then bind the team to it. (Layer 1 shipped in the prior commit; this entry covers the full feature.)
230
+
231
+ ### Layer 1 — genesis covers the whole project (commit `feat(genesis)`)
232
+ - **Interview reworked** (`templates/project-discovery.md`): added §9 **capability inventory** (every capability needed for DONE — the whole thing) + §10 **whole-project definition-of-done**; dropped the old §9 "stop at 3–5 chapters" self-cap. Full path 14 → 15 questions, refocused from brand-vibe to functional completeness.
233
+ - **Milestone cap removed** (`agents/roadmapper.md`, `templates/journey.md`, `templates/requirements.md`): the arc spans until the §9 inventory reaches the §10 done-state — as many milestones as needed. `Post-Handoff`/`Out of Scope` holds ONLY explicit client deferrals (§8), never overflow. Handoff optional for internal/ongoing products.
234
+ - **Coverage gate** (`/qualia-new` Step 14): genesis refuses to present a journey that leaves any §9 capability unmapped (0 unmapped before the approval ladder).
235
+
236
+ ### Layer 2 — bind the team to the arc (this commit)
237
+ - **Milestone close gates on requirements** (`bin/state.js`): new `MILESTONE_REQS_INCOMPLETE` — close refuses (strict) / warns (standard) when a REQ-ID mapped to the milestone in REQUIREMENTS.md isn't `Complete`. Stops "finishing a milestone with scope still open". New `state.js reqs-check [--milestone N]` exposes the same check (exit 0/1) for `/qualia-milestone` to show coverage before closing.
238
+ - **Off-road work is recorded, not silent** (`bin/state.js` note path): `transition --to note` gains `--scope in|off --ref {REQ/why}`. Off-road work increments `lifetime.offroad_count` and appends to an `offroad[]` ledger (OWNER + ERP visible), mirroring branch-guard's accountability model.
239
+ - **`/qualia-feature` + `/qualia-fix` scope gate**: before building, both check the active milestone. In-scope → proceed, tagged `--scope in`. Off-road → **strict blocks** (route to `/qualia-scope`/`/qualia-milestone` to fold it into the arc) / **standard records** (`--scope off`, counted). The drift vector the user named is now governed at the source.
240
+
241
+ ### Tests
242
+ - `tests/state.test.sh`: +5 cases — `reqs-check` (complete/incomplete/milestone-filter/untracked), `--scope off` tally + ledger, `--scope in` no-op, `--force` bypass. 96 state assertions green; all 17 suites pass. (Genesis is prose/templates — validated by the skills + refs suites.)
243
+
244
+ ## [6.19.0] - 2026-06-21 (trunk integration — ship is the merge point, report sweeps)
245
+
246
+ Fixes a real lifecycle gap + doc drift: **no skill ever integrated feature → main.** Branches/PRs accumulated with nothing closing them; `/qualia-ship` deployed *from the feature branch* and said "never push to main," so production ran branch code while `main` lagged ("main is always deployable" was false in practice); and three sources disagreed on the policy (the hard rule said "through review", `branch-guard` 6.10 said "accountability not block", `infrastructure.md` still claimed PR-review was enforced). This completes the 6.10 "accountability over block" turn into a coherent trunk model.
247
+
248
+ ### Changed — `/qualia-ship` integrates to main, deploys from main, closes the branch
249
+ - New §3: commit → fast-forward-integrate the feature branch into `main` (auto-rebase if `main` moved; STOP on conflict) → push. `branch-guard` records the main push (accountability). §4 deploys from `main` HEAD, so the deployed artifact == `main` byte-for-byte. New §4b deletes the integrated branch on a verified deploy. The normal path now leaves **zero lingering branches/PRs**.
250
+
251
+ ### Added — `bin/branch-hygiene.js` + `/qualia-report` sweep (the safety net)
252
+ - `branch-hygiene.js`: read-only clock-out sweep — finds local branches with commits **ahead of `main` that were never shipped** (stranded work) and **stale open PRs** (best-effort via `gh`, skipped when absent). Exit 0 clean / 1 found / 2 not-a-repo; `--json`; library `analyze`. Detects `main` or `master` as base.
253
+ - `/qualia-report` Step 5b runs it so stranded work surfaces to the employee + OWNER at clock-out instead of rotting.
254
+
255
+ ### Fixed — policy drift now single-voiced
256
+ - `rules/infrastructure.md`: the stale "main requires PR reviews (enforced by guards)" line replaced with the real model (integrate-at-ship; main pushes allowed + recorded; report sweeps; keep GitHub branch protection off, or switch ship to an auto-merged PR if you re-enable it).
257
+ - Canonical hard rule (`templates/instructions.md` → recompiled `CLAUDE.md`/`AGENTS.md`): "ship through review" → "`/qualia-ship` integrates it to main." Drift guard green.
258
+
259
+ ### Tests
260
+ - `tests/branch-hygiene.test.sh` (new, 13 cases): not-a-repo, clean, stranded branch (ahead count + json), ff-merged-no-longer-stranded, `master` base detection, `analyze()` lib. run-all now 17 suites; manifest + `lib.test.sh` install set updated.
261
+
262
+ ## [6.18.0] - 2026-06-21 (v7 kernel, step 8 — R7: /qualia-eval lane for AI features)
263
+
264
+ Qualia gates UI and code — `contract-runner` proves the code exists, `verify-panel` proves it's correct — but it had **no gate for the AI artifacts a project builds**. "The chatbot answers the refund question" / "the RAG answer is grounded" / "the agent stays under 2s" is not checkable by a grep. R7 adds the equivalent gate, layered: cheap deterministic assertions first, model judgment only where a model is required.
265
+
266
+ ### Added — `bin/eval-runner.js` (layered assertion runner, zero-dependency)
267
+ - Runs an eval suite (JSON — no YAML parser pulled in) of cases against captured AI outputs. **Deterministic assertion types** settled with no model: `contains`, `not_contains`, `equals`, `regex`, `not_regex`, `min_length`, `max_length`, `json_valid`, `json_path` (`equals`/`contains`), `max_latency_ms`, `max_cost_usd`. Outputs inline or via `output_file`.
268
+ - **`llm_rubric`** is the only model-dependent type — it carries a `verdict` (pass|fail) the skill fills by spawning a judge BEFORE the runner (same pattern `verify-panel` uses for skeptic votes). An unjudged rubric is PENDING and **fails** the suite — never a silent pass. Asserting a latency/cost budget with no metric recorded also fails (no silent pass).
269
+ - Exit 0 = all cases pass, 1 = failure/unjudged, 2 = bad input. `--write` emits `.planning/evals/eval-{feature}.json`. Library exports `run`, `runAssertion`, `getPath`.
270
+
271
+ ### Added — `/qualia-eval` skill (new active surface)
272
+ - The lane: capture the AI feature's real outputs → spawn one judge per `llm_rubric` (reusing the `qualia-verifier` agent, role-anchored) → `eval-runner.js` settles deterministic assertions + folds in verdicts → gate. Usable standalone (`/qualia-eval suite.json`) or as a phase verify-step gate (`/qualia-eval {N}`), where a FAIL has the same standing as a failing contract. Registered in `command-surface.js` `ACTIVE_SKILLS`.
273
+
274
+ ### Tests
275
+ - `tests/eval-runner.test.sh` (new, 19 cases): deterministic pass/fail, latency budget (incl. missing-metric → fail), `json_valid`/`json_path`, `llm_rubric` pass/fail/pending, `output_file` resolution + graceful missing-file, `--write` artifact, `runAssertion`/`getPath` units, malformed→exit 2. run-all now 16 suites; manifest + `lib.test.sh` install set updated; `qualia-eval` passes the skill smoke + refs suites.
276
+
277
+ ## [6.17.0] - 2026-06-21 (v7 kernel, step 7 — R16: dependency-derived wave width + --parallel knob)
278
+
279
+ `/qualia-build` spawned EVERY task in a contract "wave" concurrently, with no cap — two failure modes at once: over-serialization (the planner's hand-numbered waves can be deeper than the dependency graph requires) and over-parallelization (a wide wave spawns 9 builders past the 3–5 sweet spot where coordination cost overwhelms the gain — the LangGraph `max_concurrency` lesson). R16 replaces orchestrator guesswork with a deterministic scheduler derived from the task DAG.
280
+
281
+ ### Added — `bin/wave-plan.js` (deterministic build scheduler, zero-dependency)
282
+ - Recomputes **minimal-depth waves** from `depends_on` (topological levels = maximal safe parallelism), then splits each level into **batches capped at `max_concurrency`**. Output is an ordered `batches[]` the orchestrator spawns one at a time. Same contract + cap → same schedule.
283
+ - `max_concurrency`: `--parallel N` → exactly N; **auto** (default) → 1 if <3 tasks ("don't parallelize tiny phases"), else 5.
284
+ - Flags **over-serialization** (a task whose declared wave is deeper than the DAG requires — the schedule runs it earlier) and wide-level capping. Cycle in the DAG → exit 1; library exports `deriveLevels`, `resolveConcurrency`, `plan`.
285
+
286
+ ### Changed — `/qualia-build` consumes the derived schedule
287
+ - **§2** now runs `wave-plan.js .planning/phase-{N}-contract.json [--parallel K] --json` and spawns the emitted batches in order (not the raw contract `wave` numbers, not all-at-once). New `--parallel K` usage knob.
288
+ - **Batch fan-in barrier:** `agent-status.js` (R2) gains a `barrier --tasks T1,T2` mode that gates on an explicit batch (no contract needed) — required because derived waves needn't match the contract's declared wave numbers, so the per-wave barrier would mismatch. The build now barriers per batch, keeping R16 + R2 coherent.
289
+
290
+ ### Tests
291
+ - `tests/wave-plan.test.sh` (new, 23 cases): chain/independent/tiny/diamond DAGs, auto vs `--parallel` cap, wide-level batching, over-serialization flag, cycle→exit 1, `deriveLevels`/`resolveConcurrency` units. `tests/agent-status.test.sh` +3 `barrier --tasks` cases. run-all now 15 suites; manifest + `lib.test.sh` install set updated.
292
+
293
+ ## [6.16.0] - 2026-06-21 (v7 kernel, step 6 — R8: verifier panel + adversarial skeptics)
294
+
295
+ A single LLM judge is adversarially fragile — the literature puts a lone stray token at ~35% false positives, and self-grading bias hides ~70% of findings. `/qualia-verify` was a single cooperative verifier with an optional second pass. R8 replaces it with a **panel** (one verifier per lens) + **per-finding skeptics** (majority-survives), and — crucially — makes the SURVIVE/KILL and PASS/FAIL decision **deterministic math**, not another LLM judgment.
296
+
297
+ ### Added — `bin/verify-panel.js` (deterministic aggregator, zero-dependency)
298
+ - **`aggregate(panel)`**: dedupes findings across lenses (same `file:line:title` → one finding; highest severity wins, lenses union, votes sum), applies **majority-survives** (a finding is killed only when skeptics are a strict majority calling it not-real — ties and unvoted findings survive: unverified ≠ disproven), and computes category + per-lens scores via the **`rules/grounding.md` formula** (`5 − floor(weighted_sum/8)`). Verdict FAIL iff any surviving CRITICAL/HIGH. Exit 0 = PASS, 1 = FAIL.
299
+ - **`assemble <phase>`**: globs the per-lens `phase-{N}-panel-{lens}.json` files into one `phase-{N}-panel.json` skeleton (votes zeroed) so the orchestrator never hand-builds the panel.
300
+ - `--write` emits `.planning/phase-{N}-verification-panel.{json,md}`. Library exports (`aggregate`, `dedupeFindings`, `survives`, `scoreFromCounts`, `assemble`) for reuse.
301
+
302
+ ### Changed — `/qualia-verify` is now panel-based
303
+ - **§3 Panel:** spawns one `qualia-verifier` per *relevant* lens (correctness always; security/performance/design by what the phase touches — cost scales to risk, not a flat 4×), in parallel, each anchored on the same contract-run + harness-eval evidence as shared ground truth, each emitting structured findings JSON.
304
+ - **§3c Skeptics + aggregation:** assemble → 3 skeptics per CRITICAL/HIGH finding (5 with `--adversarial`/Handoff/security lens), each prompted to *refute* with evidence → tally votes → `verify-panel.js` produces the verdict. MEDIUM/LOW auto-survive (documented cost bound, not a silent cap). The old single-verifier + adversarial-second-pass sections are replaced.
305
+ - **§4:** the phase is PASS only if the panel verdict, harness-eval, AND anti-slop all agree. Reuses the existing `qualia-verifier` agent (lens/skeptic are prompt modes — no new agent registration).
306
+
307
+ ### Tests
308
+ - `tests/verify-panel.test.sh` (new, 28 cases): empty→PASS, surviving CRITICAL→FAIL, skeptic-killed→PASS, tie/no-vote survive, cross-lens dedupe (severity-max + vote-sum + lens-union), grounding-formula scores, MEDIUM/LOW-only→PASS, `--write` artifacts, `assemble` round-trip, malformed→exit 2. Registered in `run-all.sh` (now 14 suites); `lib.test.sh` trust-score install set carries `verify-panel.js`.
309
+
310
+ ## [6.15.0] - 2026-06-21 (v7 kernel, step 5 — R4+R5: single-source the dual-runtime surface)
311
+
312
+ Dual-runtime drift is the #1 risk of supporting both Claude Code and Codex. `CLAUDE.md` and `AGENTS.md` were hand-maintained twins — and they *had already drifted* (the MVP-first line and the substrate list differed between them). This batch makes that class of bug unmergeable: one canonical source, compiled per host, with a drift guard in CI.
313
+
314
+ ### Added — R4: one canonical instruction source, compiled to both files
315
+ - **`templates/instructions.md`** is now the single source of truth. `CLAUDE.md` and `AGENTS.md` are **generated artifacts** (committed, like a lockfile) carrying a `GENERATED` header.
316
+ - **`bin/compile-instructions.js`** compiles the canonical into both files. `--check` mode is the **drift guard**: it exits non-zero if either committed file is stale, making "edited one twin, forgot the other" impossible to merge. Wired into the test suite + an `npm run compile:instructions` script.
317
+ - Host-specific content uses conditional blocks (`<!--QUALIA-HOST claude-->…<!--/QUALIA-HOST-->`): the Claude file keeps the Pocock budget note, the Codex file keeps the cross-vendor (Cursor/Continue/Aider/Devin) note — the **body is byte-identical**, only the footer differs. The pre-existing drift is resolved (AGENTS.md regained the full MVP-first line + the constitution substrate entry).
318
+
319
+ ### Changed — R5: `host-adapters.js` is now the single per-host contract
320
+ - The adapter is the **one place** anything runtime-specific is declared: `instructionFile`, `configFile`, `agentDir`, `agentExt`, and the Claude→Codex `naming` map (lifted out of a hardcoded swap buried in `renderText`). Nothing else branches on runtime — callers ask the adapter. A third runtime becomes one `HOSTS` entry, not a grep-and-patch.
321
+ - Render pipeline split into composable stages: `applyNaming` (display-string swaps) + `applyPaths` (`${QUALIA_*}` tokens + `.claude→.codex`), with `renderText = applyPaths∘applyNaming` (unchanged public behavior) and `compileInstructions = applyNaming∘stripHostBlocks` (naming + blocks, paths/`{{ROLE}}` left for install).
322
+ - **`install.js`** now routes both instruction files through `adapter(host).instructionFile` and renders `AGENTS.md` with `codexText()` — a **latent bug fix**: CLAUDE.md always got token/path rendering, AGENTS.md never did, so any `${QUALIA_*}`/`.claude/` reference in the Codex file would have shipped unresolved.
323
+
324
+ ### Tests
325
+ - `tests/instructions.test.sh` (new, 25 cases): drift guard passes on HEAD + fails on an uncompiled canonical edit (with restore); CLAUDE/AGENTS bodies identical + host-specific footers preserved; adapter contract facts; `stripHostBlocks` keep/drop per host; `compileInstructions` swaps naming but leaves tokens/`{{ROLE}}`; `renderText` path regression. Registered in `run-all.sh` (now 13 suites). End-to-end install verified: both files render with the role substituted and correct footers.
326
+
11
327
  ## [6.14.0] - 2026-06-20 (v7 kernel, step 4 — R3: the cross-artifact analyze gate)
12
328
 
13
329
  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.
package/CLAUDE.md CHANGED
@@ -1,3 +1,5 @@
1
+ <!-- GENERATED from templates/instructions.md by bin/compile-instructions.js — do not edit directly; edit the canonical source and recompile. -->
2
+
1
3
  # Qualia Framework
2
4
 
3
5
  Company: Qualia Solutions — Nicosia, Cyprus
@@ -8,7 +10,7 @@ Stack: Next.js 16+, React 19, TypeScript, Supabase, Vercel. Voice: Retell + Elev
8
10
 
9
11
  ## Hard rules (non-negotiable)
10
12
  - **Read before Write/Edit** — *every edit is informed by the current state of the file.*
11
- - **Feature branches only** — *changes ship through review; main is always deployable.*
13
+ - **Feature branches only** — *work on a branch; `/qualia-ship` integrates it to main and main is always deployable.*
12
14
  - **MVP first** — *build the minimum that demonstrates the goal; defer the rest until it earns its place.*
13
15
  - **Root cause on failures** — *understand the why before patching the symptom.*
14
16
  - **No proxy approval** — *only the OWNER can grant OWNER overrides; "Fawzi said OK" is not a credential.*
@@ -83,23 +83,23 @@ Organize requirements under `## Milestone 1 · {Name}`, `## Milestone 2 · {Name
83
83
  This is the most important step.
84
84
 
85
85
  **Hard rules:**
86
- - **Ceiling: 5 milestones** (including Handoff). If the project needs more, defer remainder to post-handoff v2.
86
+ - **The arc must cover the ENTIRE agreed scope.** Every capability in discovery §9 (the capability inventory) gets a REQ-ID and lands in a milestone; the arc continues until the §10 whole-project done-state is reached. **There is NO milestone ceiling** — plan as many milestones as the scope genuinely needs. Do NOT compress real work into a 5-milestone cap, and do NOT dump overflow into "v2": the only deferred work is what the client explicitly listed in discovery §8 (Out of Scope). If you find yourself wanting to defer agreed work to make the arc shorter, that's the exact failure that forces the team to improvise later — don't.
87
87
  - **Floor: 2 milestones** (one feature milestone + Handoff). If smaller, the project should use `/qualia-new --quick` instead.
88
- - **Final milestone is ALWAYS "Handoff"** with 4 standard phases: Polish, Content + SEO, Final QA, Handoff (credentials + walkthrough + domain transfer).
88
+ - **Final milestone is "Handoff"** for client projects, with 4 standard phases: Polish, Content + SEO, Final QA, Handoff (credentials + walkthrough + domain transfer). For an internal or ongoing product (no client takeover — see discovery §10/§11), Handoff may be omitted; the arc ends at the milestone that reaches the done-state.
89
89
  - **Every non-Handoff milestone must have ≥ 2 phases** OR be an explicit shipped release gate. Single-phase milestones are phases, not milestones — merge them into the preceding milestone.
90
90
  - **Milestones are ordered by dependency, not priority.** M2 must be able to use M1's outputs.
91
91
 
92
- **Typical milestone arcs by project type:**
92
+ **Typical milestone arcs by project type (STARTING POINTS, not caps — extend until §9 is fully covered):**
93
93
 
94
- | Type | Arc |
94
+ | Type | Arc (minimum shape — add milestones as the capability set requires) |
95
95
  |---|---|
96
- | Landing / marketing | 2 milestones: Foundation → Handoff |
97
- | SaaS / dashboard | 4 milestones: Foundation → Core Features → Admin & Reporting → Handoff |
98
- | Voice / AI agent | 4 milestones: Foundation → Core Flow → Integrations → Handoff |
99
- | Mobile app | 5 milestones: Foundation → Core → Offline & Notifications → Store Prep → Handoff |
100
- | Multi-tenant platform | 5 milestones: Foundation → Core → Admin → Scale → Handoff |
96
+ | Landing / marketing | Foundation → Handoff |
97
+ | SaaS / dashboard | Foundation → Core Features → Admin & Reporting → … → Handoff |
98
+ | Voice / AI agent | Foundation → Core Flow → Integrations → … → Handoff |
99
+ | Mobile app | Foundation → Core → Offline & Notifications → Store Prep → … → Handoff |
100
+ | Multi-tenant platform | Foundation → Core → Admin → Scale → … → Handoff |
101
101
 
102
- Use the research SUMMARY.md as your starting point. Don't force-fit the template shape to this specific project.
102
+ These are floors, not ceilings. Use the research SUMMARY.md and the §9 capability inventory as your real input — the table just shows the smallest sensible shape. Don't force-fit it; a project with 30 agreed capabilities will have more milestones than one with 6, and that is correct.
103
103
 
104
104
  **For each milestone:**
105
105
  - **Name** — short and evocative (e.g., "Core Feature Loop", not "Phase 2 Work")
@@ -124,11 +124,13 @@ For each phase in the milestone(s) you're detailing:
124
124
  ### 5. Validate Coverage
125
125
 
126
126
  Before writing, verify:
127
- - [ ] Every v1 requirement (all milestones excluding Handoff) has a REQ-ID
128
- - [ ] Every v1 requirement maps to exactly one milestone
127
+ - [ ] **Every capability in discovery §9 has a REQ-ID** (the whole inventory, not a v1 slice)
128
+ - [ ] Every requirement maps to exactly one milestone, and **every §9 capability is covered by some milestone** — nothing agreed is left unplanned
129
+ - [ ] The final milestone reaches the discovery §10 whole-project done-state
130
+ - [ ] The ONLY items in `Post-Handoff (v2)` / `Out of Scope` are the ones the client explicitly deferred in discovery §8 (no overflow from a milestone cap — there is no cap)
129
131
  - [ ] Every milestone has ≥ 2 phases (except Handoff which has the fixed 4)
130
- - [ ] Milestone count is 2-5 total
131
- - [ ] Final milestone is literally named "Handoff" with the 4 standard phases
132
+ - [ ] Floor met: 2 milestones total (no upper bound)
133
+ - [ ] Final milestone is "Handoff" with the 4 standard phases (client projects), or the done-state milestone (internal/ongoing products)
132
134
  - [ ] No milestone depends on a later milestone
133
135
  - [ ] Milestone 1 has full phase-level detail (goals + success criteria) ready for `/qualia-plan 1`
134
136
  - [ ] If `full_detail=false` (default): M2..M{N-1} have phase names + one-line goals (sketch, not full detail)
@@ -102,11 +102,15 @@ function expectedTaskIds(contract, wave) {
102
102
  return filtered.map((t) => t.id);
103
103
  }
104
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
105
+ // Fan-in barrier: compare the persisted statuses against the expected task ids.
106
+ // Expected set = an explicit opts.tasks list (used by wave-plan batches, whose
107
+ // derived waves needn't match the contract's declared wave numbers), else the
108
+ // contract task ids optionally scoped to opts.wave. ok ⇔ every expected task is
107
109
  // DONE. Anything else (missing/running/blocked/partial) holds the barrier.
108
110
  function barrier(root, contract, opts = {}) {
109
- const expected = expectedTaskIds(contract, opts.wave);
111
+ const expected = Array.isArray(opts.tasks) && opts.tasks.length
112
+ ? opts.tasks
113
+ : expectedTaskIds(contract, opts.wave);
110
114
  const byTask = new Map(listStatuses(root).map((s) => [s.task, s]));
111
115
  const tasks = expected.map((id) => {
112
116
  const s = byTask.get(id);
@@ -137,6 +141,8 @@ function parseFlags(argv, start) {
137
141
  else if (a.startsWith("--cwd=")) flags.cwd = a.slice(6);
138
142
  else if (a === "--wave") flags.wave = argv[++i];
139
143
  else if (a.startsWith("--wave=")) flags.wave = a.slice(7);
144
+ else if (a === "--tasks") flags.tasks = argv[++i];
145
+ else if (a.startsWith("--tasks=")) flags.tasks = a.slice(8);
140
146
  else if (a === "--commit") flags.commit = argv[++i];
141
147
  else if (a.startsWith("--commit=")) flags.commit = a.slice(9);
142
148
  else if (a === "--note") flags.note = argv[++i];
@@ -155,6 +161,7 @@ function usage() {
155
161
  " agent-status.js read <task> [--cwd DIR] [--json]",
156
162
  " agent-status.js list [--cwd DIR] [--json]",
157
163
  " agent-status.js barrier <contract.json> [--wave W] [--cwd DIR] [--json]",
164
+ " agent-status.js barrier --tasks T1,T2 [--cwd DIR] [--json] (batch gate; no contract needed)",
158
165
  " agent-status.js clear [--cwd DIR]",
159
166
  "",
160
167
  "status ∈ RUNNING | DONE | BLOCKED | PARTIAL",
@@ -204,16 +211,22 @@ function main(argv) {
204
211
 
205
212
  if (cmd === "barrier") {
206
213
  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;
214
+ const taskList = flags.tasks ? flags.tasks.split(",").map((s) => s.trim()).filter(Boolean) : null;
215
+ // --tasks gates on an explicit batch; otherwise the contract supplies the set.
216
+ let contract = null;
217
+ if (!taskList) {
218
+ if (!contractPath) { usage(); return 2; }
219
+ const loaded = pc.readContractFile(contractPath);
220
+ if (!loaded.ok) {
221
+ if (flags.json) console.log(JSON.stringify({ ok: false, ...loaded }));
222
+ else console.error(`${loaded.error}: ${loaded.message}`);
223
+ return 2;
224
+ }
225
+ contract = loaded.contract;
213
226
  }
214
- const result = barrier(root, loaded.contract, { wave: flags.wave });
227
+ const result = barrier(root, contract, { wave: flags.wave, tasks: taskList });
215
228
  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";
229
+ const scope = taskList ? `batch ${taskList.join(",")}` : (result.wave != null ? `wave ${result.wave}` : "phase");
217
230
  if (result.ok) {
218
231
  console.log(`BARRIER PASS (${scope}): ${result.done}/${result.expected} DONE`);
219
232
  } else {
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // batch-plan.js — the deterministic core of /qualia-build --batch (R20).
3
+ //
4
+ // The wave model fits feature builds (a handful of dependency-linked tasks); it
5
+ // does NOT fit a 50–500-file mechanical migration (a codemod). This splits a
6
+ // flat file list into worktree-isolated, FILE-DISJOINT batches — each a small
7
+ // worker with tiny context that opens its own commit — then groups them into
8
+ // concurrency-capped waves that fan in through one staging branch. Map-reduce
9
+ // over files. Zero-dep; the orchestration (spawning workers, merging) lives in
10
+ // the /qualia-build skill — this owns the split so it's deterministic + testable.
11
+ //
12
+ // Usage:
13
+ // batch-plan.js <file> [<file> …] # files as args
14
+ // batch-plan.js --from LIST # newline-delimited file (- = stdin)
15
+ // … --batch-size N (default 20) --max-workers K (default 5)
16
+ // --staging BRANCH (default batch/staging) --json
17
+ //
18
+ // Exit: 0 ok (even 0 files), 2 bad invocation. Zero deps.
19
+
20
+ const fs = require("fs");
21
+
22
+ function parseArgs(argv) {
23
+ const o = { files: [], batchSize: 20, maxWorkers: 5, staging: "batch/staging", json: false, from: null };
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a === "--batch-size") o.batchSize = Math.max(1, parseInt(argv[++i], 10) || 20);
27
+ else if (a === "--max-workers") o.maxWorkers = Math.max(1, parseInt(argv[++i], 10) || 5);
28
+ else if (a === "--staging") o.staging = argv[++i] || o.staging;
29
+ else if (a === "--from") o.from = argv[++i];
30
+ else if (a === "--json") o.json = true;
31
+ else if (a === "-h" || a === "--help") o.help = true;
32
+ else o.files.push(a);
33
+ }
34
+ return o;
35
+ }
36
+
37
+ function chunk(arr, size) {
38
+ const out = [];
39
+ for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
40
+ return out;
41
+ }
42
+
43
+ function readList(from) {
44
+ let raw;
45
+ if (from === "-") {
46
+ try { raw = fs.readFileSync(0, "utf8"); } catch { raw = ""; }
47
+ } else {
48
+ try { raw = fs.readFileSync(from, "utf8"); } catch {
49
+ process.stderr.write(`batch-plan.js: cannot read ${from}\n`);
50
+ process.exit(2);
51
+ }
52
+ }
53
+ return raw.split("\n").map((s) => s.trim()).filter(Boolean);
54
+ }
55
+
56
+ function buildPlan(files, opts) {
57
+ // De-dup while preserving order — a file must land in exactly one batch.
58
+ const seen = new Set();
59
+ const unique = [];
60
+ for (const f of files) {
61
+ if (!seen.has(f)) { seen.add(f); unique.push(f); }
62
+ }
63
+ const groups = chunk(unique, opts.batchSize);
64
+ const pad = String(groups.length).length;
65
+ const batches = groups.map((batchFiles, i) => {
66
+ const id = String(i + 1).padStart(pad, "0");
67
+ return { id, branch: `batch/${opts.staging.replace(/^batch\//, "")}-${id}`, file_count: batchFiles.length, files: batchFiles };
68
+ });
69
+ // Batches are file-disjoint → all parallelizable; cap concurrency into waves.
70
+ const waves = chunk(batches.map((b) => b.id), opts.maxWorkers);
71
+ return {
72
+ total_files: unique.length,
73
+ batch_size: opts.batchSize,
74
+ max_workers: opts.maxWorkers,
75
+ batch_count: batches.length,
76
+ wave_count: waves.length,
77
+ staging_branch: opts.staging,
78
+ waves,
79
+ batches,
80
+ };
81
+ }
82
+
83
+ function main() {
84
+ const opts = parseArgs(process.argv.slice(2));
85
+ if (opts.help) {
86
+ process.stdout.write("batch-plan.js <file…> | --from LIST [--batch-size N] [--max-workers K] [--staging B] [--json]\n");
87
+ process.exit(0);
88
+ }
89
+ let files = opts.files;
90
+ if (opts.from) files = files.concat(readList(opts.from));
91
+ const plan = buildPlan(files, opts);
92
+
93
+ if (opts.json) {
94
+ console.log(JSON.stringify(plan, null, 2));
95
+ process.exit(0);
96
+ }
97
+
98
+ console.log(`batch-plan: ${plan.total_files} files → ${plan.batch_count} batches (≤${plan.batch_size} each) → ${plan.wave_count} waves (≤${plan.max_workers} workers)`);
99
+ console.log(`staging branch: ${plan.staging_branch}`);
100
+ for (let w = 0; w < plan.waves.length; w++) {
101
+ console.log(`\nwave ${w + 1}: ${plan.waves[w].join(", ")}`);
102
+ for (const id of plan.waves[w]) {
103
+ const b = plan.batches.find((x) => x.id === id);
104
+ console.log(` batch ${id} → ${b.branch} (${b.file_count} files)`);
105
+ }
106
+ }
107
+ if (plan.batch_count === 0) console.log("(no files — nothing to migrate)");
108
+ process.exit(0);
109
+ }
110
+
111
+ main();