qualia-framework-v2 2.7.0 → 2.8.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 +8 -4
- package/bin/cli.js +14 -6
- package/bin/install.js +11 -21
- package/bin/qualia-ui.js +6 -3
- package/bin/statusline.js +202 -0
- package/package.json +3 -4
- package/skills/qualia-skill-new/SKILL.md +24 -5
- package/tests/state.test.sh +398 -0
- package/statusline.sh +0 -93
package/README.md
CHANGED
|
@@ -50,12 +50,16 @@ See `guide.md` for the full developer guide.
|
|
|
50
50
|
- **19 skills** — slash commands from setup to handoff, plus debugging, design, review, knowledge, session management, and skill authoring
|
|
51
51
|
- **4 agents** — planner, builder, verifier, qa-browser (each in fresh context)
|
|
52
52
|
- **8 hooks** — session start, branch guard, pre-push tracking sync, env protection, migration guard, deploy gate, pre-compact state save, auto-update (all Node.js — cross-platform)
|
|
53
|
-
- **
|
|
53
|
+
- **4 rules** — security, frontend, design-reference, deployment
|
|
54
54
|
- **5 templates** — tracking.json, state.md, project.md, plan.md, DESIGN.md
|
|
55
55
|
|
|
56
56
|
## Supported Platforms
|
|
57
57
|
|
|
58
|
-
Works on **Windows 10/11, macOS, and Linux
|
|
58
|
+
Works on **Windows 10/11, macOS, and Linux**. Requires Node.js 18+ and Claude Code.
|
|
59
|
+
|
|
60
|
+
- Every hook and the status line are pure Node.js — no external bash, jq, or GNU coreutils required.
|
|
61
|
+
- Skills are executed by Claude Code's own Bash tool (which Claude Code provides on all platforms, including Windows).
|
|
62
|
+
- Tested on Fedora, EndeavourOS, macOS, and Windows 10/11.
|
|
59
63
|
|
|
60
64
|
## Why It Works
|
|
61
65
|
|
|
@@ -69,7 +73,7 @@ Splitting planner, builder, and verifier into separate agents with separate cont
|
|
|
69
73
|
|
|
70
74
|
### Production-Grade Hooks
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
All 8 hooks are real ops engineering, not theoretical. Highlights:
|
|
73
77
|
|
|
74
78
|
- **Pre-deploy gate** — TypeScript, lint, tests, build, and `service_role` leak scan before `vercel --prod`
|
|
75
79
|
- **Branch guard** — Role-aware: owner can push to main, employees can't
|
|
@@ -100,7 +104,7 @@ npx qualia-framework-v2 install
|
|
|
100
104
|
├── agents/ planner.md, builder.md, verifier.md, qa-browser.md
|
|
101
105
|
├── hooks/ 8 Node.js hooks — cross-platform (no bash dependency)
|
|
102
106
|
├── bin/ state.js (state machine) + qualia-ui.js (cosmetics library)
|
|
103
|
-
├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md (
|
|
107
|
+
├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md (loaded by plan/debug/new)
|
|
104
108
|
├── rules/ security.md, frontend.md, deployment.md
|
|
105
109
|
├── qualia-templates/ tracking.json, state.md, project.md, plan.md, DESIGN.md
|
|
106
110
|
├── CLAUDE.md global instructions (role-configured per team member)
|
package/bin/cli.js
CHANGED
|
@@ -94,13 +94,21 @@ function cmdUpdate() {
|
|
|
94
94
|
console.log("");
|
|
95
95
|
|
|
96
96
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
const { spawnSync } = require("child_process");
|
|
98
|
+
const r = spawnSync("npx", ["qualia-framework-v2@latest", "install"], {
|
|
99
|
+
input: cfg.code + "\n",
|
|
100
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
101
|
+
shell: process.platform === "win32", // npx is a .cmd shim on Windows — must go through shell
|
|
102
|
+
timeout: 120000,
|
|
103
|
+
encoding: "utf8",
|
|
104
|
+
});
|
|
105
|
+
if (r.status !== 0) {
|
|
106
|
+
console.log(` ${RED}✗${RESET} Update failed. Run manually: npx qualia-framework-v2@latest install`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
102
109
|
} catch (e) {
|
|
103
|
-
console.log(` ${RED}✗${RESET} Update failed
|
|
110
|
+
console.log(` ${RED}✗${RESET} Update failed: ${e.message}`);
|
|
111
|
+
console.log(` ${DIM}Run manually:${RESET} npx qualia-framework-v2@latest install`);
|
|
104
112
|
process.exit(1);
|
|
105
113
|
}
|
|
106
114
|
}
|
package/bin/install.js
CHANGED
|
@@ -170,12 +170,11 @@ async function main() {
|
|
|
170
170
|
// ─── Status line ───────────────────────────────────────
|
|
171
171
|
log(`${WHITE}Status line${RESET}`);
|
|
172
172
|
try {
|
|
173
|
-
const slDest = path.join(CLAUDE_DIR, "statusline.
|
|
174
|
-
copy(path.join(FRAMEWORK_DIR, "statusline.
|
|
175
|
-
|
|
176
|
-
ok("statusline.sh");
|
|
173
|
+
const slDest = path.join(CLAUDE_DIR, "bin", "statusline.js");
|
|
174
|
+
copy(path.join(FRAMEWORK_DIR, "bin", "statusline.js"), slDest);
|
|
175
|
+
ok("statusline.js");
|
|
177
176
|
} catch (e) {
|
|
178
|
-
warn(`statusline.
|
|
177
|
+
warn(`statusline.js — ${e.message}`);
|
|
179
178
|
}
|
|
180
179
|
|
|
181
180
|
// ─── Templates ─────────────────────────────────────────
|
|
@@ -224,6 +223,11 @@ async function main() {
|
|
|
224
223
|
);
|
|
225
224
|
fs.chmodSync(path.join(binDest, "qualia-ui.js"), 0o755);
|
|
226
225
|
ok("qualia-ui.js (cosmetics library)");
|
|
226
|
+
copy(
|
|
227
|
+
path.join(FRAMEWORK_DIR, "bin", "statusline.js"),
|
|
228
|
+
path.join(binDest, "statusline.js")
|
|
229
|
+
);
|
|
230
|
+
ok("statusline.js (status bar renderer)");
|
|
227
231
|
} catch (e) {
|
|
228
232
|
warn(`scripts — ${e.message}`);
|
|
229
233
|
}
|
|
@@ -303,7 +307,7 @@ async function main() {
|
|
|
303
307
|
// Status line
|
|
304
308
|
settings.statusLine = {
|
|
305
309
|
type: "command",
|
|
306
|
-
command: "
|
|
310
|
+
command: `node "${path.join(CLAUDE_DIR, "bin", "statusline.js")}"`,
|
|
307
311
|
};
|
|
308
312
|
|
|
309
313
|
// Spinner
|
|
@@ -424,18 +428,6 @@ async function main() {
|
|
|
424
428
|
],
|
|
425
429
|
},
|
|
426
430
|
],
|
|
427
|
-
SubagentStart: [
|
|
428
|
-
{
|
|
429
|
-
matcher: ".*",
|
|
430
|
-
hooks: [
|
|
431
|
-
{
|
|
432
|
-
type: "command",
|
|
433
|
-
command:
|
|
434
|
-
'echo \'{"additionalContext": "◆ Qualia agent spawned"}\'',
|
|
435
|
-
},
|
|
436
|
-
],
|
|
437
|
-
},
|
|
438
|
-
],
|
|
439
431
|
};
|
|
440
432
|
|
|
441
433
|
// Permissions
|
|
@@ -449,8 +441,6 @@ async function main() {
|
|
|
449
441
|
];
|
|
450
442
|
}
|
|
451
443
|
|
|
452
|
-
settings.effortLevel = "high";
|
|
453
|
-
|
|
454
444
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
455
445
|
|
|
456
446
|
ok("Hooks: session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact");
|
|
@@ -467,7 +457,7 @@ async function main() {
|
|
|
467
457
|
console.log(` Agents: ${WHITE}${agentCount}${RESET} ${DIM}(planner, builder, verifier, qa-browser)${RESET}`);
|
|
468
458
|
console.log(` Hooks: ${WHITE}8${RESET} ${DIM}(session-start, auto-update, branch-guard, pre-push, env-block, migration-guard, deploy-gate, pre-compact)${RESET}`);
|
|
469
459
|
console.log(` Rules: ${WHITE}${fs.readdirSync(rulesDir).length}${RESET} ${DIM}(security, frontend, design-reference, deployment)${RESET}`);
|
|
470
|
-
console.log(` Scripts: ${WHITE}
|
|
460
|
+
console.log(` Scripts: ${WHITE}3${RESET} ${DIM}(state.js, qualia-ui.js, statusline.js)${RESET}`);
|
|
471
461
|
console.log(` Knowledge: ${WHITE}3${RESET} ${DIM}(patterns, fixes, client prefs)${RESET}`);
|
|
472
462
|
console.log(` Templates: ${WHITE}${fs.readdirSync(tmplDir).length}${RESET}`);
|
|
473
463
|
console.log(` Status line: ${GREEN}✓${RESET}`);
|
package/bin/qualia-ui.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
const fs = require("fs");
|
|
21
21
|
const path = require("path");
|
|
22
22
|
const os = require("os");
|
|
23
|
-
const {
|
|
23
|
+
const { spawnSync } = require("child_process");
|
|
24
24
|
|
|
25
25
|
// ─── Colors ──────────────────────────────────────────────
|
|
26
26
|
const TEAL = "\x1b[38;2;0;206;209m";
|
|
@@ -64,11 +64,14 @@ const ACTIONS = {
|
|
|
64
64
|
// ─── State Reading ───────────────────────────────────────
|
|
65
65
|
function readState() {
|
|
66
66
|
try {
|
|
67
|
-
const
|
|
67
|
+
const statePath = path.join(os.homedir(), ".claude", "bin", "state.js");
|
|
68
|
+
const r = spawnSync(process.execPath, [statePath, "check"], {
|
|
68
69
|
encoding: "utf8",
|
|
69
70
|
timeout: 3000,
|
|
71
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
70
72
|
});
|
|
71
|
-
|
|
73
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
74
|
+
return JSON.parse(r.stdout);
|
|
72
75
|
} catch {
|
|
73
76
|
return null;
|
|
74
77
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Qualia status line — teal branded, shows phase + context + git.
|
|
3
|
+
// Pure Node.js port of the original statusline.sh. Cross-platform
|
|
4
|
+
// (Windows/macOS/Linux). No jq, no GNU stat, no /tmp hardcoding.
|
|
5
|
+
//
|
|
6
|
+
// Reads JSON from stdin (Claude Code status line schema), prints
|
|
7
|
+
// two ANSI-formatted lines to stdout. Never throws — every section
|
|
8
|
+
// is wrapped so missing data degrades gracefully.
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const os = require("os");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const { spawnSync } = require("child_process");
|
|
14
|
+
|
|
15
|
+
// ─── Colors (matches bin/qualia-ui.js palette) ───────────
|
|
16
|
+
const TEAL = "\x1b[38;2;0;206;209m";
|
|
17
|
+
const TEAL_GLOW = "\x1b[38;2;0;170;175m";
|
|
18
|
+
const TEAL_DIM = "\x1b[38;2;0;130;135m";
|
|
19
|
+
const WHITE = "\x1b[38;2;220;225;230m";
|
|
20
|
+
const DIM = "\x1b[38;2;80;90;100m";
|
|
21
|
+
const GREEN = "\x1b[38;2;52;211;153m";
|
|
22
|
+
const YELLOW = "\x1b[38;2;234;179;8m";
|
|
23
|
+
const RED = "\x1b[38;2;239;68;68m";
|
|
24
|
+
const RESET = "\x1b[0m";
|
|
25
|
+
|
|
26
|
+
// ─── Read input ──────────────────────────────────────────
|
|
27
|
+
function readInput() {
|
|
28
|
+
try {
|
|
29
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const input = readInput();
|
|
37
|
+
|
|
38
|
+
function pick(obj, keypath, fallback) {
|
|
39
|
+
try {
|
|
40
|
+
let cur = obj;
|
|
41
|
+
for (const k of keypath.split(".")) {
|
|
42
|
+
if (cur == null) return fallback;
|
|
43
|
+
cur = cur[k];
|
|
44
|
+
}
|
|
45
|
+
return cur == null ? fallback : cur;
|
|
46
|
+
} catch {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MODEL = String(pick(input, "model.display_name", ""));
|
|
52
|
+
const DIR = String(pick(input, "workspace.current_dir", process.cwd()));
|
|
53
|
+
const PCT_RAW = Number(pick(input, "context_window.used_percentage", 0)) || 0;
|
|
54
|
+
const PCT = Math.floor(PCT_RAW);
|
|
55
|
+
const COST = Number(pick(input, "cost.total_cost_usd", 0)) || 0;
|
|
56
|
+
const DURATION_MS = Number(pick(input, "cost.total_duration_ms", 0)) || 0;
|
|
57
|
+
const AGENT = String(pick(input, "agent.name", "") || "");
|
|
58
|
+
const WORKTREE = String(pick(input, "worktree.name", "") || "");
|
|
59
|
+
|
|
60
|
+
// ─── Context bar ─────────────────────────────────────────
|
|
61
|
+
let BAR = "";
|
|
62
|
+
let BAR_COLOR = TEAL;
|
|
63
|
+
try {
|
|
64
|
+
if (PCT >= 80) BAR_COLOR = RED;
|
|
65
|
+
else if (PCT >= 50) BAR_COLOR = YELLOW;
|
|
66
|
+
else BAR_COLOR = TEAL;
|
|
67
|
+
|
|
68
|
+
const BAR_WIDTH = 10;
|
|
69
|
+
const filled = Math.max(0, Math.min(BAR_WIDTH, Math.floor((PCT * BAR_WIDTH) / 100)));
|
|
70
|
+
const empty = BAR_WIDTH - filled;
|
|
71
|
+
BAR = "━".repeat(filled) + "╌".repeat(empty);
|
|
72
|
+
} catch {
|
|
73
|
+
BAR = "╌".repeat(10);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Git branch (cached, cross-platform) ─────────────────
|
|
77
|
+
let BRANCH = "";
|
|
78
|
+
let CHANGES = 0;
|
|
79
|
+
try {
|
|
80
|
+
const username = (os.userInfo().username || "anon").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
81
|
+
const cacheFile = path.join(os.tmpdir(), `qualia-git-cache-${username}`);
|
|
82
|
+
|
|
83
|
+
let fresh = false;
|
|
84
|
+
try {
|
|
85
|
+
const st = fs.statSync(cacheFile);
|
|
86
|
+
if (Date.now() - st.mtimeMs <= 3000) fresh = true;
|
|
87
|
+
} catch {
|
|
88
|
+
fresh = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!fresh) {
|
|
92
|
+
let branch = "";
|
|
93
|
+
let changes = 0;
|
|
94
|
+
try {
|
|
95
|
+
const dirCheck = spawnSync("git", ["rev-parse", "--git-dir"], {
|
|
96
|
+
cwd: DIR,
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
timeout: 1000,
|
|
99
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
100
|
+
});
|
|
101
|
+
if (dirCheck.status === 0) {
|
|
102
|
+
const br = spawnSync("git", ["branch", "--show-current"], {
|
|
103
|
+
cwd: DIR,
|
|
104
|
+
encoding: "utf8",
|
|
105
|
+
timeout: 1000,
|
|
106
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
107
|
+
});
|
|
108
|
+
if (br.status === 0) branch = (br.stdout || "").trim();
|
|
109
|
+
|
|
110
|
+
const st = spawnSync("git", ["status", "--porcelain"], {
|
|
111
|
+
cwd: DIR,
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
timeout: 1000,
|
|
114
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
115
|
+
});
|
|
116
|
+
if (st.status === 0) {
|
|
117
|
+
const out = (st.stdout || "").trim();
|
|
118
|
+
changes = out ? out.split("\n").length : 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
try {
|
|
123
|
+
fs.writeFileSync(cacheFile, `${branch}|${changes}`);
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const cached = fs.readFileSync(cacheFile, "utf8");
|
|
129
|
+
const [b, c] = cached.split("|");
|
|
130
|
+
BRANCH = b || "";
|
|
131
|
+
CHANGES = parseInt(c, 10) || 0;
|
|
132
|
+
} catch {}
|
|
133
|
+
} catch {}
|
|
134
|
+
|
|
135
|
+
// ─── Phase info from .planning/tracking.json ─────────────
|
|
136
|
+
let PHASE_INFO = "";
|
|
137
|
+
try {
|
|
138
|
+
const trackingPath = path.join(DIR, ".planning", "tracking.json");
|
|
139
|
+
if (fs.existsSync(trackingPath)) {
|
|
140
|
+
const tracking = JSON.parse(fs.readFileSync(trackingPath, "utf8"));
|
|
141
|
+
const phase = Number(tracking.phase || 0) || 0;
|
|
142
|
+
const total = Number(tracking.total_phases || 0) || 0;
|
|
143
|
+
const status = String(tracking.status || "");
|
|
144
|
+
if (total > 0) {
|
|
145
|
+
const pdone = Math.floor((phase * 100) / total);
|
|
146
|
+
const pfill = Math.max(0, Math.min(4, Math.floor(pdone / 25)));
|
|
147
|
+
const pempt = 4 - pfill;
|
|
148
|
+
const pbar = "●".repeat(pfill) + "○".repeat(pempt);
|
|
149
|
+
PHASE_INFO = `${TEAL}${pbar}${RESET} ${WHITE}P${phase}/${total}${RESET} ${TEAL_GLOW}${status}${RESET}`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {}
|
|
153
|
+
|
|
154
|
+
// ─── Duration ────────────────────────────────────────────
|
|
155
|
+
let DUR = "0s";
|
|
156
|
+
try {
|
|
157
|
+
if (DURATION_MS >= 60000) {
|
|
158
|
+
DUR = `${Math.floor(DURATION_MS / 60000)}m`;
|
|
159
|
+
} else {
|
|
160
|
+
DUR = `${Math.floor(DURATION_MS / 1000)}s`;
|
|
161
|
+
}
|
|
162
|
+
} catch {}
|
|
163
|
+
|
|
164
|
+
// ─── Cost ────────────────────────────────────────────────
|
|
165
|
+
let COST_FMT = "$0.00";
|
|
166
|
+
try {
|
|
167
|
+
COST_FMT = `$${COST.toFixed(2)}`;
|
|
168
|
+
} catch {}
|
|
169
|
+
|
|
170
|
+
// ─── Line 1: Project + Git + Agent + Worktree + Phase ────
|
|
171
|
+
let LINE1 = "";
|
|
172
|
+
try {
|
|
173
|
+
const dirBase = path.basename(DIR) || DIR;
|
|
174
|
+
LINE1 = `${TEAL}◆${RESET} ${WHITE}${dirBase}${RESET}`;
|
|
175
|
+
if (BRANCH) {
|
|
176
|
+
if (CHANGES > 0) {
|
|
177
|
+
LINE1 += ` ${DIM}on${RESET} ${TEAL_GLOW}${BRANCH}${RESET} ${YELLOW}~${CHANGES}${RESET}`;
|
|
178
|
+
} else {
|
|
179
|
+
LINE1 += ` ${DIM}on${RESET} ${TEAL_GLOW}${BRANCH}${RESET}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (AGENT) LINE1 += ` ${DIM}│${RESET} ${TEAL}⚡${AGENT}${RESET}`;
|
|
183
|
+
if (WORKTREE) LINE1 += ` ${DIM}│${RESET} ${TEAL_DIM}⎇ ${WORKTREE}${RESET}`;
|
|
184
|
+
if (PHASE_INFO) LINE1 += ` ${DIM}│${RESET} ${PHASE_INFO}`;
|
|
185
|
+
} catch {
|
|
186
|
+
LINE1 = `${TEAL}◆${RESET} ${WHITE}qualia${RESET}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Line 2: Context bar + Cost + Duration + Model ───────
|
|
190
|
+
let LINE2 = "";
|
|
191
|
+
try {
|
|
192
|
+
LINE2 =
|
|
193
|
+
`${BAR_COLOR}${BAR}${RESET} ${DIM}${PCT}%${RESET} ` +
|
|
194
|
+
`${DIM}│${RESET} ${DIM}${COST_FMT}${RESET} ` +
|
|
195
|
+
`${DIM}│${RESET} ${DIM}${DUR}${RESET} ` +
|
|
196
|
+
`${DIM}│${RESET} ${TEAL_DIM}${MODEL}${RESET}`;
|
|
197
|
+
} catch {
|
|
198
|
+
LINE2 = `${DIM}${PCT}%${RESET}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
process.stdout.write(LINE1 + "\n");
|
|
202
|
+
process.stdout.write(LINE2 + "\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qualia-framework-v2",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.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"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"homepage": "https://github.com/qualia-solutions/qualia-framework-v2#readme",
|
|
25
25
|
"scripts": {
|
|
26
|
-
"test": "bash tests/hooks.test.sh"
|
|
26
|
+
"test": "bash tests/hooks.test.sh && bash tests/state.test.sh"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"bin/",
|
|
@@ -34,8 +34,7 @@
|
|
|
34
34
|
"templates/",
|
|
35
35
|
"tests/",
|
|
36
36
|
"CLAUDE.md",
|
|
37
|
-
"guide.md"
|
|
38
|
-
"statusline.sh"
|
|
37
|
+
"guide.md"
|
|
39
38
|
],
|
|
40
39
|
"engines": {
|
|
41
40
|
"node": ">=18"
|
|
@@ -29,9 +29,27 @@ options:
|
|
|
29
29
|
description: "This is a subagent role, not a slash command. Creates agents/{name}.md."
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
### 1a. Resolve framework directory
|
|
33
|
+
|
|
34
|
+
If the user chose **Framework skill** or a framework-scoped **Agent**, resolve `${FRAMEWORK_DIR}` — the checkout path of this user's qualia-framework-v2 repo — BEFORE computing any target paths. Never hardcode `/home/<user>/...`; different teammates and operating systems have different paths.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Priority order: env var → git detection → ask user
|
|
38
|
+
FRAMEWORK_DIR="${QUALIA_FRAMEWORK_DIR:-}"
|
|
39
|
+
if [ -z "$FRAMEWORK_DIR" ] && git -C . rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
40
|
+
ORIGIN=$(git -C . config --get remote.origin.url 2>/dev/null)
|
|
41
|
+
case "$ORIGIN" in
|
|
42
|
+
*qualia-framework-v2*) FRAMEWORK_DIR=$(git -C . rev-parse --show-toplevel) ;;
|
|
43
|
+
esac
|
|
44
|
+
fi
|
|
45
|
+
echo "${FRAMEWORK_DIR:-UNRESOLVED}"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If the command prints `UNRESOLVED`, ask the user: *"Where is your qualia-framework-v2 checkout? (absolute path, or type 'local' to save only to ~/.claude/)"*. If they type `local`, downgrade the scope to Local. Otherwise store the answer as `${FRAMEWORK_DIR}` for the rest of the session.
|
|
49
|
+
|
|
50
|
+
**Framework** → target: `${FRAMEWORK_DIR}/skills/{name}/SKILL.md`
|
|
33
51
|
**Local** → target: `~/.claude/skills/{name}/SKILL.md`
|
|
34
|
-
**Agent** → target:
|
|
52
|
+
**Agent** → target: `${FRAMEWORK_DIR}/agents/{name}.md` (framework) or `~/.claude/agents/{name}.md` (local)
|
|
35
53
|
|
|
36
54
|
### 2. Gather Requirements
|
|
37
55
|
|
|
@@ -119,10 +137,11 @@ Fix any ambiguity the test agent found.
|
|
|
119
137
|
|
|
120
138
|
```bash
|
|
121
139
|
# Framework skill — copy to local .claude for immediate testing
|
|
122
|
-
|
|
140
|
+
mkdir -p ~/.claude/skills/{name}
|
|
141
|
+
cp "${FRAMEWORK_DIR}/skills/{name}/SKILL.md" ~/.claude/skills/{name}/SKILL.md
|
|
123
142
|
|
|
124
143
|
# Verify it parses
|
|
125
|
-
node -e "const fs=require('fs');const c=fs.readFileSync('
|
|
144
|
+
node -e "const fs=require('fs');const os=require('os');const path=require('path');const c=fs.readFileSync(path.join(os.homedir(),'.claude/skills/{name}/SKILL.md'),'utf8');if(!c.includes('---'))throw new Error('missing frontmatter');if(!c.match(/^name:\s*\S/m))throw new Error('missing name');if(!c.match(/^description:\s*\S/m))throw new Error('missing description');console.log('OK')"
|
|
126
145
|
```
|
|
127
146
|
|
|
128
147
|
### 7. Commit (framework skills only)
|
|
@@ -131,7 +150,7 @@ Do NOT commit unless the user explicitly says "commit" or "ship it".
|
|
|
131
150
|
|
|
132
151
|
When they do:
|
|
133
152
|
```bash
|
|
134
|
-
cd
|
|
153
|
+
cd "${FRAMEWORK_DIR}"
|
|
135
154
|
git add skills/{name}/
|
|
136
155
|
git commit -m "feat: add /{name} skill"
|
|
137
156
|
```
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Qualia Framework v2 — state.js behavioral tests
|
|
3
|
+
# Run: bash tests/state.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
# Resolve STATE_JS to an ABSOLUTE path so `cd` inside subshells doesn't break it.
|
|
8
|
+
STATE_JS="$(cd "$(dirname "$0")/../bin" && pwd)/state.js"
|
|
9
|
+
NODE="${NODE:-node}"
|
|
10
|
+
|
|
11
|
+
# Track tmp dirs we create so we can clean them up on exit
|
|
12
|
+
TMP_DIRS=()
|
|
13
|
+
cleanup() {
|
|
14
|
+
for d in "${TMP_DIRS[@]}"; do
|
|
15
|
+
[ -d "$d" ] && rm -rf "$d"
|
|
16
|
+
done
|
|
17
|
+
}
|
|
18
|
+
trap cleanup EXIT
|
|
19
|
+
|
|
20
|
+
# Make a fresh temp project with 2 phases, already initialized.
|
|
21
|
+
# Prints the absolute path to the new tmp dir (does NOT cd).
|
|
22
|
+
make_project() {
|
|
23
|
+
local TMP
|
|
24
|
+
TMP=$(mktemp -d)
|
|
25
|
+
TMP_DIRS+=("$TMP")
|
|
26
|
+
(
|
|
27
|
+
cd "$TMP" || exit 1
|
|
28
|
+
$NODE "$STATE_JS" init \
|
|
29
|
+
--project "TestProject" \
|
|
30
|
+
--phases '[{"name":"Foundation","goal":"Auth"},{"name":"Core","goal":"Features"}]' \
|
|
31
|
+
>/dev/null 2>&1
|
|
32
|
+
)
|
|
33
|
+
echo "$TMP"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# pass "name" — record a passing assertion
|
|
37
|
+
pass() {
|
|
38
|
+
echo " ✓ $1"
|
|
39
|
+
PASS=$((PASS + 1))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# fail "name" "detail"
|
|
43
|
+
fail_case() {
|
|
44
|
+
echo " ✗ $1${2:+ — $2}"
|
|
45
|
+
FAIL=$((FAIL + 1))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
echo "=== state.js Behavioral Tests ==="
|
|
49
|
+
echo ""
|
|
50
|
+
|
|
51
|
+
# Sanity check
|
|
52
|
+
if [ ! -f "$STATE_JS" ]; then
|
|
53
|
+
echo "FATAL: state.js not found at $STATE_JS"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# ─── Basic I/O ───────────────────────────────────────────
|
|
58
|
+
echo "basic I/O:"
|
|
59
|
+
|
|
60
|
+
# 1. cmdInit produces valid tracking.json + STATE.md
|
|
61
|
+
TMP=$(mktemp -d); TMP_DIRS+=("$TMP")
|
|
62
|
+
(
|
|
63
|
+
cd "$TMP" || exit 1
|
|
64
|
+
$NODE "$STATE_JS" init \
|
|
65
|
+
--project "TestProject" \
|
|
66
|
+
--phases '[{"name":"Foundation","goal":"Auth"},{"name":"Core","goal":"Features"}]' \
|
|
67
|
+
>/tmp/qualia-state-test.out 2>&1
|
|
68
|
+
)
|
|
69
|
+
INIT_EXIT=$?
|
|
70
|
+
if [ "$INIT_EXIT" -eq 0 ] \
|
|
71
|
+
&& [ -f "$TMP/.planning/tracking.json" ] \
|
|
72
|
+
&& [ -f "$TMP/.planning/STATE.md" ] \
|
|
73
|
+
&& grep -q '"ok": true' /tmp/qualia-state-test.out \
|
|
74
|
+
&& grep -q '"action": "init"' /tmp/qualia-state-test.out; then
|
|
75
|
+
pass "cmdInit creates tracking.json + STATE.md"
|
|
76
|
+
else
|
|
77
|
+
fail_case "cmdInit creates tracking.json + STATE.md" "exit=$INIT_EXIT"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# tracking.json content sanity
|
|
81
|
+
if grep -q '"project": "TestProject"' "$TMP/.planning/tracking.json" \
|
|
82
|
+
&& grep -q '"total_phases": 2' "$TMP/.planning/tracking.json" \
|
|
83
|
+
&& grep -q '"phase": 1' "$TMP/.planning/tracking.json" \
|
|
84
|
+
&& grep -q '"status": "setup"' "$TMP/.planning/tracking.json"; then
|
|
85
|
+
pass "cmdInit tracking.json has correct fields"
|
|
86
|
+
else
|
|
87
|
+
fail_case "cmdInit tracking.json fields"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# STATE.md content sanity
|
|
91
|
+
if grep -q 'Phase: 1 of 2 — Foundation' "$TMP/.planning/STATE.md" \
|
|
92
|
+
&& grep -q 'Status: setup' "$TMP/.planning/STATE.md"; then
|
|
93
|
+
pass "cmdInit STATE.md has correct header"
|
|
94
|
+
else
|
|
95
|
+
fail_case "cmdInit STATE.md header"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# 2. cmdCheck reads back init state
|
|
99
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
|
|
100
|
+
CHECK_EXIT=$?
|
|
101
|
+
if [ "$CHECK_EXIT" -eq 0 ] \
|
|
102
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
103
|
+
&& echo "$OUT" | grep -q '"phase": 1' \
|
|
104
|
+
&& echo "$OUT" | grep -q '"status": "setup"' \
|
|
105
|
+
&& echo "$OUT" | grep -q '"total_phases": 2'; then
|
|
106
|
+
pass "cmdCheck returns phase=1 status=setup total_phases=2"
|
|
107
|
+
else
|
|
108
|
+
fail_case "cmdCheck returns init state" "exit=$CHECK_EXIT"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# 3. cmdCheck with no project → ok:false NO_PROJECT, exit 1
|
|
112
|
+
TMP2=$(mktemp -d); TMP_DIRS+=("$TMP2")
|
|
113
|
+
OUT=$(cd "$TMP2" && $NODE "$STATE_JS" check 2>&1)
|
|
114
|
+
CHECK_EXIT=$?
|
|
115
|
+
if [ "$CHECK_EXIT" -eq 1 ] \
|
|
116
|
+
&& echo "$OUT" | grep -q '"ok": false' \
|
|
117
|
+
&& echo "$OUT" | grep -q '"error": "NO_PROJECT"'; then
|
|
118
|
+
pass "cmdCheck without .planning → NO_PROJECT, exit 1"
|
|
119
|
+
else
|
|
120
|
+
fail_case "cmdCheck NO_PROJECT" "exit=$CHECK_EXIT"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# ─── Happy path transitions ──────────────────────────────
|
|
124
|
+
echo ""
|
|
125
|
+
echo "happy path transitions:"
|
|
126
|
+
|
|
127
|
+
# 4. setup → planned (with plan file)
|
|
128
|
+
TMP=$(make_project)
|
|
129
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
130
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
131
|
+
EXIT=$?
|
|
132
|
+
if [ "$EXIT" -eq 0 ] \
|
|
133
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
134
|
+
&& echo "$OUT" | grep -q '"status": "planned"' \
|
|
135
|
+
&& echo "$OUT" | grep -q '"previous_status": "setup"'; then
|
|
136
|
+
pass "setup → planned succeeds with plan file"
|
|
137
|
+
else
|
|
138
|
+
fail_case "setup → planned" "exit=$EXIT out=$OUT"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# 5. planned → built (records tasks_done/tasks_total)
|
|
142
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 5 --tasks-total 5 2>&1)
|
|
143
|
+
EXIT=$?
|
|
144
|
+
if [ "$EXIT" -eq 0 ] \
|
|
145
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
146
|
+
&& echo "$OUT" | grep -q '"status": "built"' \
|
|
147
|
+
&& grep -q '"tasks_done": 5' "$TMP/.planning/tracking.json" \
|
|
148
|
+
&& grep -q '"tasks_total": 5' "$TMP/.planning/tracking.json"; then
|
|
149
|
+
pass "planned → built records tasks_done/tasks_total"
|
|
150
|
+
else
|
|
151
|
+
fail_case "planned → built" "exit=$EXIT"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# 6. built → verified(pass) auto-advances to phase 2, resets status to setup
|
|
155
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
156
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
|
|
157
|
+
EXIT=$?
|
|
158
|
+
if [ "$EXIT" -eq 0 ] \
|
|
159
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
160
|
+
&& echo "$OUT" | grep -q '"phase": 2' \
|
|
161
|
+
&& echo "$OUT" | grep -q '"status": "setup"'; then
|
|
162
|
+
pass "built → verified(pass) auto-advances phase and resets to setup"
|
|
163
|
+
else
|
|
164
|
+
fail_case "built → verified(pass) auto-advance" "exit=$EXIT out=$OUT"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# 7. built → verified(fail) stays on phase 1, records verification=fail
|
|
168
|
+
TMP=$(make_project)
|
|
169
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
170
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
171
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 3 --tasks-total 5 >/dev/null 2>&1)
|
|
172
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
173
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail 2>&1)
|
|
174
|
+
EXIT=$?
|
|
175
|
+
if [ "$EXIT" -eq 0 ] \
|
|
176
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
177
|
+
&& echo "$OUT" | grep -q '"phase": 1' \
|
|
178
|
+
&& echo "$OUT" | grep -q '"status": "verified"' \
|
|
179
|
+
&& echo "$OUT" | grep -q '"verification": "fail"'; then
|
|
180
|
+
pass "built → verified(fail) stays on phase 1"
|
|
181
|
+
else
|
|
182
|
+
fail_case "built → verified(fail)" "exit=$EXIT out=$OUT"
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# ─── Precondition failures ───────────────────────────────
|
|
186
|
+
echo ""
|
|
187
|
+
echo "precondition failures:"
|
|
188
|
+
|
|
189
|
+
# 8. setup → built fails with PRECONDITION_FAILED
|
|
190
|
+
TMP=$(make_project)
|
|
191
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to built 2>&1)
|
|
192
|
+
EXIT=$?
|
|
193
|
+
if [ "$EXIT" -eq 1 ] \
|
|
194
|
+
&& echo "$OUT" | grep -q '"ok": false' \
|
|
195
|
+
&& echo "$OUT" | grep -q '"error": "PRECONDITION_FAILED"' \
|
|
196
|
+
&& echo "$OUT" | grep -q "Cannot go from 'setup' to 'built'"; then
|
|
197
|
+
pass "setup → built fails with PRECONDITION_FAILED"
|
|
198
|
+
else
|
|
199
|
+
fail_case "setup → built precondition" "exit=$EXIT out=$OUT"
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# 9. planned → verified fails (requires status=built)
|
|
203
|
+
TMP=$(make_project)
|
|
204
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
205
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
206
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
207
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
|
|
208
|
+
EXIT=$?
|
|
209
|
+
if [ "$EXIT" -eq 1 ] \
|
|
210
|
+
&& echo "$OUT" | grep -q '"error": "PRECONDITION_FAILED"' \
|
|
211
|
+
&& echo "$OUT" | grep -q "Cannot go from 'planned' to 'verified'"; then
|
|
212
|
+
pass "planned → verified fails (requires built)"
|
|
213
|
+
else
|
|
214
|
+
fail_case "planned → verified precondition" "exit=$EXIT out=$OUT"
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# 10. planned with missing plan file → MISSING_FILE
|
|
218
|
+
TMP=$(make_project)
|
|
219
|
+
# no phase-1-plan.md created
|
|
220
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
221
|
+
EXIT=$?
|
|
222
|
+
if [ "$EXIT" -eq 1 ] \
|
|
223
|
+
&& echo "$OUT" | grep -q '"error": "MISSING_FILE"' \
|
|
224
|
+
&& echo "$OUT" | grep -q "phase-1-plan.md"; then
|
|
225
|
+
pass "setup → planned fails without plan file (MISSING_FILE)"
|
|
226
|
+
else
|
|
227
|
+
fail_case "setup → planned MISSING_FILE" "exit=$EXIT out=$OUT"
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# 11. built → verified with missing verification file → MISSING_FILE
|
|
231
|
+
TMP=$(make_project)
|
|
232
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
233
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
234
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
235
|
+
# NO verification file
|
|
236
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
|
|
237
|
+
EXIT=$?
|
|
238
|
+
if [ "$EXIT" -eq 1 ] \
|
|
239
|
+
&& echo "$OUT" | grep -q '"error": "MISSING_FILE"' \
|
|
240
|
+
&& echo "$OUT" | grep -q "phase-1-verification.md"; then
|
|
241
|
+
pass "built → verified fails without verification file (MISSING_FILE)"
|
|
242
|
+
else
|
|
243
|
+
fail_case "built → verified MISSING_FILE" "exit=$EXIT out=$OUT"
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# 12. built → verified without --verification → MISSING_ARG
|
|
247
|
+
TMP=$(make_project)
|
|
248
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
249
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
250
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
251
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
252
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified 2>&1)
|
|
253
|
+
EXIT=$?
|
|
254
|
+
if [ "$EXIT" -eq 1 ] \
|
|
255
|
+
&& echo "$OUT" | grep -q '"error": "MISSING_ARG"' \
|
|
256
|
+
&& echo "$OUT" | grep -q "verification"; then
|
|
257
|
+
pass "built → verified without --verification → MISSING_ARG"
|
|
258
|
+
else
|
|
259
|
+
fail_case "built → verified MISSING_ARG" "exit=$EXIT out=$OUT"
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
# 13. → shipped without --deployed-url → MISSING_ARG
|
|
263
|
+
# Must go through polished first, so fabricate state by transitioning through the full path.
|
|
264
|
+
TMP=$(make_project)
|
|
265
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
266
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
267
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
268
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
269
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
270
|
+
# Now on phase 2, status=setup. Run phase 2 to completion.
|
|
271
|
+
touch "$TMP/.planning/phase-2-plan.md"
|
|
272
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
273
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
274
|
+
touch "$TMP/.planning/phase-2-verification.md"
|
|
275
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
276
|
+
# Status should now be "verified" on last phase (no auto-advance past last phase)
|
|
277
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to polished >/dev/null 2>&1)
|
|
278
|
+
# Now try ship without deployed-url
|
|
279
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to shipped 2>&1)
|
|
280
|
+
EXIT=$?
|
|
281
|
+
if [ "$EXIT" -eq 1 ] \
|
|
282
|
+
&& echo "$OUT" | grep -q '"error": "MISSING_ARG"' \
|
|
283
|
+
&& echo "$OUT" | grep -q "deployed-url"; then
|
|
284
|
+
pass "→ shipped without --deployed-url → MISSING_ARG"
|
|
285
|
+
else
|
|
286
|
+
fail_case "→ shipped MISSING_ARG" "exit=$EXIT out=$OUT"
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
# 14. Unknown target --to frobnicate → INVALID_STATUS
|
|
290
|
+
TMP=$(make_project)
|
|
291
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to frobnicate 2>&1)
|
|
292
|
+
EXIT=$?
|
|
293
|
+
if [ "$EXIT" -eq 1 ] \
|
|
294
|
+
&& echo "$OUT" | grep -q '"error": "INVALID_STATUS"'; then
|
|
295
|
+
pass "--to frobnicate → INVALID_STATUS"
|
|
296
|
+
else
|
|
297
|
+
fail_case "invalid target" "exit=$EXIT out=$OUT"
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
# ─── Gap cycle circuit breaker ───────────────────────────
|
|
301
|
+
echo ""
|
|
302
|
+
echo "gap cycle circuit breaker:"
|
|
303
|
+
|
|
304
|
+
# 15. First gap closure: verified(fail) → planned, gap_cycles[1]=1
|
|
305
|
+
TMP=$(make_project)
|
|
306
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
307
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
308
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
309
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
310
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
311
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
312
|
+
EXIT=$?
|
|
313
|
+
if [ "$EXIT" -eq 0 ] \
|
|
314
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
315
|
+
&& echo "$OUT" | grep -q '"gap_cycles": 1'; then
|
|
316
|
+
pass "first gap closure: verified(fail) → planned, gap_cycles=1"
|
|
317
|
+
else
|
|
318
|
+
fail_case "first gap closure" "exit=$EXIT out=$OUT"
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
# 16. Second gap closure: gap_cycles[1]=2
|
|
322
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
323
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
324
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
325
|
+
EXIT=$?
|
|
326
|
+
if [ "$EXIT" -eq 0 ] \
|
|
327
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
328
|
+
&& echo "$OUT" | grep -q '"gap_cycles": 2'; then
|
|
329
|
+
pass "second gap closure: gap_cycles=2"
|
|
330
|
+
else
|
|
331
|
+
fail_case "second gap closure" "exit=$EXIT out=$OUT"
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
# 17. Third gap closure attempt → GAP_CYCLE_LIMIT
|
|
335
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
336
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
337
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
338
|
+
EXIT=$?
|
|
339
|
+
if [ "$EXIT" -eq 1 ] \
|
|
340
|
+
&& echo "$OUT" | grep -q '"error": "GAP_CYCLE_LIMIT"'; then
|
|
341
|
+
pass "third gap closure attempt blocked (GAP_CYCLE_LIMIT)"
|
|
342
|
+
else
|
|
343
|
+
fail_case "gap cycle limit" "exit=$EXIT out=$OUT"
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
# 18. verified(pass) resets gap_cycles[1] to 0
|
|
347
|
+
# Set up a fresh project, do ONE failed cycle, then pass on the next attempt.
|
|
348
|
+
TMP=$(make_project)
|
|
349
|
+
touch "$TMP/.planning/phase-1-plan.md"
|
|
350
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
351
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
352
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
353
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
354
|
+
# gap_cycles[1] is now 0 before the gap closure; becomes 1 after
|
|
355
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
356
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
357
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
358
|
+
# After pass, gap_cycles[1] should be reset to 0 in tracking.json
|
|
359
|
+
if grep -q '"1": 0' "$TMP/.planning/tracking.json"; then
|
|
360
|
+
pass "verified(pass) resets gap_cycles[1] to 0"
|
|
361
|
+
else
|
|
362
|
+
fail_case "gap cycle reset on pass"
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
# ─── Special transitions ─────────────────────────────────
|
|
366
|
+
echo ""
|
|
367
|
+
echo "special transitions:"
|
|
368
|
+
|
|
369
|
+
# 19. --to note --notes "foo" succeeds, records notes
|
|
370
|
+
TMP=$(make_project)
|
|
371
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "hello world" 2>&1)
|
|
372
|
+
EXIT=$?
|
|
373
|
+
if [ "$EXIT" -eq 0 ] \
|
|
374
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
375
|
+
&& echo "$OUT" | grep -q '"action": "note"' \
|
|
376
|
+
&& echo "$OUT" | grep -q '"status": "setup"' \
|
|
377
|
+
&& grep -q '"notes": "hello world"' "$TMP/.planning/tracking.json"; then
|
|
378
|
+
pass "--to note records notes, status unchanged"
|
|
379
|
+
else
|
|
380
|
+
fail_case "--to note" "exit=$EXIT out=$OUT"
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
# 20. --to activity succeeds without status change
|
|
384
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to activity 2>&1)
|
|
385
|
+
EXIT=$?
|
|
386
|
+
if [ "$EXIT" -eq 0 ] \
|
|
387
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
388
|
+
&& echo "$OUT" | grep -q '"action": "activity"' \
|
|
389
|
+
&& echo "$OUT" | grep -q '"status": "setup"'; then
|
|
390
|
+
pass "--to activity succeeds without status change"
|
|
391
|
+
else
|
|
392
|
+
fail_case "--to activity" "exit=$EXIT out=$OUT"
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
# ─── Summary ─────────────────────────────────────────────
|
|
396
|
+
echo ""
|
|
397
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
398
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
package/statusline.sh
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Qualia status line — teal branded, shows phase + context + git
|
|
3
|
-
input=$(cat)
|
|
4
|
-
|
|
5
|
-
MODEL=$(echo "$input" | jq -r '.model.display_name')
|
|
6
|
-
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
|
|
7
|
-
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
|
|
8
|
-
COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
|
|
9
|
-
DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
|
|
10
|
-
AGENT=$(echo "$input" | jq -r '.agent.name // empty')
|
|
11
|
-
WORKTREE=$(echo "$input" | jq -r '.worktree.name // empty')
|
|
12
|
-
|
|
13
|
-
# Teal palette
|
|
14
|
-
T='\033[38;2;0;206;209m' # Primary teal
|
|
15
|
-
TG='\033[38;2;0;170;175m' # Teal glow (darker)
|
|
16
|
-
TD='\033[38;2;0;130;135m' # Teal dim
|
|
17
|
-
W='\033[38;2;220;225;230m' # White
|
|
18
|
-
DIM='\033[38;2;80;90;100m' # Dim gray
|
|
19
|
-
GREEN='\033[38;2;52;211;153m' # Success green
|
|
20
|
-
YELLOW='\033[38;2;234;179;8m' # Warning
|
|
21
|
-
RED='\033[38;2;239;68;68m' # Error
|
|
22
|
-
RESET='\033[0m'
|
|
23
|
-
|
|
24
|
-
# Context bar with teal gradient
|
|
25
|
-
if [ "$PCT" -ge 80 ]; then BAR_COLOR="$RED"
|
|
26
|
-
elif [ "$PCT" -ge 50 ]; then BAR_COLOR="$YELLOW"
|
|
27
|
-
else BAR_COLOR="$T"; fi
|
|
28
|
-
|
|
29
|
-
BAR_WIDTH=10
|
|
30
|
-
FILLED=$((PCT * BAR_WIDTH / 100))
|
|
31
|
-
EMPTY=$((BAR_WIDTH - FILLED))
|
|
32
|
-
BAR=""
|
|
33
|
-
[ "$FILLED" -gt 0 ] && printf -v FILL "%${FILLED}s" && BAR="${FILL// /━}"
|
|
34
|
-
[ "$EMPTY" -gt 0 ] && printf -v PAD "%${EMPTY}s" && BAR="${BAR}${PAD// /╌}"
|
|
35
|
-
|
|
36
|
-
# Git branch (cached for speed)
|
|
37
|
-
CACHE="/tmp/qualia-git-cache"
|
|
38
|
-
if [ ! -f "$CACHE" ] || [ $(($(date +%s) - $(stat -c %Y "$CACHE" 2>/dev/null || echo 0))) -gt 3 ]; then
|
|
39
|
-
BRANCH=""
|
|
40
|
-
CHANGES=0
|
|
41
|
-
if git rev-parse --git-dir > /dev/null 2>&1; then
|
|
42
|
-
BRANCH=$(git branch --show-current 2>/dev/null)
|
|
43
|
-
CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
44
|
-
fi
|
|
45
|
-
echo "$BRANCH|$CHANGES" > "$CACHE"
|
|
46
|
-
fi
|
|
47
|
-
IFS='|' read -r BRANCH CHANGES < "$CACHE"
|
|
48
|
-
|
|
49
|
-
# Qualia phase from tracking.json
|
|
50
|
-
PHASE_INFO=""
|
|
51
|
-
TRACKING=".planning/tracking.json"
|
|
52
|
-
if [ -f "$TRACKING" ]; then
|
|
53
|
-
PHASE=$(jq -r '.phase // 0' "$TRACKING" 2>/dev/null)
|
|
54
|
-
TOTAL=$(jq -r '.total_phases // 0' "$TRACKING" 2>/dev/null)
|
|
55
|
-
STATUS=$(jq -r '.status // ""' "$TRACKING" 2>/dev/null)
|
|
56
|
-
if [ "$TOTAL" -gt 0 ]; then
|
|
57
|
-
# Phase progress mini-bar
|
|
58
|
-
PDONE=$((PHASE * 100 / TOTAL))
|
|
59
|
-
PFILL=$((PDONE / 25))
|
|
60
|
-
PEMPT=$((4 - PFILL))
|
|
61
|
-
PBAR=""
|
|
62
|
-
[ "$PFILL" -gt 0 ] && printf -v PF "%${PFILL}s" && PBAR="${PF// /●}"
|
|
63
|
-
[ "$PEMPT" -gt 0 ] && printf -v PE "%${PEMPT}s" && PBAR="${PBAR}${PE// /○}"
|
|
64
|
-
PHASE_INFO="${T}${PBAR}${RESET} ${W}P${PHASE}/${TOTAL}${RESET} ${TG}${STATUS}${RESET}"
|
|
65
|
-
fi
|
|
66
|
-
fi
|
|
67
|
-
|
|
68
|
-
# Duration
|
|
69
|
-
MINS=$((DURATION_MS / 60000))
|
|
70
|
-
SECS=$(((DURATION_MS % 60000) / 1000))
|
|
71
|
-
[ "$MINS" -gt 0 ] && DUR="${MINS}m" || DUR="${SECS}s"
|
|
72
|
-
|
|
73
|
-
# Cost
|
|
74
|
-
COST_FMT=$(printf '$%.2f' "$COST")
|
|
75
|
-
|
|
76
|
-
# Line 1: Project + Git + Phase
|
|
77
|
-
LINE1="${T}◆${RESET} ${W}${DIR##*/}${RESET}"
|
|
78
|
-
if [ -n "$BRANCH" ]; then
|
|
79
|
-
if [ "$CHANGES" -gt 0 ]; then
|
|
80
|
-
LINE1="${LINE1} ${DIM}on${RESET} ${TG}${BRANCH}${RESET} ${YELLOW}~${CHANGES}${RESET}"
|
|
81
|
-
else
|
|
82
|
-
LINE1="${LINE1} ${DIM}on${RESET} ${TG}${BRANCH}${RESET}"
|
|
83
|
-
fi
|
|
84
|
-
fi
|
|
85
|
-
[ -n "$AGENT" ] && LINE1="${LINE1} ${DIM}│${RESET} ${T}⚡${AGENT}${RESET}"
|
|
86
|
-
[ -n "$WORKTREE" ] && LINE1="${LINE1} ${DIM}│${RESET} ${TD}⎇ ${WORKTREE}${RESET}"
|
|
87
|
-
[ -n "$PHASE_INFO" ] && LINE1="${LINE1} ${DIM}│${RESET} ${PHASE_INFO}"
|
|
88
|
-
|
|
89
|
-
# Line 2: Context bar + Cost + Duration + Model
|
|
90
|
-
LINE2="${BAR_COLOR}${BAR}${RESET} ${DIM}${PCT}%${RESET} ${DIM}│${RESET} ${DIM}${COST_FMT}${RESET} ${DIM}│${RESET} ${DIM}${DUR}${RESET} ${DIM}│${RESET} ${TD}${MODEL}${RESET}"
|
|
91
|
-
|
|
92
|
-
printf '%b\n' "$LINE1"
|
|
93
|
-
printf '%b\n' "$LINE2"
|