qualia-framework 4.3.0 → 4.4.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.
@@ -0,0 +1,321 @@
1
+ # Plan Contract
2
+
3
+ Machine-readable plan format consumed by builder, verifier, plan-checker, and `state.js`. Replaces ad-hoc markdown re-parsing — markdown plans become presentation, this JSON contract is truth.
4
+
5
+ Status: **draft, v1.** Pressure-test the shape against real phases before locking.
6
+
7
+ ## Why this exists
8
+
9
+ Today, `templates/plan.md` is structured markdown. Planner emits it, builder re-interprets it, verifier re-interprets it, plan-checker re-interprets it. Three independent LLM interpretations of the same prose = drift. The drift is invisible until verification fails for a reason that doesn't match the planner's intent.
10
+
11
+ The contract shifts every machine-driven step (task assignment, dependency check, verification execution) onto deterministic JSON. Prose stays in `phase-N-plan.md` for humans; code reads `phase-N-contract.json`.
12
+
13
+ ## File layout
14
+
15
+ ```
16
+ .planning/
17
+ phase-1-plan.md # human-facing prose (existing)
18
+ phase-1-contract.json # machine truth (NEW)
19
+ phase-1-deviations.json # builder→verifier deltas (existing)
20
+ phase-1-verification.md # verifier output (existing)
21
+ ```
22
+
23
+ `contract.json` is committed. It is regenerated only by re-running `/qualia-plan` or `qualia-framework state.js compile-plan`.
24
+
25
+ ## Schema (v1)
26
+
27
+ TypeScript-flavored for readability. Authoritative validator lives at `bin/lib/plan-contract.js` (Zod or hand-rolled — TBD; framework currently has zero deps).
28
+
29
+ ```ts
30
+ interface PlanContract {
31
+ version: 1; // bump on breaking change
32
+ phase: number; // 1-indexed
33
+ goal: string; // 1-2 sentences, what's TRUE when done
34
+ why: string; // unlocks-what; one sentence
35
+ generated_at: string; // ISO 8601 UTC
36
+ generated_by: "planner" | "compile-plan" | "manual";
37
+ source_plan_hash: string; // sha256 of phase-N-plan.md at compile time; "" if generated_by="manual"
38
+ tasks: Task[];
39
+ success_criteria: string[]; // phase-level user-facing truths
40
+ }
41
+
42
+ interface Task {
43
+ id: string; // "T1", "T2" — stable across reorders
44
+ title: string;
45
+ wave: number; // 1-indexed; tasks in same wave run in parallel
46
+ depends_on: string[]; // task ids this task needs
47
+ persona?: PersonaTag; // optional, for agent specialization
48
+ files_modify: string[]; // repo-relative paths
49
+ files_create: string[]; // repo-relative paths
50
+ files_delete: string[]; // repo-relative paths (for refactors that remove code)
51
+ acceptance_criteria: string[]; // observable behaviors (human-facing)
52
+ action: string; // concrete builder steps (advisory prose, max 500 chars)
53
+ context_files: string[]; // repo-relative paths the builder should read
54
+ verification: VerificationCheck[];
55
+ }
56
+
57
+ type PersonaTag =
58
+ | "security" | "architect" | "ux" | "frontend"
59
+ | "backend" | "data" | "performance" | "none";
60
+
61
+ type VerificationCheck =
62
+ | FileExistsCheck
63
+ | GrepMatchCheck
64
+ | CommandExitCheck
65
+ | BehavioralCheck;
66
+
67
+ interface FileExistsCheck {
68
+ type: "file-exists";
69
+ path: string; // repo-relative
70
+ must_contain?: string; // optional substring assertion
71
+ }
72
+
73
+ interface GrepMatchCheck {
74
+ type: "grep-match";
75
+ path: string; // file or glob
76
+ pattern: string; // regex
77
+ expect: "present" | "absent";
78
+ }
79
+
80
+ interface CommandExitCheck {
81
+ type: "command-exit";
82
+ command: string; // executed via execFile, NOT shell
83
+ args: string[]; // positional args (no shell parsing)
84
+ cwd?: string; // repo-relative; default = repo root
85
+ expected_exit: number; // typically 0
86
+ timeout_ms?: number; // default 30000
87
+ expect_stdout_match?: string; // regex; optional
88
+ }
89
+
90
+ interface BehavioralCheck {
91
+ type: "behavioral";
92
+ description: string; // human-readable; verifier interprets
93
+ evidence_required: Evidence[]; // structured citation requirements; vibes-based passes blocked at schema level
94
+ }
95
+
96
+ interface Evidence {
97
+ path: string; // repo-relative file path the verifier must cite
98
+ matcher?: string; // optional regex the cited line must satisfy
99
+ description: string; // what the cited line should demonstrate
100
+ }
101
+ ```
102
+
103
+ ### Why these four check types
104
+
105
+ They map 1:1 with the existing markdown Verification Contract section, so compilation is mechanical:
106
+
107
+ | Markdown section | Maps to |
108
+ |---|---|
109
+ | `Check type: file-exists` | `FileExistsCheck` |
110
+ | `Check type: grep-match` | `GrepMatchCheck` |
111
+ | `Check type: command-exit` | `CommandExitCheck` |
112
+ | `Check type: behavioral` | `BehavioralCheck` (last resort) |
113
+
114
+ `behavioral` is the only check that retains LLM interpretation — and even there, the schema forces evidence-required so the verifier can't produce vibes-based passes.
115
+
116
+ ## Example: a real phase contract
117
+
118
+ ```json
119
+ {
120
+ "version": 1,
121
+ "phase": 2,
122
+ "goal": "Authenticated users can sign in with email/password and reach the dashboard.",
123
+ "why": "Session persistence is the #1 abandonment trigger in onboarding — verification emails are wasted without it.",
124
+ "generated_at": "2026-04-28T14:32:00Z",
125
+ "generated_by": "planner",
126
+ "source_plan_hash": "sha256:9c1ae6f2b4d8e1f3a5c7b9d0e2f4a6c8e0b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9",
127
+ "tasks": [
128
+ {
129
+ "id": "T1",
130
+ "title": "Add email/password sign-in handler",
131
+ "wave": 1,
132
+ "depends_on": [],
133
+ "persona": "backend",
134
+ "files_modify": ["src/lib/auth.ts"],
135
+ "files_create": ["src/lib/auth-schema.ts"],
136
+ "files_delete": [],
137
+ "acceptance_criteria": [
138
+ "POST /api/auth/signin returns 200 with valid creds",
139
+ "POST /api/auth/signin returns 401 with invalid creds",
140
+ "Session cookie is httpOnly and sameSite=lax"
141
+ ],
142
+ "action": "Use supabase.auth.signInWithPassword. Validate email/password with Zod schema. Set cookie via Next.js Response API.",
143
+ "context_files": [
144
+ "src/lib/supabase/server.ts",
145
+ "src/lib/supabase/client.ts"
146
+ ],
147
+ "verification": [
148
+ {
149
+ "type": "file-exists",
150
+ "path": "src/lib/auth-schema.ts",
151
+ "must_contain": "z.object"
152
+ },
153
+ {
154
+ "type": "command-exit",
155
+ "command": "npx",
156
+ "args": ["tsc", "--noEmit"],
157
+ "expected_exit": 0,
158
+ "timeout_ms": 60000
159
+ },
160
+ {
161
+ "type": "grep-match",
162
+ "path": "src/lib/auth.ts",
163
+ "pattern": "signInWithPassword",
164
+ "expect": "present"
165
+ }
166
+ ]
167
+ },
168
+ {
169
+ "id": "T2",
170
+ "title": "Wire sign-in form to handler",
171
+ "wave": 2,
172
+ "depends_on": ["T1"],
173
+ "persona": "frontend",
174
+ "files_modify": ["src/app/(auth)/signin/page.tsx"],
175
+ "files_create": [],
176
+ "files_delete": [],
177
+ "acceptance_criteria": [
178
+ "Form posts to /api/auth/signin",
179
+ "Error toast shows on 401",
180
+ "Redirect to /dashboard on 200"
181
+ ],
182
+ "action": "Add server action; show error state via useFormState; redirect via redirect() from next/navigation.",
183
+ "context_files": ["src/app/(auth)/signin/page.tsx"],
184
+ "verification": [
185
+ {
186
+ "type": "behavioral",
187
+ "description": "Form submission with valid creds redirects to /dashboard",
188
+ "evidence_required": [
189
+ {
190
+ "path": "src/app/(auth)/signin/page.tsx",
191
+ "matcher": "redirect\\(['\"]/dashboard",
192
+ "description": "redirect() call targeting /dashboard after successful signin"
193
+ },
194
+ {
195
+ "path": "src/app/(auth)/signin/page.tsx",
196
+ "matcher": "useFormState|action=",
197
+ "description": "form is wired to a server action or POST handler"
198
+ }
199
+ ]
200
+ }
201
+ ]
202
+ }
203
+ ],
204
+ "success_criteria": [
205
+ "User can sign in with valid credentials and land on /dashboard",
206
+ "User sees a clear error message on invalid credentials without leaving the page",
207
+ "Session persists across page reloads"
208
+ ]
209
+ }
210
+ ```
211
+
212
+ ## Validation rules (enforced at emission)
213
+
214
+ 1. **`tasks[].id` must be unique** within the phase.
215
+ 2. **Task ids must match** `^T\d+$` — `T1`, `T2`, etc. The compiler prefixes markdown task numbers (`## Task 1` → `T1`).
216
+ 3. **`depends_on` must reference ids that exist** in the same contract.
217
+ 4. **No cycles in `depends_on`.**
218
+ 5. **Wave assignment must respect dependencies** — a task's wave must be `>` than the max wave of its dependencies. (Trivially: if T2 depends on T1, T2.wave > T1.wave.)
219
+ 6. **At least one verification check per task.** Empty `verification: []` is rejected.
220
+ 7. **`files_modify`, `files_create`, `files_delete` are pairwise disjoint** — a file is in at most one of the three.
221
+ 8. **`command-exit` checks must use execFile-safe args** — no shell metacharacters in `command`; `args[]` carries positional values.
222
+ 9. **`success_criteria` minimum 1 entry.**
223
+ 10. **`action` ≤ 500 characters** — enforced. Keeps planner from over-specifying implementation.
224
+ 11. **`evidence_required[].path` must be repo-relative** and `matcher` (when present) must be a valid regex.
225
+
226
+ `bin/state.js validate-plan` runs these. Failures block transition to `built`.
227
+
228
+ Validator implementation: hand-rolled at `bin/lib/plan-contract.js`, ~80 LOC, zero dependencies. Framework's no-deps posture is preserved.
229
+
230
+ ## Drift detection (contract vs markdown)
231
+
232
+ Manual edits to `phase-N-plan.md` happen in practice. Without detection, the contract silently goes stale: builder reads JSON truth that no longer matches what humans see in markdown.
233
+
234
+ `source_plan_hash` is `sha256(plan_md_contents)` at compile time, prefixed `sha256:`. Stored in the contract.
235
+
236
+ `bin/state.js validate-plan --check-drift` re-hashes the current plan markdown and compares. Drift behavior:
237
+
238
+ | Scenario | Action |
239
+ |---|---|
240
+ | Hashes match | OK, no output |
241
+ | Hashes differ | Exit 2, message: `plan.md drifted from contract; run compile-plan --refresh` |
242
+ | Contract missing `source_plan_hash` (legacy or `manual`) | Warn but pass — drift checking disabled for that contract |
243
+
244
+ `compile-plan --refresh` re-reads markdown, regenerates contract, updates hash. Builder/verifier refuse to run if `--check-drift` fails.
245
+
246
+ ## Verification execution errors
247
+
248
+ A check that *cannot run* (binary missing, timeout, cwd doesn't exist) is distinct from a check that *ran and failed*. The verifier records:
249
+
250
+ | Outcome | `verification_result` | `failure_reason` |
251
+ |---|---|---|
252
+ | Check ran, passed | `pass` | — |
253
+ | Check ran, criteria unmet | `fail` | `verification-criteria-unmet` |
254
+ | Behavioral check, evidence missing | `fail` | `verification-evidence-missing` |
255
+ | Check itself errored (cmd not found, timeout, etc.) | `partial` | `verification-execution-error` |
256
+
257
+ Execution errors are NOT verification failures. They block phase advance the same way, but a postmortem treats them differently — fix the infrastructure, then re-run.
258
+
259
+ ## How builder reads it
260
+
261
+ ```js
262
+ // pseudocode — the actual implementation lives in skills/qualia-build
263
+ const contract = JSON.parse(fs.readFileSync(`.planning/phase-${N}-contract.json`));
264
+ const myTask = contract.tasks.find(t => t.id === assignedTaskId);
265
+
266
+ // builder gets:
267
+ // - exact files to touch
268
+ // - acceptance_criteria as the "definition of done"
269
+ // - context_files to read first
270
+ // - verification[] is the self-check before declaring DONE
271
+ ```
272
+
273
+ The builder still receives the Action prose as advisory guidance. The contract is the boundary.
274
+
275
+ ## How verifier reads it
276
+
277
+ For each task in the contract:
278
+ 1. Walk `verification[]`.
279
+ 2. For deterministic checks (`file-exists`, `grep-match`, `command-exit`): execute and record pass/fail with stdout/stderr captured. Distinguish "ran and failed" (`verification-criteria-unmet`) from "could not run" (`verification-execution-error`).
280
+ 3. For `behavioral` checks: for each `evidence_required[i]`, the verifier MUST produce a `{path, line, snippet}` citation. If `matcher` is present, the cited line must satisfy the regex. Missing evidence or matcher mismatch → automatic `verification-evidence-missing`.
281
+ 4. Aggregate per-task → per-phase pass/fail.
282
+ 5. Write `phase-N-verification.json` (machine output) alongside `phase-N-verification.md` (human output).
283
+
284
+ This eliminates the "verifier wrote a glowing pass when half the criteria weren't actually met" failure mode — `evidence_required[]` is structured, so vibes-based passes are blocked at the schema level.
285
+
286
+ ## Compile mode (migrating in-flight projects)
287
+
288
+ `bin/state.js compile-plan --phase N` reads `phase-N-plan.md` and emits a best-effort `phase-N-contract.json`:
289
+
290
+ - Frontmatter → `phase`, `goal`
291
+ - `## Task N — title` blocks → `tasks[]`
292
+ - `**Files:**` line → `files_modify` (cannot distinguish create vs modify from prose; defaults to modify, warns)
293
+ - `**Acceptance Criteria:**` bullets → `acceptance_criteria`
294
+ - `### Contract for Task N` blocks → `verification[]`
295
+ - Missing fields → `compile-plan` exits non-zero with a list of gaps
296
+
297
+ Compile is a one-time bridge. New plans emit JSON directly from the planner agent.
298
+
299
+ ## Design decisions (locked v1)
300
+
301
+ These were called out as open questions during draft; resolved here so implementation can proceed.
302
+
303
+ 1. **Persona enum:** dropped `data` — covered by `backend`. Six personas + `none`.
304
+ 2. **`acceptance_criteria` vs `verification[]`:** kept separate. AC is the human-facing definition of done (lands in commit messages, milestone summaries, ERP reports). `verification[]` is the mechanical execution path. The verifier never interprets AC — it executes `verification[]`. This separation is the whole point of the contract.
305
+ 3. **`action` cap:** 500 chars. Advisory only. Validator enforces.
306
+ 4. **Versioning:** in-place migration via `compile-plan --upgrade`. `version` field tells the loader which schema to apply. No filename suffixes — canonical filename stays `phase-N-contract.json`.
307
+ 5. **Wave placement:** lives on the task. The validator enforces `task.wave > max(deps wave)` so the redundancy with `depends_on` is contained. Wave is a scheduling/display hint; `depends_on` is the constraint.
308
+ 6. **`behavioral` checks:** permanent. UX feel, error message clarity, animation timing — none of these are deterministic. The escape hatch is healthy. The `evidence_required[]` field forces verifier to cite proof; vibes-based passes are blocked at the schema level.
309
+ 7. **Validator:** hand-rolled in plain Node. Framework keeps zero npm dependencies. Zod is rejected for this layer.
310
+
311
+ ## Migration plan
312
+
313
+ 1. Add schema + validator + `compile-plan` command. No callers yet.
314
+ 2. Backfill contracts for active projects via `compile-plan` — manual review of warnings.
315
+ 3. Update planner agent prompt to emit JSON alongside markdown.
316
+ 4. Update builder skill to read JSON for files/AC/verification; markdown still readable.
317
+ 5. Update verifier agent to execute `verification[]` deterministically; keep prose verification report for humans.
318
+ 6. Update plan-checker to validate JSON.
319
+ 7. After two milestones run cleanly on JSON, mark prose plan as advisory-only in docs.
320
+
321
+ No hard cutover. Both formats coexist during migration.
@@ -63,12 +63,12 @@ try {
63
63
  }
64
64
 
65
65
  // Synchronously fetch the latest version from npm. Tight timeout so the hook
66
- // never blocks Claude Code for long. The cache timestamp is written ONLY if
67
- // this fetch succeeds otherwise the next session retries (no 24h blackout
68
- // when the network is unreachable).
66
+ // never blocks Claude Code for long. Stamp the cache before the network call:
67
+ // if npm/DNS is down, we avoid paying the timeout on every Bash command.
69
68
  let latest = "";
70
69
  try {
71
70
  fs.writeFileSync(LOCK_FILE, String(process.pid));
71
+ fs.writeFileSync(CACHE_FILE, String(Math.floor(Date.now() / 1000)));
72
72
  const r = spawnSync("npm", ["view", "qualia-framework", "version"], {
73
73
  encoding: "utf8",
74
74
  timeout: 3000,
@@ -80,14 +80,10 @@ try {
80
80
  try { fs.unlinkSync(LOCK_FILE); } catch {}
81
81
 
82
82
  if (!latest) {
83
- // Fetch failed — leave cache untouched so the next call retries.
84
83
  _trace("auto-update", "allow", { reason: "npm-fetch-failed" });
85
84
  process.exit(0);
86
85
  }
87
86
 
88
- // Successful fetch — debounce future checks for 24h.
89
- fs.writeFileSync(CACHE_FILE, String(Math.floor(Date.now() / 1000)));
90
-
91
87
  const cmp = (a, b) => {
92
88
  const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
93
89
  for (let i = 0; i < 3; i++) {
@@ -37,6 +37,23 @@ function readCompactConfig() {
37
37
  }
38
38
  }
39
39
 
40
+ function git(args, opts = {}) {
41
+ return spawnSync("git", args, {
42
+ encoding: "utf8",
43
+ timeout: 3000,
44
+ shell: process.platform === "win32",
45
+ ...opts,
46
+ });
47
+ }
48
+
49
+ function stateHasPendingChanges() {
50
+ const diff = git(["diff", "--name-only", "--", STATE_FILE]);
51
+ if ((diff.stdout || "").split(/\r?\n/).includes(STATE_FILE)) return true;
52
+
53
+ const untracked = git(["ls-files", "--others", "--exclude-standard", "--", STATE_FILE]);
54
+ return (untracked.stdout || "").split(/\r?\n/).includes(STATE_FILE);
55
+ }
56
+
40
57
  let _commitStatus = null;
41
58
  let _commitReason = "no-state-file";
42
59
  let _commitFlags = null;
@@ -45,17 +62,9 @@ try {
45
62
  if (fs.existsSync(STATE_FILE)) {
46
63
  console.log("QUALIA: Saving state before compaction...");
47
64
  _commitReason = "state-clean";
48
- // Check if STATE.md has uncommitted changes
49
- const diff = spawnSync("git", ["diff", "--name-only", STATE_FILE], {
50
- encoding: "utf8",
51
- timeout: 3000,
52
- shell: process.platform === "win32",
53
- });
54
- if ((diff.stdout || "").includes("STATE.md")) {
55
- const addRes = spawnSync("git", ["add", STATE_FILE], {
56
- timeout: 3000,
57
- shell: process.platform === "win32",
58
- });
65
+ // Check if STATE.md has tracked or untracked changes.
66
+ if (stateHasPendingChanges()) {
67
+ const addRes = git(["add", STATE_FILE]);
59
68
  const cfg = readCompactConfig();
60
69
  const commitArgs = ["commit"];
61
70
  if (!cfg.respect_user_hooks) commitArgs.push("--no-verify");
@@ -76,6 +85,8 @@ try {
76
85
  _commitReason = addRes.status === 0 && commitRes.status === 0
77
86
  ? "committed"
78
87
  : "commit-failed";
88
+ } else {
89
+ _commitReason = "state-clean";
79
90
  }
80
91
  }
81
92
  } catch {
@@ -31,7 +31,8 @@ function _trace(hookName, result, extra) {
31
31
 
32
32
  function runGate(label, cmd, args, { required = true } = {}) {
33
33
  const r = spawnSync(cmd, args, {
34
- stdio: "ignore",
34
+ stdio: ["ignore", "pipe", "pipe"],
35
+ encoding: "utf8",
35
36
  timeout: 180000,
36
37
  shell: process.platform === "win32",
37
38
  });
@@ -41,7 +42,20 @@ function runGate(label, cmd, args, { required = true } = {}) {
41
42
  }
42
43
  if (required) {
43
44
  console.error(`BLOCKED: ${label} errors. Fix before deploying.`);
44
- _trace("pre-deploy-gate", "block", { gate: label });
45
+ const output = `${r.stdout || ""}${r.stderr || ""}`.trim();
46
+ if (output) {
47
+ const lines = output.split(/\r?\n/).filter(Boolean).slice(-20);
48
+ console.error(`Last ${lines.length} output line${lines.length === 1 ? "" : "s"}:`);
49
+ for (const line of lines) console.error(` ${line}`);
50
+ } else if (r.error) {
51
+ console.error(` ${r.error.message}`);
52
+ }
53
+ _trace("pre-deploy-gate", "block", {
54
+ gate: label,
55
+ status: r.status,
56
+ signal: r.signal || undefined,
57
+ error: r.error ? r.error.message : undefined,
58
+ });
45
59
  process.exit(2);
46
60
  }
47
61
  return false;
package/hooks/pre-push.js CHANGED
@@ -102,6 +102,15 @@ function commitStamp() {
102
102
  return { committed: true, sha: lastCommit, ts: now };
103
103
  }
104
104
 
105
+ function shouldBlock(result) {
106
+ const hardSkips = new Set([
107
+ "tracking-unreadable",
108
+ "git-add-failed",
109
+ "git-commit-failed",
110
+ ]);
111
+ return result && hardSkips.has(result.skipped);
112
+ }
113
+
105
114
  function _trace(result, extra) {
106
115
  try {
107
116
  const traceDir = path.join(HOME, ".claude", ".qualia-traces");
@@ -120,10 +129,21 @@ function _trace(result, extra) {
120
129
 
121
130
  try {
122
131
  const result = commitStamp();
132
+ if (shouldBlock(result)) {
133
+ const detail = result.error ? ` ${String(result.error).slice(0, 500)}` : "";
134
+ const msg = `BLOCKED: tracking.json sync failed (${result.skipped}). Fix before pushing.${detail}`;
135
+ console.error(msg);
136
+ console.log(msg);
137
+ _trace("block", result);
138
+ process.exit(2);
139
+ }
123
140
  _trace("allow", result);
124
141
  } catch (err) {
125
- // Never block a push log and exit clean.
126
- _trace("allow", { error: err.message });
142
+ const msg = `BLOCKED: tracking.json sync failed (${err.message}). Fix before pushing.`;
143
+ console.error(msg);
144
+ console.log(msg);
145
+ _trace("block", { error: err.message });
146
+ process.exit(2);
127
147
  }
128
148
 
129
149
  process.exit(0);
@@ -103,7 +103,7 @@ try {
103
103
  const tracking = readJson(path.join(repoRoot, ".planning", "tracking.json"));
104
104
  if (tracking) {
105
105
  const p = tracking.phase || 0;
106
- const pt = tracking.phase_total || 0;
106
+ const pt = tracking.total_phases || tracking.phase_total || 0;
107
107
  if (pt > 0) phase = `${p}/${pt}`;
108
108
  const td = tracking.tasks_done || 0;
109
109
  const tt = tracking.tasks_total || 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Claude Code workflow framework by Qualia Solutions. Plan, build, verify, ship.",
5
5
  "bin": {
6
6
  "qualia-framework": "./bin/cli.js"
@@ -23,7 +23,13 @@
23
23
  },
24
24
  "homepage": "https://github.com/Qualiasolutions/qualia-framework#readme",
25
25
  "scripts": {
26
- "test": "node --test tests/runner.js"
26
+ "test": "npm run test:shell",
27
+ "test:state": "bash tests/state.test.sh",
28
+ "test:hooks": "bash tests/hooks.test.sh",
29
+ "test:bin": "bash tests/bin.test.sh",
30
+ "test:lib": "bash tests/lib.test.sh",
31
+ "test:statusline": "bash tests/statusline.test.sh",
32
+ "test:shell": "bash tests/statusline.test.sh && bash tests/state.test.sh && bash tests/hooks.test.sh && bash tests/bin.test.sh && bash tests/lib.test.sh"
27
33
  },
28
34
  "files": [
29
35
  "bin/",
@@ -32,9 +32,9 @@ cat .planning/phase-{N}-plan.md
32
32
 
33
33
  Parse: tasks, waves, file references.
34
34
 
35
- ### 1b. Create Recovery Point
35
+ ### 1b. Create Recovery Reference
36
36
 
37
- Before executing any tasks, tag current HEAD for rollback:
37
+ Before executing any tasks, record current HEAD for diagnosis. This is a reference, not an automatic rollback instruction.
38
38
 
39
39
  ```bash
40
40
  git tag -f "pre-build-phase-{N}" HEAD 2>/dev/null
@@ -44,10 +44,10 @@ git tag -f "pre-build-phase-{N}" HEAD 2>/dev/null
44
44
  node ~/.claude/bin/qualia-ui.js info "Recovery point: pre-build-phase-{N}"
45
45
  ```
46
46
 
47
- If a wave fails and the user needs to roll back:
47
+ If a wave fails, stop and inspect `git status` plus the failed task output. Do not run destructive rollback commands automatically. Preserve user work, then either fix forward or ask before reverting specific files.
48
48
  ```bash
49
- git reset --hard pre-build-phase-{N}
50
- node ~/.claude/bin/state.js transition --to planned --force
49
+ git status --short
50
+ git diff --stat
51
51
  ```
52
52
 
53
53
  ### 2. Execute Waves
@@ -30,7 +30,7 @@ the same — manually-triggered, internal-data only, no vector DB.
30
30
  - **Manually:** `/qualia-flush` whenever the daily-log feels rich. Once a week
31
31
  is the recommended cadence. More than once a day is wasteful — the
32
32
  signal-to-noise ratio is too low at single-day windows.
33
- - **Automatically:** not yet wired. v4.3.0 will add a cron-friendly
33
+ - **CLI runner:** `qualia-framework flush` wraps the cron-friendly
34
34
  `bin/knowledge-flush.js` non-interactive runner.
35
35
 
36
36
  ## Inputs
@@ -24,7 +24,7 @@ node ~/.claude/bin/qualia-ui.js banner quick
24
24
  2. **Build:** Do it directly — read before write, MVP only
25
25
  3. **Verify:** Run `npx tsc --noEmit`, test locally
26
26
  4. **Commit:** Atomic commit with clear message
27
- 5. **Update:** Update tracking.json notes field
27
+ 5. **Update:** Record the work through `state.js`
28
28
 
29
29
  End with:
30
30
  ```bash
@@ -134,7 +134,7 @@ SUBMITTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
134
134
  # returns a generic 401 that is hard to diagnose.
135
135
  if [ "$ERP_ENABLED" = "true" ] && [ -z "$API_KEY" ] && [ "$DRY_RUN" != "true" ]; then
136
136
  node ~/.claude/bin/qualia-ui.js warn "ERP API key missing (~/.claude/.erp-api-key is empty or unreadable). Skipping upload."
137
- node ~/.claude/bin/qualia-ui.js info "Ask Fawzi for the ERP key, save to ~/.claude/.erp-api-key, then re-run /qualia-report --upload-only."
137
+ node ~/.claude/bin/qualia-ui.js info "Ask Fawzi for the ERP key, run 'qualia-framework set-erp-key <key>', then run 'qualia-framework erp-ping'."
138
138
  ERP_ENABLED="false"
139
139
  fi
140
140
 
@@ -37,13 +37,14 @@ VERIFICATION=$(echo "$STATE" | node -e "try{const d=JSON.parse(require('fs').rea
37
37
  # verified+pass — final phase verified; skipping polish is allowed for hotfixes
38
38
  # Anything else (setup, planned, built, shipped, handed_off, verified+fail) is refused.
39
39
  if [ "$STATUS" != "polished" ] && ! { [ "$STATUS" = "verified" ] && [ "$VERIFICATION" = "pass" ]; }; then
40
+ if [ "${QUALIA_SHIP_FORCE:-0}" = "1" ]; then
41
+ node ~/.claude/bin/qualia-ui.js warn "Forced ship from state '$STATUS' (verification: ${VERIFICATION:-none}). Record the reason in the final report."
42
+ else
40
43
  node ~/.claude/bin/qualia-ui.js fail "Cannot ship from state '$STATUS' (verification: ${VERIFICATION:-none})."
41
44
  node ~/.claude/bin/qualia-ui.js info "Run /qualia-polish first, or /qualia-verify {phase} if verification is still pending."
42
- node ~/.claude/bin/qualia-ui.js info "Override: add --force to the skill invocation (hotfix escape hatch, use with care)."
43
- # The --force escape hatch exists for production hotfixes where the polished
44
- # state was never reached. The operator is expected to have read and
45
- # understood the pending verification findings.
45
+ node ~/.claude/bin/qualia-ui.js info "Hotfix override: set QUALIA_SHIP_FORCE=1 only when the user explicitly approved it."
46
46
  exit 1
47
+ fi
47
48
  fi
48
49
  ```
49
50
 
@@ -53,7 +54,8 @@ Run in sequence. Auto-fix failures (up to 2 attempts).
53
54
 
54
55
  ```bash
55
56
  npx tsc --noEmit # TypeScript — must pass
56
- npx eslint . --max-warnings 0 # Lint auto-fix first
57
+ if node -e "const p=require('./package.json');process.exit(p.scripts&&p.scripts.lint?0:1)"; then npm run lint; fi
58
+ if node -e "const p=require('./package.json');process.exit(p.scripts&&p.scripts.test?0:1)"; then npm test; fi
57
59
  npm run build # Build — must succeed
58
60
  ```
59
61
 
@@ -130,15 +132,15 @@ wrangler deploy # Cloudflare Workers
130
132
 
131
133
  ### 5. Post-Deploy Verification
132
134
 
133
- Read the deployed URL from `tracking.json.deployed_url` — set by the deploy tool's output parser, or passed via `--url` to this skill. Do NOT use a `{domain}` placeholder — that expects the LLM to hallucinate the URL, which is exactly the kind of silent fail the state guard above prevents.
135
+ Read the deployed URL from `tracking.json.deployed_url` or from an explicit user-provided URL. Do NOT use a `{domain}` placeholder — that expects the LLM to hallucinate the URL, which is exactly the kind of silent fail the state guard above prevents.
134
136
 
135
137
  ```bash
136
- # Read URL from tracking.json (set by /qualia-handoff or previous ship), or
137
- # let the operator pass it as an argument. Never assume a placeholder.
138
- URL=$(node -e "try{const t=JSON.parse(require('fs').readFileSync('.planning/tracking.json','utf8'));process.stdout.write(t.deployed_url||'')}catch{}")
138
+ # If the user invoked `/qualia-ship --url ...`, set QUALIA_SHIP_URL to that
139
+ # exact value before running this block. Otherwise use tracking.json.
140
+ URL="${QUALIA_SHIP_URL:-$(node -e 'try{const t=JSON.parse(require("fs").readFileSync(".planning/tracking.json","utf8"));process.stdout.write(t.deployed_url||"")}catch{}')}"
139
141
  if [ -z "$URL" ]; then
140
142
  node ~/.claude/bin/qualia-ui.js warn "No deployed_url in tracking.json — parse it from the deploy command output (vercel/supabase/wrangler all print the URL on success)."
141
- node ~/.claude/bin/qualia-ui.js info "Re-run with: /qualia-ship --url https://your-site.com"
143
+ node ~/.claude/bin/qualia-ui.js info "Re-run with: /qualia-ship --url https://your-site.com, then export that value as QUALIA_SHIP_URL for this check."
142
144
  exit 1
143
145
  fi
144
146