qualia-framework-v2 2.1.1 → 2.3.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/README.md CHANGED
@@ -19,18 +19,28 @@ Open Claude Code in any project directory:
19
19
  ```
20
20
  /qualia-new # Set up a new project
21
21
  /qualia # What should I do next?
22
+ /qualia-idk # I'm stuck — smart advisor
22
23
  /qualia-plan # Plan the current phase
23
- /qualia-build # Build it
24
- /qualia-verify # Verify it works
25
- /qualia-ship # Deploy
26
- /qualia-report # Log your work
24
+ /qualia-build # Build it (parallel tasks)
25
+ /qualia-verify # Verify it actually works
26
+ /qualia-design # One-shot design transformation
27
+ /qualia-debug # Structured debugging
28
+ /qualia-review # Production audit
29
+ /qualia-quick # Skip planning, just do it
30
+ /qualia-task # Build one thing properly
31
+ /qualia-polish # Design and UX pass
32
+ /qualia-ship # Deploy to production
33
+ /qualia-handoff # Deliver to client
34
+ /qualia-pause # Save session, continue later
35
+ /qualia-resume # Pick up where you left off
36
+ /qualia-report # Log your work (mandatory)
27
37
  ```
28
38
 
29
39
  See `guide.md` for the full developer guide.
30
40
 
31
41
  ## What's Inside
32
42
 
33
- - **11 skills** — slash commands that guide you from setup to handoff
43
+ - **17 skills** — slash commands from setup to handoff, plus debugging, design, review, and session management
34
44
  - **3 agents** — planner, builder, verifier (each in fresh context)
35
45
  - **7 hooks** — branch guard, pre-push tracking sync, env protection, migration guard, deploy gate, pre-compact state save, session start
36
46
  - **3 rules** — security, frontend, deployment
@@ -56,6 +66,10 @@ The `settings.json` hooks are real ops engineering, not theoretical:
56
66
  - **Env block** — Prevents Claude from touching `.env` files
57
67
  - **Pre-compact** — Saves state before context compression
58
68
 
69
+ ### Enforced State Machine
70
+
71
+ Every workflow step calls `state.js` — a Node.js state machine that validates preconditions, updates both STATE.md and tracking.json atomically, and tracks gap-closure cycles. You can't build without planning, can't verify without building, and can't loop on gap-closure more than twice before escalating.
72
+
59
73
  ### Wave-Based Parallelization
60
74
 
61
75
  Plans are grouped into waves for parallel execution. No fancy DAG solver — the planner assigns wave numbers, the orchestrator spawns agents per wave. Pragmatic over clever.
@@ -71,9 +85,10 @@ npx qualia-framework-v2 install
71
85
  |
72
86
  v
73
87
  ~/.claude/
74
- ├── skills/ 11 slash commands
88
+ ├── skills/ 17 slash commands
75
89
  ├── agents/ planner.md, builder.md, verifier.md
76
90
  ├── hooks/ 7 shell scripts (branch, env, migration, deploy, push, compact, session)
91
+ ├── bin/ state.js (state machine with precondition enforcement)
77
92
  ├── rules/ security.md, frontend.md, deployment.md
78
93
  ├── qualia-templates/ tracking.json, state.md, project.md, plan.md
79
94
  ├── CLAUDE.md global instructions (role-configured per team member)
package/bin/install.js CHANGED
@@ -30,6 +30,16 @@ const TEAM = {
30
30
  role: "EMPLOYEE",
31
31
  description: "Developer. Feature branches only. Cannot push to main or edit .env files.",
32
32
  },
33
+ "QS-RAMA-04": {
34
+ name: "Rama",
35
+ role: "EMPLOYEE",
36
+ description: "Developer. Feature branches only. Cannot push to main or edit .env files.",
37
+ },
38
+ "QS-SALLY-05": {
39
+ name: "Sally",
40
+ role: "EMPLOYEE",
41
+ description: "Developer. Feature branches only. Cannot push to main or edit .env files.",
42
+ },
33
43
  };
34
44
 
35
45
  const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
@@ -188,6 +198,20 @@ async function main() {
188
198
  warn(`CLAUDE.md — ${e.message}`);
189
199
  }
190
200
 
201
+ // ─── Scripts ─────────────────────────────────────────────
202
+ log(`${WHITE}Scripts${RESET}`);
203
+ try {
204
+ const binDest = path.join(CLAUDE_DIR, "bin");
205
+ if (!fs.existsSync(binDest)) fs.mkdirSync(binDest, { recursive: true });
206
+ copy(
207
+ path.join(FRAMEWORK_DIR, "bin", "state.js"),
208
+ path.join(binDest, "state.js")
209
+ );
210
+ ok("state.js (state machine)");
211
+ } catch (e) {
212
+ warn(`state.js — ${e.message}`);
213
+ }
214
+
191
215
  // ─── Guide ─────────────────────────────────────────────
192
216
  try {
193
217
  copy(
@@ -373,6 +397,7 @@ async function main() {
373
397
  console.log(` Agents: ${WHITE}3${RESET} ${DIM}(planner, builder, verifier)${RESET}`);
374
398
  console.log(` Hooks: ${WHITE}6${RESET} ${DIM}(branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact)${RESET}`);
375
399
  console.log(` Rules: ${WHITE}3${RESET} ${DIM}(security, frontend, deployment)${RESET}`);
400
+ console.log(` Scripts: ${WHITE}1${RESET} ${DIM}(state.js)${RESET}`);
376
401
  console.log(` Templates: ${WHITE}4${RESET}`);
377
402
  console.log(` Status line: ${GREEN}✓${RESET}`);
378
403
  console.log(` CLAUDE.md: ${GREEN}✓${RESET} ${DIM}(${member.role})${RESET}`);
package/bin/state.js ADDED
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env node
2
+ // Qualia State Machine — atomic state transitions with precondition validation
3
+ // No external dependencies. Node >= 18 only.
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const PLANNING = ".planning";
9
+ const STATE_FILE = path.join(PLANNING, "STATE.md");
10
+ const TRACKING_FILE = path.join(PLANNING, "tracking.json");
11
+
12
+ // ─── Arg Parsing ─────────────────────────────────────────
13
+ function parseArgs(argv) {
14
+ const args = {};
15
+ for (let i = 0; i < argv.length; i++) {
16
+ if (argv[i].startsWith("--")) {
17
+ const key = argv[i].slice(2).replace(/-/g, "_");
18
+ const next = argv[i + 1];
19
+ if (!next || next.startsWith("--")) {
20
+ args[key] = true;
21
+ } else {
22
+ args[key] = next;
23
+ i++;
24
+ }
25
+ }
26
+ }
27
+ return args;
28
+ }
29
+
30
+ // ─── File I/O ────────────────────────────────────────────
31
+ function readTracking() {
32
+ try {
33
+ return JSON.parse(fs.readFileSync(TRACKING_FILE, "utf8"));
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function writeTracking(t) {
40
+ fs.writeFileSync(TRACKING_FILE, JSON.stringify(t, null, 2) + "\n");
41
+ }
42
+
43
+ function readState() {
44
+ try {
45
+ return fs.readFileSync(STATE_FILE, "utf8");
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ // ─── STATE.md Parser ─────────────────────────────────────
52
+ function parseStateMd(content) {
53
+ if (!content) return null;
54
+ const get = (prefix) => {
55
+ const m = content.match(new RegExp(`^${prefix}:\\s*(.+)$`, "m"));
56
+ return m ? m[1].trim() : "";
57
+ };
58
+ const phaseMatch = content.match(
59
+ /^Phase:\s*(\d+)\s+of\s+(\d+)\s*[—-]\s*(.+)$/m
60
+ );
61
+ // Parse roadmap table
62
+ const phases = [];
63
+ const tableMatch = content.match(
64
+ /\| # \| Phase \| Goal \| Status \|\n\|[-|]+\|\n([\s\S]*?)(?=\n##|\n$|$)/
65
+ );
66
+ if (tableMatch) {
67
+ for (const row of tableMatch[1].trim().split("\n")) {
68
+ const cols = row.split("|").map((c) => c.trim()).filter(Boolean);
69
+ if (cols.length >= 4) {
70
+ phases.push({
71
+ num: parseInt(cols[0]),
72
+ name: cols[1],
73
+ goal: cols[2],
74
+ status: cols[3],
75
+ });
76
+ }
77
+ }
78
+ }
79
+ return {
80
+ phase: phaseMatch ? parseInt(phaseMatch[1]) : 1,
81
+ total_phases: phaseMatch ? parseInt(phaseMatch[2]) : phases.length || 1,
82
+ phase_name: phaseMatch ? phaseMatch[3].trim() : "",
83
+ status: get("Status").toLowerCase().replace(/\s+/g, "_") || "setup",
84
+ assigned_to: get("Assigned to") || "",
85
+ phases,
86
+ };
87
+ }
88
+
89
+ // ─── STATE.md Writer ─────────────────────────────────────
90
+ function writeStateMd(s) {
91
+ const phaseFrac = Math.round(((s.phase - 1) / s.total_phases) * 100);
92
+ const filled = Math.round(phaseFrac / 10);
93
+ const bar = "█".repeat(filled) + "░".repeat(10 - filled);
94
+ const now = new Date().toISOString().split("T")[0];
95
+
96
+ const roadmap = s.phases
97
+ .map((p) => `| ${p.num} | ${p.name} | ${p.goal} | ${p.status} |`)
98
+ .join("\n");
99
+
100
+ const md = `# Project State
101
+
102
+ ## Project
103
+ See: .planning/PROJECT.md
104
+
105
+ ## Current Position
106
+ Phase: ${s.phase} of ${s.total_phases} — ${s.phase_name}
107
+ Status: ${s.status}
108
+ Assigned to: ${s.assigned_to}
109
+ Last activity: ${now} — ${s.last_activity || "State updated"}
110
+
111
+ Progress: [${bar}] ${phaseFrac}%
112
+
113
+ ## Roadmap
114
+ | # | Phase | Goal | Status |
115
+ |---|-------|------|--------|
116
+ ${roadmap}
117
+
118
+ ## Blockers
119
+ ${s.blockers || "None."}
120
+
121
+ ## Session
122
+ Last session: ${now}
123
+ Last worked by: ${s.assigned_to}
124
+ Resume: ${s.resume || "—"}
125
+ `;
126
+ fs.writeFileSync(STATE_FILE, md);
127
+ }
128
+
129
+ // ─── Precondition Checks ─────────────────────────────────
130
+ const VALID_FROM = {
131
+ planned: ["setup", "verified"], // verified(fail) → planned = gap closure
132
+ built: ["planned"],
133
+ verified: ["built"],
134
+ polished: ["verified"],
135
+ shipped: ["polished"],
136
+ handed_off: ["shipped"],
137
+ done: ["handed_off"],
138
+ };
139
+
140
+ function checkPreconditions(current, target, opts) {
141
+ const phase = parseInt(opts.phase) || current.phase;
142
+
143
+ // Special transitions (no status gate)
144
+ if (target === "note" || target === "activity") return { ok: true };
145
+
146
+ // Check valid transition
147
+ const allowed = VALID_FROM[target];
148
+ if (!allowed) return fail("INVALID_STATUS", `Unknown status: ${target}`);
149
+ if (!allowed.includes(current.status)) {
150
+ return fail(
151
+ "PRECONDITION_FAILED",
152
+ `Cannot go from '${current.status}' to '${target}'. Allowed from: ${allowed.join(", ")}`
153
+ );
154
+ }
155
+
156
+ // File checks
157
+ if (target === "planned") {
158
+ const planFile = path.join(PLANNING, `phase-${phase}-plan.md`);
159
+ if (!fs.existsSync(planFile))
160
+ return fail("MISSING_FILE", `Plan file not found: ${planFile}`);
161
+ }
162
+
163
+ if (target === "verified") {
164
+ const vFile = path.join(PLANNING, `phase-${phase}-verification.md`);
165
+ if (!fs.existsSync(vFile))
166
+ return fail("MISSING_FILE", `Verification file not found: ${vFile}`);
167
+ if (!opts.verification || !["pass", "fail"].includes(opts.verification))
168
+ return fail("MISSING_ARG", "--verification must be 'pass' or 'fail'");
169
+ }
170
+
171
+ if (target === "shipped") {
172
+ if (!opts.deployed_url)
173
+ return fail("MISSING_ARG", "--deployed-url is required for 'shipped'");
174
+ }
175
+
176
+ if (target === "handed_off") {
177
+ const hFile = path.join(PLANNING, "HANDOFF.md");
178
+ if (!fs.existsSync(hFile))
179
+ return fail("MISSING_FILE", `Handoff file not found: ${hFile}`);
180
+ }
181
+
182
+ // Gap-closure circuit breaker
183
+ if (target === "planned" && current.status === "verified") {
184
+ const t = readTracking() || {};
185
+ const cycles = (t.gap_cycles || {})[String(phase)] || 0;
186
+ if (cycles >= 2) {
187
+ return fail(
188
+ "GAP_CYCLE_LIMIT",
189
+ `Phase ${phase} has failed verification ${cycles} times. Escalate to Fawzi or re-plan from scratch.`
190
+ );
191
+ }
192
+ }
193
+
194
+ return { ok: true };
195
+ }
196
+
197
+ function fail(error, message) {
198
+ return { ok: false, error, message };
199
+ }
200
+
201
+ // ─── Next Command Logic ──────────────────────────────────
202
+ function nextCommand(status, phase, totalPhases, verification) {
203
+ switch (status) {
204
+ case "setup":
205
+ return `/qualia-plan ${phase}`;
206
+ case "planned":
207
+ return `/qualia-build ${phase}`;
208
+ case "built":
209
+ return `/qualia-verify ${phase}`;
210
+ case "verified":
211
+ if (verification === "fail") return `/qualia-plan ${phase} --gaps`;
212
+ if (phase < totalPhases) return `/qualia-plan ${phase + 1}`;
213
+ return "/qualia-polish";
214
+ case "polished":
215
+ return "/qualia-ship";
216
+ case "shipped":
217
+ return "/qualia-handoff";
218
+ case "handed_off":
219
+ return "/qualia-report";
220
+ case "done":
221
+ return "Done.";
222
+ default:
223
+ return `/qualia`;
224
+ }
225
+ }
226
+
227
+ // ─── Commands ────────────────────────────────────────────
228
+
229
+ function cmdCheck(opts) {
230
+ const t = readTracking();
231
+ const s = parseStateMd(readState());
232
+ if (!t || !s) {
233
+ return output({
234
+ ok: false,
235
+ error: "NO_PROJECT",
236
+ message: "No .planning/ found. Run /qualia-new to start.",
237
+ });
238
+ }
239
+ output({
240
+ ok: true,
241
+ phase: s.phase,
242
+ phase_name: s.phase_name,
243
+ total_phases: s.total_phases,
244
+ status: s.status,
245
+ assigned_to: s.assigned_to,
246
+ verification: t.verification || "pending",
247
+ gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
248
+ tasks_done: t.tasks_done || 0,
249
+ tasks_total: t.tasks_total || 0,
250
+ deployed_url: t.deployed_url || "",
251
+ next_command: nextCommand(
252
+ s.status,
253
+ s.phase,
254
+ s.total_phases,
255
+ t.verification
256
+ ),
257
+ });
258
+ }
259
+
260
+ function cmdTransition(opts) {
261
+ const target = opts.to;
262
+ if (!target) return output(fail("MISSING_ARG", "--to is required"));
263
+
264
+ const t = readTracking();
265
+ const s = parseStateMd(readState());
266
+ if (!t || !s) {
267
+ return output(
268
+ fail("NO_PROJECT", "No .planning/ found. Run /qualia-new.")
269
+ );
270
+ }
271
+
272
+ // Special: note/activity (no status change)
273
+ if (target === "note" || target === "activity") {
274
+ const now = new Date().toISOString().split("T")[0];
275
+ if (opts.notes) t.notes = opts.notes;
276
+ t.last_updated = new Date().toISOString();
277
+ writeTracking(t);
278
+ s.last_activity = opts.notes || "Activity logged";
279
+ writeStateMd(s);
280
+ return output({
281
+ ok: true,
282
+ phase: s.phase,
283
+ status: s.status,
284
+ action: target,
285
+ });
286
+ }
287
+
288
+ const phase = parseInt(opts.phase) || s.phase;
289
+
290
+ // Precondition check
291
+ const check = checkPreconditions(
292
+ { ...s, phase },
293
+ target,
294
+ { ...opts, phase }
295
+ );
296
+ if (!check.ok) return output(check);
297
+
298
+ const prevStatus = s.status;
299
+
300
+ // Apply transition
301
+ s.status = target;
302
+ s.last_activity = `${target} (phase ${phase})`;
303
+
304
+ // Update tracking fields
305
+ t.status = target;
306
+ t.phase = phase;
307
+ t.phase_name = s.phases[phase - 1]?.name || s.phase_name;
308
+ t.last_updated = new Date().toISOString();
309
+
310
+ if (target === "planned") {
311
+ // Gap closure: increment counter if coming from verified(fail)
312
+ if (prevStatus === "verified") {
313
+ if (!t.gap_cycles) t.gap_cycles = {};
314
+ t.gap_cycles[String(phase)] = (t.gap_cycles[String(phase)] || 0) + 1;
315
+ s.last_activity = `Gap closure #${t.gap_cycles[String(phase)]} planned (phase ${phase})`;
316
+ }
317
+ // Update roadmap
318
+ if (s.phases[phase - 1]) s.phases[phase - 1].status = "planned";
319
+ }
320
+
321
+ if (target === "built") {
322
+ t.tasks_done = parseInt(opts.tasks_done) || 0;
323
+ t.tasks_total = parseInt(opts.tasks_total) || 0;
324
+ t.wave = parseInt(opts.wave) || 0;
325
+ s.last_activity = `Phase ${phase} built (${t.tasks_done}/${t.tasks_total} tasks)`;
326
+ if (s.phases[phase - 1]) s.phases[phase - 1].status = "built";
327
+ }
328
+
329
+ if (target === "verified") {
330
+ t.verification = opts.verification;
331
+ s.last_activity = `Phase ${phase} verified — ${opts.verification}`;
332
+ if (s.phases[phase - 1])
333
+ s.phases[phase - 1].status =
334
+ opts.verification === "pass" ? "verified" : "failed";
335
+
336
+ // Auto-advance on pass
337
+ if (opts.verification === "pass") {
338
+ if (phase < s.total_phases) {
339
+ s.phase = phase + 1;
340
+ s.phase_name = s.phases[phase]?.name || `Phase ${phase + 1}`;
341
+ s.status = "setup";
342
+ t.phase = s.phase;
343
+ t.phase_name = s.phase_name;
344
+ t.status = "setup";
345
+ t.verification = "pending";
346
+ t.tasks_done = 0;
347
+ t.tasks_total = 0;
348
+ s.last_activity = `Phase ${phase} passed — advancing to phase ${s.phase}`;
349
+ }
350
+ // Reset gap counter for the passed phase
351
+ if (t.gap_cycles) t.gap_cycles[String(phase)] = 0;
352
+ }
353
+ }
354
+
355
+ if (target === "polished") {
356
+ if (s.phases[s.phases.length - 1])
357
+ s.phases[s.phases.length - 1].status = "verified";
358
+ }
359
+
360
+ if (target === "shipped") {
361
+ t.deployed_url = opts.deployed_url || "";
362
+ }
363
+
364
+ // Write both files
365
+ const backupState = readState();
366
+ try {
367
+ writeStateMd(s);
368
+ writeTracking(t);
369
+ } catch (e) {
370
+ // Revert STATE.md on failure
371
+ if (backupState) fs.writeFileSync(STATE_FILE, backupState);
372
+ return output(fail("WRITE_ERROR", e.message));
373
+ }
374
+
375
+ output({
376
+ ok: true,
377
+ phase: s.phase,
378
+ phase_name: s.phase_name,
379
+ status: s.status,
380
+ previous_status: prevStatus,
381
+ verification: t.verification,
382
+ gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
383
+ next_command: nextCommand(s.status, s.phase, s.total_phases, t.verification),
384
+ });
385
+ }
386
+
387
+ function cmdInit(opts) {
388
+ if (!opts.project) return output(fail("MISSING_ARG", "--project required"));
389
+
390
+ // Parse phases
391
+ let phases = [];
392
+ if (opts.phases) {
393
+ try {
394
+ phases = JSON.parse(opts.phases);
395
+ } catch {
396
+ return output(fail("INVALID_ARG", "--phases must be valid JSON array"));
397
+ }
398
+ }
399
+ const totalPhases = parseInt(opts.total_phases) || phases.length || 1;
400
+
401
+ // Ensure phases array has entries
402
+ while (phases.length < totalPhases) {
403
+ phases.push({
404
+ name: `Phase ${phases.length + 1}`,
405
+ goal: "TBD",
406
+ });
407
+ }
408
+
409
+ // Create .planning/ if needed
410
+ if (!fs.existsSync(PLANNING)) fs.mkdirSync(PLANNING, { recursive: true });
411
+
412
+ const now = new Date().toISOString();
413
+ const date = now.split("T")[0];
414
+
415
+ // Build state
416
+ const s = {
417
+ phase: 1,
418
+ total_phases: totalPhases,
419
+ phase_name: phases[0].name,
420
+ status: "setup",
421
+ assigned_to: opts.assigned_to || "",
422
+ last_activity: `Project initialized`,
423
+ phases: phases.map((p, i) => ({
424
+ num: i + 1,
425
+ name: p.name,
426
+ goal: p.goal,
427
+ status: i === 0 ? "ready" : "—",
428
+ })),
429
+ blockers: "None.",
430
+ resume: "—",
431
+ };
432
+
433
+ // Build tracking
434
+ const t = {
435
+ project: opts.project,
436
+ client: opts.client || "",
437
+ type: opts.type || "",
438
+ assigned_to: opts.assigned_to || "",
439
+ phase: 1,
440
+ phase_name: phases[0].name,
441
+ total_phases: totalPhases,
442
+ status: "setup",
443
+ wave: 0,
444
+ tasks_done: 0,
445
+ tasks_total: 0,
446
+ verification: "pending",
447
+ gap_cycles: {},
448
+ blockers: [],
449
+ last_updated: now,
450
+ last_commit: "",
451
+ deployed_url: "",
452
+ notes: "",
453
+ };
454
+
455
+ writeStateMd(s);
456
+ writeTracking(t);
457
+
458
+ output({
459
+ ok: true,
460
+ action: "init",
461
+ project: opts.project,
462
+ phase: 1,
463
+ total_phases: totalPhases,
464
+ status: "setup",
465
+ next_command: "/qualia-plan 1",
466
+ });
467
+ }
468
+
469
+ // ─── Output ──────────────────────────────────────────────
470
+ function output(obj) {
471
+ console.log(JSON.stringify(obj, null, 2));
472
+ if (!obj.ok) process.exit(1);
473
+ }
474
+
475
+ // ─── Main ────────────────────────────────────────────────
476
+ const [cmd, ...rest] = process.argv.slice(2);
477
+ const opts = parseArgs(rest);
478
+
479
+ switch (cmd) {
480
+ case "check":
481
+ cmdCheck(opts);
482
+ break;
483
+ case "transition":
484
+ cmdTransition(opts);
485
+ break;
486
+ case "init":
487
+ cmdInit(opts);
488
+ break;
489
+ default:
490
+ output(
491
+ fail(
492
+ "UNKNOWN_COMMAND",
493
+ `Usage: state.js <check|transition|init> [--options]`
494
+ )
495
+ );
496
+ }
package/hooks/pre-push.sh CHANGED
@@ -1,35 +1,28 @@
1
1
  #!/bin/bash
2
- # Update tracking.json from STATE.md before push
2
+ # Update tracking.json timestamps before push
3
+ # State.js handles phase/status sync — this just updates commit hash and timestamp
3
4
 
4
5
  TRACKING=".planning/tracking.json"
5
- STATE=".planning/STATE.md"
6
6
 
7
- if [ -f "$STATE" ] && [ -f "$TRACKING" ]; then
8
- # Extract current phase from STATE.md
9
- PHASE=$(grep "^Phase:" "$STATE" | head -1 | sed 's/Phase: *//' | cut -d' ' -f1)
10
- STATUS=$(grep "^Status:" "$STATE" | head -1 | sed 's/Status: *//')
11
- LAST_COMMIT=$(git log --oneline -1 --format="%h" 2>/dev/null)
12
- NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
13
-
14
- # Update tracking.json with current values
15
- if ! command -v python3 &>/dev/null; then
16
- echo "WARNING: python3 not found — tracking.json not updated" >&2
7
+ if [ -f "$TRACKING" ]; then
8
+ if ! command -v node &>/dev/null; then
9
+ echo "WARNING: node not found, skipping tracking sync" >&2
17
10
  exit 0
18
11
  fi
19
12
 
20
- if ! python3 -c "
21
- import json
22
- with open('$TRACKING', 'r') as f:
23
- t = json.load(f)
24
- t['phase'] = int('${PHASE:-0}') if '${PHASE:-0}'.isdigit() else t.get('phase', 0)
25
- t['status'] = '${STATUS:-unknown}'.lower().replace(' ', '_')
26
- t['last_commit'] = '${LAST_COMMIT}'
27
- t['last_updated'] = '${NOW}'
28
- with open('$TRACKING', 'w') as f:
29
- json.dump(t, f, indent=2)
30
- " 2>/tmp/qualia-push-err.txt; then
31
- echo "WARNING: Failed to update tracking.json $(cat /tmp/qualia-push-err.txt)" >&2
32
- exit 0
33
- fi
13
+ LAST_COMMIT=$(git log --oneline -1 --format="%h" 2>/dev/null)
14
+ NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
15
+
16
+ node -e "
17
+ const fs = require('fs');
18
+ try {
19
+ const t = JSON.parse(fs.readFileSync('$TRACKING', 'utf8'));
20
+ t.last_commit = '${LAST_COMMIT}';
21
+ t.last_updated = '${NOW}';
22
+ fs.writeFileSync('$TRACKING', JSON.stringify(t, null, 2) + '\n');
23
+ } catch (e) {
24
+ process.stderr.write('WARNING: tracking sync failed: ' + e.message + '\n');
25
+ }
26
+ "
34
27
  git add "$TRACKING" 2>/dev/null
35
28
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework-v2",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "description": "Claude Code workflow framework by Qualia Solutions. Plan, build, verify, ship.",
5
5
  "bin": {
6
6
  "qualia-framework-v2": "./bin/cli.js"