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 +21 -6
- package/bin/install.js +25 -0
- package/bin/state.js +496 -0
- package/hooks/pre-push.sh +19 -26
- package/package.json +1 -1
- package/skills/qualia/SKILL.md +51 -28
- package/skills/qualia-build/SKILL.md +5 -2
- package/skills/qualia-debug/SKILL.md +73 -0
- package/skills/qualia-design/SKILL.md +89 -0
- package/skills/qualia-handoff/SKILL.md +4 -2
- package/skills/qualia-idk/SKILL.md +8 -0
- package/skills/qualia-new/SKILL.md +6 -2
- package/skills/qualia-pause/SKILL.md +59 -0
- package/skills/qualia-plan/SKILL.md +5 -2
- package/skills/qualia-polish/SKILL.md +4 -2
- package/skills/qualia-quick/SKILL.md +4 -1
- package/skills/qualia-report/SKILL.md +38 -32
- package/skills/qualia-resume/SKILL.md +44 -0
- package/skills/qualia-review/SKILL.md +72 -0
- package/skills/qualia-ship/SKILL.md +4 -2
- package/skills/qualia-task/SKILL.md +4 -1
- package/skills/qualia-verify/SKILL.md +7 -2
- package/templates/tracking.json +1 -0
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-
|
|
26
|
-
/qualia-
|
|
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
|
-
- **
|
|
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/
|
|
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
|
|
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 "$
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
t
|
|
27
|
-
t
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|