qualia-framework 4.0.0 → 4.0.3
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/CLAUDE.md +23 -11
- package/agents/plan-checker.md +1 -1
- package/bin/cli.js +18 -13
- package/bin/install.js +34 -45
- package/bin/qualia-ui.js +2 -2
- package/bin/state.js +74 -7
- package/bin/statusline.js +4 -1
- package/docs/erp-contract.md +12 -0
- package/guide.md +1 -1
- package/hooks/migration-guard.js +23 -9
- package/hooks/pre-compact.js +39 -11
- package/hooks/pre-deploy-gate.js +3 -4
- package/hooks/pre-push.js +6 -3
- package/hooks/session-start.js +8 -8
- package/package.json +1 -1
- package/rules/frontend.md +5 -13
- package/skills/qualia/SKILL.md +5 -0
- package/skills/qualia-build/SKILL.md +10 -0
- package/skills/qualia-debug/SKILL.md +6 -0
- package/skills/qualia-design/SKILL.md +9 -1
- package/skills/qualia-discuss/SKILL.md +6 -0
- package/skills/qualia-handoff/SKILL.md +5 -0
- package/skills/qualia-help/SKILL.md +18 -4
- package/skills/qualia-idk/SKILL.md +6 -0
- package/skills/qualia-learn/SKILL.md +6 -0
- package/skills/qualia-map/SKILL.md +7 -0
- package/skills/qualia-milestone/SKILL.md +6 -0
- package/skills/qualia-new/SKILL.md +13 -1
- package/skills/qualia-optimize/SKILL.md +8 -0
- package/skills/qualia-pause/SKILL.md +5 -0
- package/skills/qualia-plan/SKILL.md +11 -1
- package/skills/qualia-polish/SKILL.md +8 -0
- package/skills/qualia-quick/SKILL.md +7 -0
- package/skills/qualia-report/SKILL.md +5 -0
- package/skills/qualia-research/SKILL.md +7 -0
- package/skills/qualia-resume/SKILL.md +3 -0
- package/skills/qualia-review/SKILL.md +7 -0
- package/skills/qualia-ship/SKILL.md +5 -0
- package/skills/qualia-skill-new/SKILL.md +6 -0
- package/skills/qualia-task/SKILL.md +8 -1
- package/skills/qualia-test/SKILL.md +7 -0
- package/skills/qualia-verify/SKILL.md +8 -0
- package/templates/help.html +4 -4
- package/tests/hooks.test.sh +5 -5
- package/tests/runner.js +212 -3
package/CLAUDE.md
CHANGED
|
@@ -23,25 +23,37 @@ Next.js 16+, React 19, TypeScript, Supabase, Vercel. Voice: Retell AI, ElevenLab
|
|
|
23
23
|
|
|
24
24
|
## The Road (how projects flow)
|
|
25
25
|
|
|
26
|
+
v4 hierarchy: **Project → Journey → Milestones (2–5, Handoff always last) → Phases (2–5 tasks each) → Tasks (one commit, one verification contract).**
|
|
27
|
+
|
|
26
28
|
```
|
|
27
|
-
/qualia-new
|
|
29
|
+
/qualia-new → kickoff + parallel research + JOURNEY.md (all milestones upfront)
|
|
30
|
+
add --auto to chain the whole road end-to-end
|
|
28
31
|
↓
|
|
29
|
-
For each phase:
|
|
30
|
-
/qualia-plan
|
|
31
|
-
/qualia-build
|
|
32
|
-
/qualia-verify
|
|
32
|
+
For each milestone, for each phase:
|
|
33
|
+
/qualia-plan → plan the phase (planner + plan-checker revision loop, fresh context)
|
|
34
|
+
/qualia-build → build it (builder subagents per task, wave-based parallel)
|
|
35
|
+
/qualia-verify → goal-backward check (verifier agent, fresh context)
|
|
33
36
|
↓
|
|
34
|
-
/qualia-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/qualia-milestone → close milestone, archive artifacts, prep next (human gate)
|
|
38
|
+
↓ (repeat for each milestone until Handoff)
|
|
39
|
+
Final milestone = Handoff:
|
|
40
|
+
/qualia-polish → design/UX pass (Phase 1 of Handoff)
|
|
41
|
+
(content + SEO) → Phase 2
|
|
42
|
+
(final QA) → Phase 3
|
|
43
|
+
/qualia-ship → deploy to production (quality gates → deploy → verify)
|
|
44
|
+
/qualia-handoff → 4 deliverables: credentials, doc, final update, report
|
|
37
45
|
↓
|
|
38
46
|
Done.
|
|
39
47
|
|
|
40
|
-
Lost?
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
Lost? → /qualia (state router — tells you the next command)
|
|
49
|
+
Stuck/weird? → /qualia-idk (diagnostic — spawns plan-view + code-view agents in parallel)
|
|
50
|
+
Quick fix? → /qualia-quick (skip planning for small tasks)
|
|
51
|
+
Paused? → /qualia-resume (restore from .continue-here.md or STATE.md)
|
|
52
|
+
End of day? → /qualia-report (mandatory before clock-out; writes ERP payload)
|
|
43
53
|
```
|
|
44
54
|
|
|
55
|
+
**Human gates:** journey approval after `/qualia-new`, then one at each milestone boundary via `/qualia-milestone`. `--auto` runs everything between gates automatically.
|
|
56
|
+
|
|
45
57
|
## Context Isolation
|
|
46
58
|
Every task runs in a fresh subagent context. Task 50 gets the same quality as Task 1.
|
|
47
59
|
- Planner gets: PROJECT.md + phase requirements
|
package/agents/plan-checker.md
CHANGED
|
@@ -34,7 +34,7 @@ Plan must have YAML frontmatter with:
|
|
|
34
34
|
|
|
35
35
|
**FAIL if:** frontmatter missing, incomplete, or `goal` differs from ROADMAP.md.
|
|
36
36
|
|
|
37
|
-
### Rule 2: Every task has the
|
|
37
|
+
### Rule 2: Every task has the 7 mandatory story-file fields
|
|
38
38
|
|
|
39
39
|
Each `## Task N — title` block must include ALL of these:
|
|
40
40
|
|
package/bin/cli.js
CHANGED
|
@@ -126,18 +126,23 @@ function cmdUpdate() {
|
|
|
126
126
|
// non-Qualia entries in settings.json (other hooks, user env vars, etc.).
|
|
127
127
|
// --yes / -y skips the confirmation prompt for scripted use.
|
|
128
128
|
|
|
129
|
-
//
|
|
130
|
-
// any other hooks the user dropped in there are left alone.
|
|
129
|
+
// Current Qualia hook filenames — only these are removed from ~/.claude/hooks/,
|
|
130
|
+
// any other hooks the user dropped in there are left alone. The LEGACY set
|
|
131
|
+
// lists hooks that were shipped by older framework versions but have since
|
|
132
|
+
// been removed; uninstall still tries to clean them so old installs get a
|
|
133
|
+
// clean removal.
|
|
131
134
|
const QUALIA_HOOK_FILES = [
|
|
132
135
|
"session-start.js",
|
|
133
136
|
"auto-update.js",
|
|
134
137
|
"branch-guard.js",
|
|
135
138
|
"pre-push.js",
|
|
136
|
-
"block-env-edit.js",
|
|
137
139
|
"migration-guard.js",
|
|
138
140
|
"pre-deploy-gate.js",
|
|
139
141
|
"pre-compact.js",
|
|
140
142
|
];
|
|
143
|
+
const QUALIA_LEGACY_HOOK_FILES = [
|
|
144
|
+
"block-env-edit.js", // removed in v3.2.0
|
|
145
|
+
];
|
|
141
146
|
|
|
142
147
|
// 4 Qualia agents — only these are removed.
|
|
143
148
|
const QUALIA_AGENT_FILES = ["planner.md", "builder.md", "verifier.md", "qa-browser.md"];
|
|
@@ -210,14 +215,14 @@ function cleanSettingsJson(counters) {
|
|
|
210
215
|
};
|
|
211
216
|
|
|
212
217
|
if (settings.hooks && typeof settings.hooks === "object") {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
// Iterate every hook event key, not a hardcoded subset — future hook
|
|
219
|
+
// events added by Claude Code or the framework get cleaned automatically.
|
|
220
|
+
for (const key of Object.keys(settings.hooks)) {
|
|
221
|
+
const cleaned = filterHookArray(settings.hooks[key]);
|
|
222
|
+
if (cleaned && cleaned.length > 0) {
|
|
223
|
+
settings.hooks[key] = cleaned;
|
|
224
|
+
} else {
|
|
225
|
+
delete settings.hooks[key];
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
228
|
// If hooks is now empty, remove it entirely.
|
|
@@ -305,8 +310,8 @@ async function cmdUninstall() {
|
|
|
305
310
|
safeUnlink(path.join(CLAUDE_DIR, "agents", f), counters);
|
|
306
311
|
}
|
|
307
312
|
|
|
308
|
-
// Hooks —
|
|
309
|
-
for (const f of QUALIA_HOOK_FILES) {
|
|
313
|
+
// Hooks — current set plus any legacy hook filenames from older versions.
|
|
314
|
+
for (const f of [...QUALIA_HOOK_FILES, ...QUALIA_LEGACY_HOOK_FILES]) {
|
|
310
315
|
safeUnlink(path.join(CLAUDE_DIR, "hooks", f), counters);
|
|
311
316
|
}
|
|
312
317
|
|
package/bin/install.js
CHANGED
|
@@ -599,16 +599,23 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
599
599
|
// bash/Git Bash requirement on Windows.
|
|
600
600
|
const hd = path.join(CLAUDE_DIR, "hooks");
|
|
601
601
|
const nodeCmd = (hookFile) => `node "${path.join(hd, hookFile)}"`;
|
|
602
|
-
|
|
602
|
+
const QUALIA_HOOK_SET = new Set([
|
|
603
|
+
"session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
|
|
604
|
+
"pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
|
|
605
|
+
]);
|
|
606
|
+
const isQualiaHookCmd = (cmd) => {
|
|
607
|
+
if (typeof cmd !== "string") return false;
|
|
608
|
+
for (const h of QUALIA_HOOK_SET) if (cmd.includes(h)) return true;
|
|
609
|
+
return false;
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// Our canonical hook definitions, grouped per event/matcher.
|
|
613
|
+
const qualiaHooks = {
|
|
603
614
|
SessionStart: [
|
|
604
615
|
{
|
|
605
616
|
matcher: ".*",
|
|
606
617
|
hooks: [
|
|
607
|
-
{
|
|
608
|
-
type: "command",
|
|
609
|
-
command: nodeCmd("session-start.js"),
|
|
610
|
-
timeout: 5,
|
|
611
|
-
},
|
|
618
|
+
{ type: "command", command: nodeCmd("session-start.js"), timeout: 5 },
|
|
612
619
|
],
|
|
613
620
|
},
|
|
614
621
|
],
|
|
@@ -616,44 +623,16 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
616
623
|
{
|
|
617
624
|
matcher: "Bash",
|
|
618
625
|
hooks: [
|
|
619
|
-
{
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
},
|
|
624
|
-
{
|
|
625
|
-
type: "command",
|
|
626
|
-
if: "Bash(git push*)",
|
|
627
|
-
command: nodeCmd("branch-guard.js"),
|
|
628
|
-
timeout: 5,
|
|
629
|
-
statusMessage: "⬢ Checking branch permissions...",
|
|
630
|
-
},
|
|
631
|
-
{
|
|
632
|
-
type: "command",
|
|
633
|
-
if: "Bash(git push*)",
|
|
634
|
-
command: nodeCmd("pre-push.js"),
|
|
635
|
-
timeout: 15,
|
|
636
|
-
statusMessage: "⬢ Syncing tracking...",
|
|
637
|
-
},
|
|
638
|
-
{
|
|
639
|
-
type: "command",
|
|
640
|
-
if: "Bash(vercel --prod*)",
|
|
641
|
-
command: nodeCmd("pre-deploy-gate.js"),
|
|
642
|
-
timeout: 180,
|
|
643
|
-
statusMessage: "⬢ Running quality gates...",
|
|
644
|
-
},
|
|
626
|
+
{ type: "command", command: nodeCmd("auto-update.js"), timeout: 5 },
|
|
627
|
+
{ type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
|
|
628
|
+
{ type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
|
|
629
|
+
{ type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
|
|
645
630
|
],
|
|
646
631
|
},
|
|
647
632
|
{
|
|
648
633
|
matcher: "Edit|Write",
|
|
649
634
|
hooks: [
|
|
650
|
-
{
|
|
651
|
-
type: "command",
|
|
652
|
-
if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)",
|
|
653
|
-
command: nodeCmd("migration-guard.js"),
|
|
654
|
-
timeout: 10,
|
|
655
|
-
statusMessage: "⬢ Checking migration safety...",
|
|
656
|
-
},
|
|
635
|
+
{ type: "command", if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)", command: nodeCmd("migration-guard.js"), timeout: 10, statusMessage: "⬢ Checking migration safety..." },
|
|
657
636
|
],
|
|
658
637
|
},
|
|
659
638
|
],
|
|
@@ -661,17 +640,27 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
661
640
|
{
|
|
662
641
|
matcher: "compact",
|
|
663
642
|
hooks: [
|
|
664
|
-
{
|
|
665
|
-
type: "command",
|
|
666
|
-
command: nodeCmd("pre-compact.js"),
|
|
667
|
-
timeout: 15,
|
|
668
|
-
statusMessage: "⬢ Saving state...",
|
|
669
|
-
},
|
|
643
|
+
{ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15, statusMessage: "⬢ Saving state..." },
|
|
670
644
|
],
|
|
671
645
|
},
|
|
672
646
|
],
|
|
673
647
|
};
|
|
674
648
|
|
|
649
|
+
// Merge user hooks: strip Qualia-owned commands, preserve everything else.
|
|
650
|
+
if (!settings.hooks || typeof settings.hooks !== "object") settings.hooks = {};
|
|
651
|
+
for (const event of Object.keys(qualiaHooks)) {
|
|
652
|
+
const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
|
|
653
|
+
// Remove Qualia-owned command entries from each matcher block, drop empty blocks.
|
|
654
|
+
const cleaned = [];
|
|
655
|
+
for (const block of existing) {
|
|
656
|
+
if (!block || !Array.isArray(block.hooks)) continue;
|
|
657
|
+
const kept = block.hooks.filter((h) => !isQualiaHookCmd(h && h.command));
|
|
658
|
+
if (kept.length > 0) cleaned.push({ ...block, hooks: kept });
|
|
659
|
+
}
|
|
660
|
+
// Append our canonical blocks after the preserved user ones.
|
|
661
|
+
settings.hooks[event] = [...cleaned, ...qualiaHooks[event]];
|
|
662
|
+
}
|
|
663
|
+
|
|
675
664
|
// Permissions — no restrictions on env files or branches.
|
|
676
665
|
// Everyone can read/write .env, push to main.
|
|
677
666
|
if (!settings.permissions) settings.permissions = {};
|
package/bin/qualia-ui.js
CHANGED
|
@@ -320,10 +320,10 @@ function cmdJourneyTree(journeyPath) {
|
|
|
320
320
|
|
|
321
321
|
// Project name from frontmatter if present
|
|
322
322
|
const projMatch = content.match(/^project:\s*"?(.+?)"?\s*$/m);
|
|
323
|
-
const
|
|
323
|
+
const projName = projMatch ? projMatch[1] : projectName();
|
|
324
324
|
|
|
325
325
|
console.log("");
|
|
326
|
-
console.log(` ${TEAL}${BOLD}◯${RESET} ${WHITE}${BOLD}JOURNEY${RESET} ${DIM}▸${RESET} ${WHITE}${
|
|
326
|
+
console.log(` ${TEAL}${BOLD}◯${RESET} ${WHITE}${BOLD}JOURNEY${RESET} ${DIM}▸${RESET} ${WHITE}${projName}${RESET}`);
|
|
327
327
|
console.log(` ${RULE_DIM}`);
|
|
328
328
|
console.log(` ${DIM}${milestones.length} milestones · currently at M${currentMilestone}${RESET}`);
|
|
329
329
|
console.log("");
|
package/bin/state.js
CHANGED
|
@@ -9,6 +9,7 @@ const PLANNING = ".planning";
|
|
|
9
9
|
const STATE_FILE = path.join(PLANNING, "STATE.md");
|
|
10
10
|
const TRACKING_FILE = path.join(PLANNING, "tracking.json");
|
|
11
11
|
const LOCK_FILE = path.join(PLANNING, ".state.lock");
|
|
12
|
+
const JOURNAL_FILE = path.join(PLANNING, ".state.journal");
|
|
12
13
|
|
|
13
14
|
// ─── Atomic write (tmp + rename) ─────────────────────────
|
|
14
15
|
// Prevents half-written files when SIGINT, OOM, or AV scanners
|
|
@@ -26,10 +27,60 @@ function atomicWrite(file, content) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// ─── Write-ahead journal (two-file crash recovery) ──────
|
|
31
|
+
// STATE.md + tracking.json are written back-to-back. A SIGKILL or power
|
|
32
|
+
// loss between the two renames can leave the pair inconsistent. The
|
|
33
|
+
// journal captures the pre-write snapshot; if a journal is still present
|
|
34
|
+
// on next startup, we know the previous mutator crashed mid-write and
|
|
35
|
+
// restore both files to the pre-transaction state.
|
|
36
|
+
function writeJournal(preState, preTracking) {
|
|
37
|
+
if (!fs.existsSync(PLANNING)) return;
|
|
38
|
+
const payload = JSON.stringify({
|
|
39
|
+
ts: new Date().toISOString(),
|
|
40
|
+
pid: process.pid,
|
|
41
|
+
state: preState != null ? preState : null,
|
|
42
|
+
tracking: preTracking != null ? preTracking : null,
|
|
43
|
+
});
|
|
44
|
+
atomicWrite(JOURNAL_FILE, payload);
|
|
45
|
+
}
|
|
46
|
+
function clearJournal() {
|
|
47
|
+
try { fs.unlinkSync(JOURNAL_FILE); } catch {}
|
|
48
|
+
}
|
|
49
|
+
function recoverFromJournal() {
|
|
50
|
+
if (!fs.existsSync(JOURNAL_FILE)) return false;
|
|
51
|
+
try {
|
|
52
|
+
const raw = fs.readFileSync(JOURNAL_FILE, "utf8");
|
|
53
|
+
const j = JSON.parse(raw);
|
|
54
|
+
if (j.state != null) atomicWrite(STATE_FILE, j.state);
|
|
55
|
+
if (j.tracking != null) atomicWrite(TRACKING_FILE, j.tracking);
|
|
56
|
+
clearJournal();
|
|
57
|
+
try { _trace("state-journal", "recover", { from_pid: j.pid, journal_ts: j.ts }); } catch {}
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
// Corrupt journal — best effort: remove so we don't loop on recovery.
|
|
61
|
+
clearJournal();
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
29
66
|
// ─── Exclusive lock ──────────────────────────────────────
|
|
30
67
|
// Prevents two concurrent state.js mutations from racing on the dual
|
|
31
68
|
// STATE.md + tracking.json write. Read commands (check, validate-plan)
|
|
32
69
|
// don't take the lock — only mutators do.
|
|
70
|
+
|
|
71
|
+
// Synchronous sleep without CPU spin. Atomics.wait on a zero-initialized
|
|
72
|
+
// SharedArrayBuffer blocks until the timeout elapses and yields the CPU.
|
|
73
|
+
function sleepSync(ms) {
|
|
74
|
+
try {
|
|
75
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
76
|
+
} catch {
|
|
77
|
+
// SharedArrayBuffer unavailable (extremely old runtimes) — last-resort
|
|
78
|
+
// tight loop, bounded to the requested duration.
|
|
79
|
+
const t = Date.now() + ms;
|
|
80
|
+
while (Date.now() < t) {}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
33
84
|
function acquireLock(timeoutMs = 5000) {
|
|
34
85
|
if (!fs.existsSync(PLANNING)) return null; // nothing to lock yet
|
|
35
86
|
const start = Date.now();
|
|
@@ -50,12 +101,13 @@ function acquireLock(timeoutMs = 5000) {
|
|
|
50
101
|
continue;
|
|
51
102
|
}
|
|
52
103
|
} catch {}
|
|
53
|
-
|
|
54
|
-
const t = Date.now() + 50;
|
|
55
|
-
while (Date.now() < t) {}
|
|
104
|
+
sleepSync(50);
|
|
56
105
|
}
|
|
57
106
|
}
|
|
58
|
-
// Couldn't acquire — proceed unlocked rather than
|
|
107
|
+
// Couldn't acquire inside the budget — proceed unlocked rather than
|
|
108
|
+
// hard-block the user. Surface this in analytics so repeated contention
|
|
109
|
+
// is visible instead of silent.
|
|
110
|
+
try { _trace("state-lock", "fallthrough", { waited_ms: Date.now() - start }); } catch {}
|
|
59
111
|
return null;
|
|
60
112
|
}
|
|
61
113
|
|
|
@@ -613,14 +665,25 @@ function cmdTransition(opts) {
|
|
|
613
665
|
t.deploy_count = (parseInt(t.deploy_count) || 0) + 1;
|
|
614
666
|
}
|
|
615
667
|
|
|
616
|
-
// Write both files
|
|
668
|
+
// Write both files. We write a journal snapshot of the pre-transition
|
|
669
|
+
// STATE.md + tracking.json first; if the process dies between the two
|
|
670
|
+
// real writes, the next invocation will see the journal and restore both
|
|
671
|
+
// files to the pre-transition state. Each individual write is torn-write
|
|
672
|
+
// safe (tmp + rename); the journal closes the gap between the two.
|
|
617
673
|
const backupState = readState();
|
|
674
|
+
const backupTracking = (() => {
|
|
675
|
+
try { return fs.readFileSync(TRACKING_FILE, "utf8"); } catch { return null; }
|
|
676
|
+
})();
|
|
618
677
|
try {
|
|
678
|
+
writeJournal(backupState, backupTracking);
|
|
619
679
|
writeStateMd(s);
|
|
620
680
|
writeTracking(t);
|
|
681
|
+
clearJournal();
|
|
621
682
|
} catch (e) {
|
|
622
|
-
// Revert
|
|
623
|
-
if (backupState) atomicWrite(STATE_FILE, backupState);
|
|
683
|
+
// Revert whichever file is out of sync with pre-transition state.
|
|
684
|
+
try { if (backupState) atomicWrite(STATE_FILE, backupState); } catch {}
|
|
685
|
+
try { if (backupTracking) atomicWrite(TRACKING_FILE, backupTracking); } catch {}
|
|
686
|
+
clearJournal();
|
|
624
687
|
return output(fail("WRITE_ERROR", e.message));
|
|
625
688
|
}
|
|
626
689
|
|
|
@@ -1193,6 +1256,10 @@ const opts = parseArgs(rest);
|
|
|
1193
1256
|
const READ_ONLY = new Set(["check", "validate-plan"]);
|
|
1194
1257
|
let __lock = null;
|
|
1195
1258
|
if (!READ_ONLY.has(cmd)) {
|
|
1259
|
+
// Before acquiring the lock, recover from any journal left by a crashed
|
|
1260
|
+
// previous mutator. Runs for mutators only; read commands should still
|
|
1261
|
+
// return the actual on-disk state even if it's mid-recovery.
|
|
1262
|
+
try { recoverFromJournal(); } catch {}
|
|
1196
1263
|
__lock = acquireLock();
|
|
1197
1264
|
process.on("exit", () => releaseLock(__lock));
|
|
1198
1265
|
process.on("SIGINT", () => { releaseLock(__lock); process.exit(130); });
|
package/bin/statusline.js
CHANGED
|
@@ -175,7 +175,10 @@ try {
|
|
|
175
175
|
// ─── Memory count ────────────────────────────────────────
|
|
176
176
|
let MEMORY_COUNT = 0;
|
|
177
177
|
try {
|
|
178
|
-
|
|
178
|
+
// Claude Code uses a hyphenated encoding of the project directory. Replace
|
|
179
|
+
// BOTH forward and backward slashes so Windows installs (where DIR contains
|
|
180
|
+
// `\`) get a correct key and the memory count renders.
|
|
181
|
+
const dirKey = DIR.replace(/[\/\\]/g, "-");
|
|
179
182
|
const memDir = path.join(HOME, ".claude", "projects", dirKey, "memory");
|
|
180
183
|
if (fs.existsSync(memDir)) {
|
|
181
184
|
const files = fs.readdirSync(memDir).filter(f => f.endsWith(".md") && f !== "MEMORY.md");
|
package/docs/erp-contract.md
CHANGED
|
@@ -39,6 +39,16 @@ Content-Type: application/json
|
|
|
39
39
|
"git_remote": "github.com/QualiasolutionsCY/acme-portal",
|
|
40
40
|
"client": "Client Name",
|
|
41
41
|
"milestone": 2,
|
|
42
|
+
"milestone_name": "Core Product",
|
|
43
|
+
"milestones": [
|
|
44
|
+
{
|
|
45
|
+
"num": 1,
|
|
46
|
+
"name": "Foundation",
|
|
47
|
+
"closed_at": "2026-04-10T18:00:00Z",
|
|
48
|
+
"phases_completed": 3,
|
|
49
|
+
"tasks_completed": 12
|
|
50
|
+
}
|
|
51
|
+
],
|
|
42
52
|
"phase": 2,
|
|
43
53
|
"phase_name": "Authentication & Dashboard",
|
|
44
54
|
"total_phases": 4,
|
|
@@ -175,6 +185,8 @@ Authorization: Bearer <api-key>
|
|
|
175
185
|
| submitted_by | string | yes | Team member name |
|
|
176
186
|
| submitted_at | string | yes | ISO 8601 timestamp |
|
|
177
187
|
| milestone | number | recommended | Current milestone number (1-indexed) |
|
|
188
|
+
| milestone_name | string | recommended (v4+) | Human name of the current milestone — from JOURNEY.md / tracking.json |
|
|
189
|
+
| milestones | array | recommended (v4+) | Array of closed milestone summaries: `{num, name, closed_at, phases_completed, tasks_completed}`. Renders the journey tree on the ERP. |
|
|
178
190
|
| lifetime | object | recommended | Cumulative counters — tasks_completed, phases_completed, milestones_completed, total_phases, last_closed_milestone |
|
|
179
191
|
| project_id | string | recommended (v3.6+) | Stable per-project identifier — preferred dedupe key over `project` slug. Survives directory renames. |
|
|
180
192
|
| team_id | string | recommended (v3.6+) | Installation's team identifier. Composite `(team_id, project_id)` is the canonical project key. |
|
package/guide.md
CHANGED
|
@@ -43,7 +43,7 @@ Append `--auto` to `/qualia-new` and the framework chains every step:
|
|
|
43
43
|
|
|
44
44
|
**Plus one halt case:** if a phase fails verification beyond the gap-cycle limit (default 2), the chain stops and asks for human intervention.
|
|
45
45
|
|
|
46
|
-
## The
|
|
46
|
+
## The Road Commands
|
|
47
47
|
|
|
48
48
|
| When | Command | What it does |
|
|
49
49
|
|------|---------|-------------|
|
package/hooks/migration-guard.js
CHANGED
|
@@ -11,6 +11,10 @@ const _traceStart = Date.now();
|
|
|
11
11
|
// Read JSON tool input from stdin with a safety timeout.
|
|
12
12
|
// On Windows, fs.readFileSync(0) can hang if stdin isn't closed by the host.
|
|
13
13
|
// We loop fs.readSync with a 1s deadline; if no data arrives, treat as empty.
|
|
14
|
+
// Between EAGAIN retries we sleep via Atomics.wait to avoid CPU-burning spin.
|
|
15
|
+
function sleepSync(ms) {
|
|
16
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch {}
|
|
17
|
+
}
|
|
14
18
|
function readInput() {
|
|
15
19
|
const deadline = Date.now() + 1000;
|
|
16
20
|
const buf = Buffer.alloc(65536);
|
|
@@ -21,8 +25,8 @@ function readInput() {
|
|
|
21
25
|
try {
|
|
22
26
|
n = fs.readSync(0, buf, 0, buf.length, null);
|
|
23
27
|
} catch (e) {
|
|
24
|
-
// EAGAIN/EWOULDBLOCK: no data yet
|
|
25
|
-
if (e && (e.code === "EAGAIN" || e.code === "EWOULDBLOCK")) continue;
|
|
28
|
+
// EAGAIN/EWOULDBLOCK: no data yet. Sleep 1ms and retry until deadline.
|
|
29
|
+
if (e && (e.code === "EAGAIN" || e.code === "EWOULDBLOCK")) { sleepSync(1); continue; }
|
|
26
30
|
// Any other read error: bail
|
|
27
31
|
break;
|
|
28
32
|
}
|
|
@@ -108,14 +112,24 @@ if (/ALTER\s+TABLE\s+[^;]*\bDROP\s+COLUMN\b/i.test(scan)) {
|
|
|
108
112
|
errors.push("ALTER TABLE ... DROP COLUMN is destructive");
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
// DELETE without WHERE
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
// DELETE / UPDATE without WHERE — check per-statement, not file-global.
|
|
116
|
+
// Previously a file containing "DELETE FROM foo;" followed by any later
|
|
117
|
+
// "... WHERE ..." (in a SELECT, JOIN, etc.) would pass the check.
|
|
118
|
+
function splitStatements(src) {
|
|
119
|
+
return src.split(/;/g).map((s) => s.trim()).filter(Boolean);
|
|
114
120
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
121
|
+
const statements = splitStatements(scan);
|
|
122
|
+
for (const stmt of statements) {
|
|
123
|
+
if (/^\s*DELETE\s+FROM\b/i.test(stmt) && !/\bWHERE\b/i.test(stmt)) {
|
|
124
|
+
errors.push("DELETE FROM without WHERE clause");
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const stmt of statements) {
|
|
129
|
+
if (/^\s*UPDATE\s+\w+(?:\.\w+)?\s+SET\b/i.test(stmt) && !/\bWHERE\b/i.test(stmt)) {
|
|
130
|
+
errors.push("UPDATE without WHERE clause — affects every row");
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
// TRUNCATE (almost always wrong in migrations)
|
package/hooks/pre-compact.js
CHANGED
|
@@ -2,17 +2,44 @@
|
|
|
2
2
|
// ~/.claude/hooks/pre-compact.js — commit STATE.md before context compaction.
|
|
3
3
|
// PreCompact hook. Silent on failure — context compaction must never be blocked.
|
|
4
4
|
// Cross-platform (Windows/macOS/Linux).
|
|
5
|
+
//
|
|
6
|
+
// BY DEFAULT this commit uses --no-verify + --no-gpg-sign. The auto-save is a
|
|
7
|
+
// framework bot commit, and pre-commit hooks that run full test suites would
|
|
8
|
+
// routinely fail (context compaction happens at any moment) and lose the
|
|
9
|
+
// STATE.md snapshot. But compliance-sensitive projects can opt into strict
|
|
10
|
+
// mode via ~/.claude/.qualia-config.json:
|
|
11
|
+
//
|
|
12
|
+
// {
|
|
13
|
+
// "pre_compact": {
|
|
14
|
+
// "respect_user_hooks": true,
|
|
15
|
+
// "respect_gpg_signing": true
|
|
16
|
+
// }
|
|
17
|
+
// }
|
|
18
|
+
//
|
|
19
|
+
// When either is true, the corresponding --no-* flag is dropped.
|
|
5
20
|
|
|
6
21
|
const fs = require("fs");
|
|
7
22
|
const path = require("path");
|
|
23
|
+
const os = require("os");
|
|
8
24
|
const { spawnSync } = require("child_process");
|
|
9
25
|
|
|
10
26
|
const _traceStart = Date.now();
|
|
11
27
|
|
|
12
28
|
const STATE_FILE = path.join(".planning", "STATE.md");
|
|
29
|
+
const CONFIG_FILE = path.join(os.homedir(), ".claude", ".qualia-config.json");
|
|
30
|
+
|
|
31
|
+
function readCompactConfig() {
|
|
32
|
+
try {
|
|
33
|
+
const cfg = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
34
|
+
return cfg.pre_compact || {};
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
13
39
|
|
|
14
40
|
let _commitStatus = null;
|
|
15
41
|
let _commitReason = "no-state-file";
|
|
42
|
+
let _commitFlags = null;
|
|
16
43
|
|
|
17
44
|
try {
|
|
18
45
|
if (fs.existsSync(STATE_FILE)) {
|
|
@@ -29,16 +56,17 @@ try {
|
|
|
29
56
|
timeout: 3000,
|
|
30
57
|
shell: process.platform === "win32",
|
|
31
58
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
const cfg = readCompactConfig();
|
|
60
|
+
const commitArgs = ["commit"];
|
|
61
|
+
if (!cfg.respect_user_hooks) commitArgs.push("--no-verify");
|
|
62
|
+
if (!cfg.respect_gpg_signing) commitArgs.push("--no-gpg-sign");
|
|
63
|
+
commitArgs.push("--author=Qualia Framework <bot@qualia.solutions>");
|
|
64
|
+
commitArgs.push("-m", "state: pre-compaction save");
|
|
65
|
+
_commitFlags = {
|
|
66
|
+
no_verify: !cfg.respect_user_hooks,
|
|
67
|
+
no_gpg_sign: !cfg.respect_gpg_signing,
|
|
68
|
+
};
|
|
69
|
+
const commitRes = spawnSync("git", commitArgs, {
|
|
42
70
|
timeout: 5000,
|
|
43
71
|
stdio: ["ignore", "ignore", "pipe"],
|
|
44
72
|
encoding: "utf8",
|
|
@@ -72,5 +100,5 @@ function _trace(hookName, result, extra) {
|
|
|
72
100
|
} catch {}
|
|
73
101
|
}
|
|
74
102
|
|
|
75
|
-
_trace("pre-compact", "allow", { commit_status: _commitStatus, commit_reason: _commitReason });
|
|
103
|
+
_trace("pre-compact", "allow", { commit_status: _commitStatus, commit_reason: _commitReason, commit_flags: _commitFlags });
|
|
76
104
|
process.exit(0);
|
package/hooks/pre-deploy-gate.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// ~/.claude/hooks/pre-deploy-gate.js — quality gates before production deploy.
|
|
3
3
|
// PreToolUse hook on `vercel --prod*` commands. Runs tsc, lint, tests, build,
|
|
4
4
|
// then scans for service_role leaks in client code.
|
|
5
|
-
// Exits
|
|
6
|
-
// protocol formally uses exit 2, but the framework's existing tests assert on 1).
|
|
5
|
+
// Exits 2 to BLOCK deploy (Claude Code PreToolUse hook contract).
|
|
7
6
|
// Exits 0 to allow.
|
|
8
7
|
// Cross-platform (Windows/macOS/Linux). No `grep` or `find` — pure Node.
|
|
9
8
|
|
|
@@ -43,7 +42,7 @@ function runGate(label, cmd, args, { required = true } = {}) {
|
|
|
43
42
|
if (required) {
|
|
44
43
|
console.error(`BLOCKED: ${label} errors. Fix before deploying.`);
|
|
45
44
|
_trace("pre-deploy-gate", "block", { gate: label });
|
|
46
|
-
process.exit(
|
|
45
|
+
process.exit(2);
|
|
47
46
|
}
|
|
48
47
|
return false;
|
|
49
48
|
}
|
|
@@ -180,7 +179,7 @@ if (leaks.length > 0) {
|
|
|
180
179
|
console.error(` ✗ ${f}`);
|
|
181
180
|
}
|
|
182
181
|
_trace("pre-deploy-gate", "block", { gate: "security", leaks: leaks.slice(0, 10) });
|
|
183
|
-
process.exit(
|
|
182
|
+
process.exit(2);
|
|
184
183
|
}
|
|
185
184
|
console.log(" ✓ Security");
|
|
186
185
|
console.log("⬢ All gates passed.");
|
package/hooks/pre-push.js
CHANGED
|
@@ -85,9 +85,12 @@ function commitStamp() {
|
|
|
85
85
|
"-m", `chore(track): ERP sync ${now}`,
|
|
86
86
|
]);
|
|
87
87
|
if (commit.status !== 0) {
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
88
|
+
// Commit failed (e.g., empty diff because git's auto-CRLF normalized the
|
|
89
|
+
// only change to nothing, or branch is in a detached/conflicted state).
|
|
90
|
+
// Unstage tracking.json and restore the working tree copy so the user's
|
|
91
|
+
// next manual commit isn't polluted by our aborted stamp.
|
|
92
|
+
try { git(["reset", "HEAD", "--", TRACKING]); } catch {}
|
|
93
|
+
try { if (raw != null) atomicWrite(TRACKING, raw); } catch {}
|
|
91
94
|
return { skipped: "git-commit-failed", error: (commit.stderr || commit.stdout || "").trim() };
|
|
92
95
|
}
|
|
93
96
|
return { committed: true, sha: lastCommit, ts: now };
|
package/hooks/session-start.js
CHANGED
|
@@ -52,6 +52,14 @@ function getNextCommand() {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function readConfig() {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(fs.readFileSync(path.join(HOME, ".claude", ".qualia-config.json"), "utf8"));
|
|
58
|
+
} catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
function fallbackText() {
|
|
56
64
|
// If qualia-ui.js is missing, emit plain text. Keeps the session informative
|
|
57
65
|
// even on a broken install.
|
|
@@ -121,14 +129,6 @@ try {
|
|
|
121
129
|
// Deliberately silent — hook must never fail
|
|
122
130
|
}
|
|
123
131
|
|
|
124
|
-
function readConfig() {
|
|
125
|
-
try {
|
|
126
|
-
return JSON.parse(fs.readFileSync(path.join(HOME, ".claude", ".qualia-config.json"), "utf8"));
|
|
127
|
-
} catch {
|
|
128
|
-
return {};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
132
|
function _trace(hookName, result, extra) {
|
|
133
133
|
try {
|
|
134
134
|
const traceDir = path.join(os.homedir(), ".claude", ".qualia-traces");
|