qualia-framework 6.3.0 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -8
- package/CLAUDE.md +5 -5
- package/README.md +17 -39
- package/bin/cli.js +64 -16
- package/bin/command-surface.js +5 -1
- package/bin/install.js +26 -11
- package/bin/learning-candidates.js +217 -0
- package/bin/prune-deprecated.js +64 -0
- package/bin/runtime-manifest.js +4 -0
- package/bin/security-scan.js +409 -0
- package/bin/status-snapshot.js +363 -0
- package/guide.md +11 -33
- package/hooks/pre-compact.js +232 -0
- package/package.json +1 -1
- package/skills/qualia/SKILL.md +1 -1
- package/skills/qualia-build/SKILL.md +1 -1
- package/skills/qualia-discuss/SKILL.md +1 -1
- package/skills/qualia-doctor/SKILL.md +1 -1
- package/skills/qualia-feature/SKILL.md +1 -1
- package/skills/qualia-fix/SKILL.md +1 -1
- package/skills/qualia-idk/SKILL.md +245 -0
- package/skills/qualia-learn/SKILL.md +1 -1
- package/skills/qualia-map/SKILL.md +1 -1
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-new/SKILL.md +1 -1
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +1 -1
- package/skills/qualia-polish/SKILL.md +1 -1
- package/skills/qualia-postmortem/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +1 -1
- package/skills/qualia-research/SKILL.md +1 -1
- package/skills/qualia-review/SKILL.md +1 -1
- package/skills/qualia-road/SKILL.md +1 -1
- package/skills/qualia-secure/SKILL.md +105 -0
- package/skills/qualia-test/SKILL.md +1 -1
- package/skills/qualia-verify/SKILL.md +1 -1
- package/skills/zoho-workflow/SKILL.md +1 -1
- package/tests/bin.test.sh +9 -9
- package/tests/install-smoke.test.sh +3 -3
- package/tests/lib.test.sh +6 -6
- package/tests/published-install-smoke.test.sh +3 -3
- package/tests/refs.test.sh +29 -22
- package/tests/runner.js +3 -3
- package/tests/state.test.sh +38 -7
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/status-snapshot.js — operator status snapshot. Mirrors ECC's
|
|
3
|
+
// `ecc status --markdown --write status.md`, but stays vertical to Qualia:
|
|
4
|
+
// install health + active project + work in flight + ERP queue + memory state.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// qualia-framework status # print markdown to stdout
|
|
8
|
+
// qualia-framework status --write [path] # write to file (default: ./STATUS.md)
|
|
9
|
+
// qualia-framework status --json # machine-readable JSON
|
|
10
|
+
// qualia-framework status --exit-code # exit 1 if readiness warnings exist
|
|
11
|
+
//
|
|
12
|
+
// Designed for cross-session paste — every line is fully qualified, no
|
|
13
|
+
// runtime variables, no secrets. Safe to paste into a Slack / GitHub
|
|
14
|
+
// comment / handoff doc.
|
|
15
|
+
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const os = require("os");
|
|
19
|
+
const { spawnSync } = require("child_process");
|
|
20
|
+
|
|
21
|
+
const _start = Date.now();
|
|
22
|
+
|
|
23
|
+
function qualiaHome() {
|
|
24
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
25
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
26
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
27
|
+
return path.join(os.homedir(), ".claude");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function git(args, opts = {}) {
|
|
31
|
+
try {
|
|
32
|
+
const r = spawnSync("git", args, { encoding: "utf8", timeout: 2000, shell: process.platform === "win32", ...opts });
|
|
33
|
+
if (r.status !== 0) return "";
|
|
34
|
+
return (r.stdout || "").trim();
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readJson(p) {
|
|
41
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safeStatM(p) {
|
|
45
|
+
try { return fs.statSync(p).mtimeMs; } catch { return 0; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function humanAge(ms) {
|
|
49
|
+
if (!ms) return "never";
|
|
50
|
+
const diff = Date.now() - ms;
|
|
51
|
+
if (diff < 0) return "future";
|
|
52
|
+
if (diff < 60_000) return `${Math.round(diff / 1000)}s ago`;
|
|
53
|
+
if (diff < 3600_000) return `${Math.round(diff / 60_000)}m ago`;
|
|
54
|
+
if (diff < 86_400_000) return `${Math.round(diff / 3600_000)}h ago`;
|
|
55
|
+
return `${Math.round(diff / 86_400_000)}d ago`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArgs(argv) {
|
|
59
|
+
const args = { write: false, writePath: "", json: false, exitCode: false };
|
|
60
|
+
for (let i = 0; i < argv.length; i++) {
|
|
61
|
+
const a = argv[i];
|
|
62
|
+
if (a === "--write") {
|
|
63
|
+
args.write = true;
|
|
64
|
+
if (argv[i + 1] && !argv[i + 1].startsWith("--")) args.writePath = argv[++i];
|
|
65
|
+
} else if (a.startsWith("--write=")) {
|
|
66
|
+
args.write = true;
|
|
67
|
+
args.writePath = a.slice("--write=".length);
|
|
68
|
+
} else if (a === "--json") {
|
|
69
|
+
args.json = true;
|
|
70
|
+
} else if (a === "--exit-code") {
|
|
71
|
+
args.exitCode = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return args;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function collectInstallHealth() {
|
|
78
|
+
const qhome = qualiaHome();
|
|
79
|
+
const cfgPath = path.join(qhome, ".qualia-config.json");
|
|
80
|
+
const cfg = readJson(cfgPath) || {};
|
|
81
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
82
|
+
const pkg = readJson(pkgPath) || {};
|
|
83
|
+
|
|
84
|
+
const homes = [path.join(os.homedir(), ".claude"), path.join(os.homedir(), ".codex")].filter((h) => fs.existsSync(h));
|
|
85
|
+
const installedTargets = homes.map((h) => path.basename(h) === ".codex" ? "Codex" : "Claude");
|
|
86
|
+
|
|
87
|
+
// Sanity: required core scripts exist?
|
|
88
|
+
const required = [
|
|
89
|
+
"bin/state.js",
|
|
90
|
+
"bin/command-surface.js",
|
|
91
|
+
"bin/prune-deprecated.js",
|
|
92
|
+
"hooks/pre-compact.js",
|
|
93
|
+
"hooks/stop-session-log.js",
|
|
94
|
+
];
|
|
95
|
+
const missing = [];
|
|
96
|
+
for (const home of homes) {
|
|
97
|
+
for (const rel of required) {
|
|
98
|
+
if (!fs.existsSync(path.join(home, rel))) missing.push(`${path.basename(home)}:${rel}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
package_version: pkg.version || "?",
|
|
104
|
+
installed_version: cfg.version || "?",
|
|
105
|
+
installed_by: cfg.installed_by || "?",
|
|
106
|
+
role: cfg.role || "?",
|
|
107
|
+
installed_at: cfg.installed_at || "?",
|
|
108
|
+
targets: installedTargets,
|
|
109
|
+
missing_files: missing,
|
|
110
|
+
healthy: missing.length === 0 && !!cfg.version,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function collectActiveProject() {
|
|
115
|
+
const cwd = process.cwd();
|
|
116
|
+
const repoRoot = git(["rev-parse", "--show-toplevel"], { cwd }) || cwd;
|
|
117
|
+
const planningDir = path.join(repoRoot, ".planning");
|
|
118
|
+
if (!fs.existsSync(planningDir)) {
|
|
119
|
+
return { is_project: false, repo: path.basename(repoRoot) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const tracking = readJson(path.join(planningDir, "tracking.json")) || {};
|
|
123
|
+
const stateText = (() => {
|
|
124
|
+
try { return fs.readFileSync(path.join(planningDir, "STATE.md"), "utf8"); } catch { return ""; }
|
|
125
|
+
})();
|
|
126
|
+
const phaseLine = stateText.split("\n").find((l) => /^Phase:/i.test(l)) || "";
|
|
127
|
+
const statusLine = stateText.split("\n").find((l) => /^Status:/i.test(l)) || "";
|
|
128
|
+
|
|
129
|
+
// Verification status of current phase.
|
|
130
|
+
const phase = tracking.phase || 0;
|
|
131
|
+
const vPath = phase ? path.join(planningDir, `phase-${phase}-verification.md`) : "";
|
|
132
|
+
let verifyState = "unverified";
|
|
133
|
+
let failCount = 0;
|
|
134
|
+
if (vPath && fs.existsSync(vPath)) {
|
|
135
|
+
try {
|
|
136
|
+
const v = fs.readFileSync(vPath, "utf8");
|
|
137
|
+
failCount = (v.match(/\bFAIL\b/g) || []).length + (v.match(/INSUFFICIENT EVIDENCE/g) || []).length;
|
|
138
|
+
verifyState = failCount > 0 ? "FAILED" : "passed";
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
is_project: true,
|
|
144
|
+
repo: path.basename(repoRoot),
|
|
145
|
+
branch: git(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: repoRoot }) || "?",
|
|
146
|
+
phase_line: phaseLine.trim() || `Phase: ${tracking.phase || "?"} of ${tracking.total_phases || tracking.phase_total || "?"}`,
|
|
147
|
+
status_line: statusLine.trim() || `Status: ${tracking.status || "?"}`,
|
|
148
|
+
verify_state: verifyState,
|
|
149
|
+
verify_fail_count: failCount,
|
|
150
|
+
gap_cycles_used: (tracking.gap_cycles || {})[String(phase)] || 0,
|
|
151
|
+
gap_cycle_limit: tracking.gap_cycle_limit || 2,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function collectWorkInFlight() {
|
|
156
|
+
const cwd = process.cwd();
|
|
157
|
+
const modified = git(["diff", "--name-only", "HEAD"], { cwd }).split("\n").filter(Boolean);
|
|
158
|
+
const staged = git(["diff", "--name-only", "--cached"], { cwd }).split("\n").filter(Boolean);
|
|
159
|
+
const untracked = git(["ls-files", "--others", "--exclude-standard"], { cwd }).split("\n").filter(Boolean);
|
|
160
|
+
const commitsAhead = (() => {
|
|
161
|
+
const head = git(["rev-parse", "HEAD"], { cwd });
|
|
162
|
+
const upstream = git(["rev-parse", "@{u}"], { cwd });
|
|
163
|
+
if (!head || !upstream) return 0;
|
|
164
|
+
const out = git(["rev-list", "--count", `${upstream}..HEAD`], { cwd });
|
|
165
|
+
return parseInt(out, 10) || 0;
|
|
166
|
+
})();
|
|
167
|
+
return {
|
|
168
|
+
modified: modified.slice(0, 20),
|
|
169
|
+
staged: staged.slice(0, 20),
|
|
170
|
+
untracked: untracked.slice(0, 20),
|
|
171
|
+
commits_ahead: commitsAhead,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function collectErpQueue() {
|
|
176
|
+
const qhome = qualiaHome();
|
|
177
|
+
const queueDir = path.join(qhome, ".erp-retry-queue");
|
|
178
|
+
if (!fs.existsSync(queueDir)) return { pending: 0, last_attempt: 0 };
|
|
179
|
+
try {
|
|
180
|
+
const entries = fs.readdirSync(queueDir).filter((f) => f.endsWith(".json"));
|
|
181
|
+
let lastMtime = 0;
|
|
182
|
+
for (const f of entries) lastMtime = Math.max(lastMtime, safeStatM(path.join(queueDir, f)));
|
|
183
|
+
return { pending: entries.length, last_attempt: lastMtime };
|
|
184
|
+
} catch {
|
|
185
|
+
return { pending: 0, last_attempt: 0 };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function collectMemoryState() {
|
|
190
|
+
const qhome = qualiaHome();
|
|
191
|
+
const dailyDir = path.join(qhome, "knowledge", "daily-log");
|
|
192
|
+
const lastFlushStamp = path.join(qhome, ".qualia-last-flush");
|
|
193
|
+
const lastLearnScanStamp = path.join(qhome, ".qualia-last-learning-scan");
|
|
194
|
+
|
|
195
|
+
let entriesThisWeek = 0;
|
|
196
|
+
if (fs.existsSync(dailyDir)) {
|
|
197
|
+
const sevenDaysAgo = Date.now() - 7 * 86_400_000;
|
|
198
|
+
for (const f of fs.readdirSync(dailyDir)) {
|
|
199
|
+
if (!f.endsWith(".md")) continue;
|
|
200
|
+
const m = f.match(/^(\d{4}-\d{2}-\d{2})\.md$/);
|
|
201
|
+
if (!m) continue;
|
|
202
|
+
const date = new Date(m[1] + "T00:00:00Z").getTime();
|
|
203
|
+
if (date >= sevenDaysAgo) entriesThisWeek++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
last_flush_mtime: safeStatM(lastFlushStamp),
|
|
209
|
+
last_learn_scan_mtime: safeStatM(lastLearnScanStamp),
|
|
210
|
+
daily_log_days_this_week: entriesThisWeek,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function readinessWarnings(snap) {
|
|
215
|
+
const warnings = [];
|
|
216
|
+
if (!snap.install.healthy) warnings.push(`install: ${snap.install.missing_files.length} missing file(s) — run \`qualia-framework doctor\``);
|
|
217
|
+
if (snap.project.is_project) {
|
|
218
|
+
if (snap.project.verify_state === "FAILED") warnings.push(`phase ${snap.project.phase_line} has ${snap.project.verify_fail_count} FAIL marker(s)`);
|
|
219
|
+
if (snap.project.gap_cycles_used >= snap.project.gap_cycle_limit) warnings.push(`gap-cycle limit reached (${snap.project.gap_cycles_used}/${snap.project.gap_cycle_limit})`);
|
|
220
|
+
}
|
|
221
|
+
if (snap.erp.pending > 0) warnings.push(`ERP retry queue: ${snap.erp.pending} pending`);
|
|
222
|
+
if (snap.memory.last_flush_mtime === 0) warnings.push("knowledge has never been flushed — run `qualia-framework flush`");
|
|
223
|
+
return warnings;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderMarkdown(snap, warnings) {
|
|
227
|
+
const lines = [];
|
|
228
|
+
lines.push(`# Qualia status — ${new Date().toISOString()}`);
|
|
229
|
+
lines.push("");
|
|
230
|
+
lines.push(`Snapshot of installed framework + active project. Generated by \`qualia-framework status\`. Safe to paste into a handoff.`);
|
|
231
|
+
lines.push("");
|
|
232
|
+
|
|
233
|
+
// ── Install ──────────────────────────────────────
|
|
234
|
+
lines.push("## Install");
|
|
235
|
+
lines.push("| Field | Value |");
|
|
236
|
+
lines.push("|---|---|");
|
|
237
|
+
lines.push(`| Package version | \`${snap.install.package_version}\` |`);
|
|
238
|
+
lines.push(`| Installed version | \`${snap.install.installed_version}\` |`);
|
|
239
|
+
lines.push(`| Owner / role | ${snap.install.installed_by} / ${snap.install.role} |`);
|
|
240
|
+
lines.push(`| Targets | ${snap.install.targets.join(", ") || "(none)"} |`);
|
|
241
|
+
lines.push(`| Health | ${snap.install.healthy ? "✅ healthy" : `⚠ ${snap.install.missing_files.length} missing file(s)`} |`);
|
|
242
|
+
if (snap.install.missing_files.length > 0) {
|
|
243
|
+
lines.push("");
|
|
244
|
+
lines.push("Missing:");
|
|
245
|
+
for (const m of snap.install.missing_files.slice(0, 10)) lines.push(`- ${m}`);
|
|
246
|
+
}
|
|
247
|
+
lines.push("");
|
|
248
|
+
|
|
249
|
+
// ── Active project ───────────────────────────────
|
|
250
|
+
lines.push("## Active project");
|
|
251
|
+
if (!snap.project.is_project) {
|
|
252
|
+
lines.push(`Repo: \`${snap.project.repo}\` — not a Qualia project (no \`.planning/\` directory).`);
|
|
253
|
+
} else {
|
|
254
|
+
lines.push("| Field | Value |");
|
|
255
|
+
lines.push("|---|---|");
|
|
256
|
+
lines.push(`| Repo | \`${snap.project.repo}\` |`);
|
|
257
|
+
lines.push(`| Branch | \`${snap.project.branch}\` |`);
|
|
258
|
+
lines.push(`| ${snap.project.phase_line.replace(/\|/g, "\\|")} | |`);
|
|
259
|
+
lines.push(`| ${snap.project.status_line.replace(/\|/g, "\\|")} | |`);
|
|
260
|
+
lines.push(`| Verification | ${snap.project.verify_state === "FAILED" ? `⚠ FAILED (${snap.project.verify_fail_count} marker(s))` : snap.project.verify_state} |`);
|
|
261
|
+
lines.push(`| Gap cycles | ${snap.project.gap_cycles_used} / ${snap.project.gap_cycle_limit} |`);
|
|
262
|
+
}
|
|
263
|
+
lines.push("");
|
|
264
|
+
|
|
265
|
+
// ── Work in flight ───────────────────────────────
|
|
266
|
+
lines.push("## Work in flight");
|
|
267
|
+
const total = snap.work.modified.length + snap.work.staged.length + snap.work.untracked.length;
|
|
268
|
+
if (total === 0 && snap.work.commits_ahead === 0) {
|
|
269
|
+
lines.push("Clean.");
|
|
270
|
+
} else {
|
|
271
|
+
if (snap.work.commits_ahead > 0) lines.push(`- ${snap.work.commits_ahead} commit(s) ahead of upstream`);
|
|
272
|
+
if (snap.work.staged.length > 0) {
|
|
273
|
+
lines.push("");
|
|
274
|
+
lines.push(`### Staged (${snap.work.staged.length})`);
|
|
275
|
+
for (const f of snap.work.staged) lines.push(`- ${f}`);
|
|
276
|
+
}
|
|
277
|
+
if (snap.work.modified.length > 0) {
|
|
278
|
+
lines.push("");
|
|
279
|
+
lines.push(`### Modified (${snap.work.modified.length})`);
|
|
280
|
+
for (const f of snap.work.modified) lines.push(`- ${f}`);
|
|
281
|
+
}
|
|
282
|
+
if (snap.work.untracked.length > 0) {
|
|
283
|
+
lines.push("");
|
|
284
|
+
lines.push(`### Untracked (${snap.work.untracked.length})`);
|
|
285
|
+
for (const f of snap.work.untracked) lines.push(`- ${f}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
lines.push("");
|
|
289
|
+
|
|
290
|
+
// ── ERP queue ────────────────────────────────────
|
|
291
|
+
lines.push("## ERP queue");
|
|
292
|
+
if (snap.erp.pending === 0) {
|
|
293
|
+
lines.push("No pending reports.");
|
|
294
|
+
} else {
|
|
295
|
+
lines.push(`${snap.erp.pending} pending. Last attempt ${humanAge(snap.erp.last_attempt)}.`);
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push("Drain: `qualia-framework erp-flush`");
|
|
298
|
+
}
|
|
299
|
+
lines.push("");
|
|
300
|
+
|
|
301
|
+
// ── Memory ───────────────────────────────────────
|
|
302
|
+
lines.push("## Memory");
|
|
303
|
+
lines.push("| Field | Value |");
|
|
304
|
+
lines.push("|---|---|");
|
|
305
|
+
lines.push(`| Last flush | ${humanAge(snap.memory.last_flush_mtime)} |`);
|
|
306
|
+
lines.push(`| Last learning scan | ${humanAge(snap.memory.last_learn_scan_mtime)} |`);
|
|
307
|
+
lines.push(`| Daily-log days (last 7) | ${snap.memory.daily_log_days_this_week} |`);
|
|
308
|
+
lines.push("");
|
|
309
|
+
|
|
310
|
+
// ── Readiness warnings ───────────────────────────
|
|
311
|
+
if (warnings.length === 0) {
|
|
312
|
+
lines.push("## Readiness");
|
|
313
|
+
lines.push("✅ All green.");
|
|
314
|
+
} else {
|
|
315
|
+
lines.push("## Readiness warnings");
|
|
316
|
+
for (const w of warnings) lines.push(`- ⚠ ${w}`);
|
|
317
|
+
}
|
|
318
|
+
lines.push("");
|
|
319
|
+
|
|
320
|
+
lines.push("---");
|
|
321
|
+
lines.push(`_Generated in ${Date.now() - _start}ms by \`qualia-framework status\`._`);
|
|
322
|
+
return lines.join("\n") + "\n";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function main() {
|
|
326
|
+
const args = parseArgs(process.argv.slice(2));
|
|
327
|
+
const snap = {
|
|
328
|
+
install: collectInstallHealth(),
|
|
329
|
+
project: collectActiveProject(),
|
|
330
|
+
work: collectWorkInFlight(),
|
|
331
|
+
erp: collectErpQueue(),
|
|
332
|
+
memory: collectMemoryState(),
|
|
333
|
+
generated_at: new Date().toISOString(),
|
|
334
|
+
};
|
|
335
|
+
const warnings = readinessWarnings(snap);
|
|
336
|
+
|
|
337
|
+
if (args.json) {
|
|
338
|
+
process.stdout.write(JSON.stringify({ ...snap, warnings }, null, 2) + "\n");
|
|
339
|
+
} else {
|
|
340
|
+
const md = renderMarkdown(snap, warnings);
|
|
341
|
+
if (args.write) {
|
|
342
|
+
const out = args.writePath || path.join(process.cwd(), "STATUS.md");
|
|
343
|
+
fs.writeFileSync(out, md);
|
|
344
|
+
console.log(`Wrote ${out}`);
|
|
345
|
+
} else {
|
|
346
|
+
process.stdout.write(md);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (args.exitCode && warnings.length > 0) {
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
module.exports = { main, collectInstallHealth, collectActiveProject, collectWorkInFlight, collectErpQueue, collectMemoryState, readinessWarnings };
|
|
356
|
+
|
|
357
|
+
if (require.main === module) {
|
|
358
|
+
try { main(); }
|
|
359
|
+
catch (e) {
|
|
360
|
+
console.error(`status failed: ${e.message}`);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
}
|
package/guide.md
CHANGED
|
@@ -1,33 +1,11 @@
|
|
|
1
|
-
# Qualia Developer Guide
|
|
1
|
+
# Qualia Developer Guide
|
|
2
2
|
|
|
3
3
|
> Follow the road. Type the commands. The framework handles the rest.
|
|
4
4
|
> `--auto` chains the whole road end-to-end with only two human checkpoints per project.
|
|
5
5
|
|
|
6
|
-
**
|
|
6
|
+
Surface: **23 active skills**. Use `/qualia-fix` for broken behavior, `/qualia-feature` for new single-feature work, `/qualia-discuss` in PROJECT MODE for kickoff capture, `/qualia-polish --loop` for the autonomous visual loop, and `/qualia-polish --vibe` for fast layout-preserving aesthetic pivots.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
**v6.2.5 carries forward.** `qualia-framework project-snapshot --write` creates a single `.planning/snapshots/project-snapshot-*.json` artifact with project identifiers, current milestone/phase, closed milestones, lifetime counters, and a project progress percentage for explicit ERP/admin import.
|
|
11
|
-
|
|
12
|
-
**v6.2.4 carries forward.** The Framework -> ERP report payload now comes from the shipped and tested `report-payload.js` builder, so admin-visible report linking fields are covered by executable tests instead of only prompt prose.
|
|
13
|
-
|
|
14
|
-
**v6.2.3 carries forward.** Framework slugs stay in `project_id`/`team_id`; ERP foreign-key identifiers (`erp_project_id`, `client_id`, `workspace_id`) are sent only when UUID-shaped, so reports do not fail validation because a local slug leaked into an ERP UUID field.
|
|
15
|
-
|
|
16
|
-
**v6.2.2 carries forward.** Framework builds, Memory remembers, ERP operates. ERP work packets can seed Claude/Codex sessions, `/qualia-report` can carry ERP-native IDs, and release verification now has a public `@latest` install smoke.
|
|
17
|
-
|
|
18
|
-
**v6.2.1 carries forward.** Active docs match the v6.2 no-bot-commit model, the explicit `/qualia-report` ERP contract, the current skill surface, and fail-closed `INSUFFICIENT EVIDENCE` behavior. `tests/refs.test.sh` guards those claims.
|
|
19
|
-
|
|
20
|
-
**v6.1.0 ships the design-pivot path you were missing.** New `/qualia-polish --vibe` is fast aesthetic pivot (~3 min): swap design tokens, keep layout. Default proposes ONE direction per `rules/one-opinion.md` (the EventMaster discipline — never give the user a menu). Sub-modes: `--variants N` for the opt-in menu, `--extract URL` reverse-engineers DESIGN.md from a reference site, `--sync` shows code↔DESIGN.md drift and can patch DESIGN.md from code. Slop-detect grew banned fonts (Montserrat/Poppins/Lato/Open Sans) and a `--watch` flag for proactive single-file mode. Several design-surface bugs from v6.0 audit are fixed too (viewport mismatch, slop-detect path resolution in the polish loop, dead `/qualia-design` references, the bounce-easing token that contradicted design-laws).
|
|
21
|
-
|
|
22
|
-
**v6.2.0 removes hook-created bot commits.** `pre-push.js` stamps local `tracking.json` telemetry but no longer commits it. `pre-compact.js` is gone because `state.js` already gives stronger crash safety with atomic writes plus a journal. ERP sync remains explicit through `/qualia-report` POSTs, not passive git scraping.
|
|
23
|
-
|
|
24
|
-
**v5.9.2 carries forward.** `pre-push.js` self-gates against `branch-guard.js` so a blocked push no longer leaves an orphan bot commit. `qualia-report` ERP payload omits empty ISO datetime fields (avoids 422 from the ERP validator).
|
|
25
|
-
|
|
26
|
-
**v5.9.1 carries forward.** `/qualia-new` opens with the Demo/Full/Quick project-shape gate via `AskUserQuestion`, then exactly one free-text pitch question, then a hard hand-off to `/qualia-discuss` for the structured interview (8 questions for demos, 14 for full projects).
|
|
27
|
-
|
|
28
|
-
**v5.9.0 carries forward.** `tests/refs.test.sh` catches dead command references in user-facing surfaces on every release. `bin/erp-retry.js` is a real persistent retry queue for ERP report uploads. Four structured agents (verifier, plan-checker, roadmapper, qa-browser) run on Sonnet for ~40% per-phase cost cut, while builder/planner/researcher/visual-evaluator stay on Opus where the architectural and vision reasoning lives. The verifier downgrades to FAIL on any `INSUFFICIENT EVIDENCE` line.
|
|
29
|
-
|
|
30
|
-
**Surface is 23 installed skills.** Use `/qualia-fix` for broken existing behavior, `/qualia-feature` for new single-feature work, and `/qualia-discuss` in PROJECT MODE for kickoff capture; `/qualia-polish --loop` for the autonomous visual loop; `/qualia-polish --vibe` for fast layout-preserving aesthetic pivots.
|
|
8
|
+
For the identity statement see [`SOUL.md`](./SOUL.md). For every skill flag see [`FLAGS.md`](./FLAGS.md). When something breaks see [`TROUBLESHOOTING.md`](./TROUBLESHOOTING.md). For release history see [`CHANGELOG.md`](./CHANGELOG.md).
|
|
31
9
|
|
|
32
10
|
## The Road
|
|
33
11
|
|
|
@@ -93,8 +71,8 @@ Append `--auto` to `/qualia-new` and the framework chains every step:
|
|
|
93
71
|
| Harness eval | `qualia-framework eval --run --write` | Run machine contract checks and write scored eval artifacts for ERP/reporting |
|
|
94
72
|
| Planning hygiene | `qualia-framework planning-hygiene scan` | Detect loose `.planning/` reports/assets before they turn into folder bloat |
|
|
95
73
|
| Road view | `/qualia-road` | View and navigate journey/milestone/phase status |
|
|
96
|
-
| Lost
|
|
97
|
-
| Confused
|
|
74
|
+
| Lost — need next command | `/qualia` | Mechanical state-driven router |
|
|
75
|
+
| Confused — need to understand the situation | `/qualia-idk` | Three-scan diagnostic + paste-ready command sequence |
|
|
98
76
|
|
|
99
77
|
## Full Journey Hierarchy
|
|
100
78
|
|
|
@@ -118,14 +96,14 @@ Hard rules (enforced by `state.js` and the roadmapper):
|
|
|
118
96
|
2. **Read before write** — don't edit files you haven't read
|
|
119
97
|
3. **MVP first** — build what's asked, nothing extra
|
|
120
98
|
4. **Every task has a `Why`** (story-file format) — if you can't explain why a task matters in one sentence, it probably shouldn't exist
|
|
121
|
-
5. **`/qualia` is your friend** — lost?
|
|
122
|
-
6. **`/qualia` is your deeper friend** —
|
|
99
|
+
5. **`/qualia` is your friend** — lost on "what's my next command?" The router reads state and returns the next move.
|
|
100
|
+
6. **`/qualia-idk` is your deeper friend** — confused about *the situation itself*. Reads conversation + planning + code, then returns guidance plus a paste-ready Qualia command sequence.
|
|
123
101
|
|
|
124
102
|
## When You're Stuck
|
|
125
103
|
|
|
126
104
|
```
|
|
127
|
-
/qualia ← "what
|
|
128
|
-
/qualia ← "what
|
|
105
|
+
/qualia ← "what's my next command?" (state-driven, instant)
|
|
106
|
+
/qualia-idk ← "what is actually going on here?" (three-scan diagnostic, ~30-45s)
|
|
129
107
|
```
|
|
130
108
|
|
|
131
109
|
If neither helps, paste the error and ask Claude directly. If Claude can't fix it, tell Fawzi.
|
|
@@ -153,8 +131,8 @@ If neither helps, paste the error and ask Claude directly. If Claude can't fix i
|
|
|
153
131
|
| Starting a new client project | `/qualia-new` (or `/qualia-new --auto` to roll end-to-end) |
|
|
154
132
|
| Starting a quick throwaway | `/qualia-new --quick` |
|
|
155
133
|
| Brownfield project | `/qualia-map` first, then `/qualia-new` |
|
|
156
|
-
| Stuck
|
|
157
|
-
| Confused
|
|
134
|
+
| Stuck — "what command should I run?" | `/qualia` |
|
|
135
|
+
| Confused — "what's actually going on?" | `/qualia-idk` |
|
|
158
136
|
| Finished the last phase of a milestone | `/qualia-milestone` |
|
|
159
137
|
| About to ship | `/qualia-ship` |
|
|
160
138
|
| Client is ready to take over | `/qualia-handoff` |
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ~/.claude/hooks/pre-compact.js — snapshot in-flight session context to
|
|
3
|
+
// .planning/.compaction-snapshot.md so /qualia-resume (or the next session)
|
|
4
|
+
// can surface what was active when compaction wiped the conversation.
|
|
5
|
+
//
|
|
6
|
+
// PreCompact hook (fires before Claude Code compacts the context window).
|
|
7
|
+
//
|
|
8
|
+
// Why this hook returned in v6.3.2:
|
|
9
|
+
// v6.2.0 removed the original pre-compact.js because it created BOT COMMITS
|
|
10
|
+
// to stamp STATE.md / tracking.json. That was the wrong mechanism —
|
|
11
|
+
// state.js's atomic write + journal already gives crash safety. This v2
|
|
12
|
+
// hook does NOT touch git, does NOT touch tracking.json, does NOT write
|
|
13
|
+
// through state.js. It only writes to a sidecar file that survives
|
|
14
|
+
// compaction and tells the next session "here's what was in flight".
|
|
15
|
+
//
|
|
16
|
+
// Design constraints (same shape as stop-session-log.js):
|
|
17
|
+
// • NEVER block — exit 0 always, even on internal failure.
|
|
18
|
+
// • Fast — under 200ms, no network, no LLM call.
|
|
19
|
+
// • No PII / secrets — file paths, commit subjects, phase numbers only.
|
|
20
|
+
// • Idempotent — overwrites the sidecar on every fire; we only care
|
|
21
|
+
// about the LATEST pre-compaction state.
|
|
22
|
+
// • No git commits, no state.js mutations.
|
|
23
|
+
//
|
|
24
|
+
// Sidecar format (.planning/.compaction-snapshot.md):
|
|
25
|
+
// # Pre-compaction snapshot — {ISO timestamp}
|
|
26
|
+
// ## State
|
|
27
|
+
// Phase: 2 of 4 — built
|
|
28
|
+
// ## Work in flight
|
|
29
|
+
// - 3 modified files (uncommitted)
|
|
30
|
+
// - foo.ts, bar.ts, baz.test.ts
|
|
31
|
+
// ## Recent commits (last 5)
|
|
32
|
+
// - abc1234 feat: implement signin
|
|
33
|
+
// - def5678 test: signin happy path
|
|
34
|
+
// ## Active planning artifacts
|
|
35
|
+
// - .planning/phase-2-plan.md (modified 14m ago)
|
|
36
|
+
// - .planning/phase-2-verification.md (modified 3h ago)
|
|
37
|
+
|
|
38
|
+
const fs = require("fs");
|
|
39
|
+
const path = require("path");
|
|
40
|
+
const { spawnSync } = require("child_process");
|
|
41
|
+
|
|
42
|
+
const _traceStart = Date.now();
|
|
43
|
+
|
|
44
|
+
function qualiaHome() {
|
|
45
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
46
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
47
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
48
|
+
return path.join(require("os").homedir(), ".claude");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _trace(result, extra) {
|
|
52
|
+
try {
|
|
53
|
+
const traceDir = path.join(qualiaHome(), ".qualia-traces");
|
|
54
|
+
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
55
|
+
fs.appendFileSync(
|
|
56
|
+
path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
hook: "pre-compact",
|
|
59
|
+
result,
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
|
+
duration_ms: Date.now() - _traceStart,
|
|
62
|
+
...extra,
|
|
63
|
+
}) + "\n",
|
|
64
|
+
);
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function git(args, opts = {}) {
|
|
69
|
+
try {
|
|
70
|
+
const r = spawnSync("git", args, {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
timeout: 2000,
|
|
73
|
+
shell: process.platform === "win32",
|
|
74
|
+
...opts,
|
|
75
|
+
});
|
|
76
|
+
if (r.status !== 0) return "";
|
|
77
|
+
return (r.stdout || "").trim();
|
|
78
|
+
} catch {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readJson(p) {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function safeStatM(p) {
|
|
92
|
+
try { return fs.statSync(p).mtimeMs; } catch { return 0; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function humanAge(ms) {
|
|
96
|
+
if (ms < 60_000) return `${Math.round(ms / 1000)}s ago`;
|
|
97
|
+
if (ms < 3600_000) return `${Math.round(ms / 60_000)}m ago`;
|
|
98
|
+
if (ms < 86_400_000) return `${Math.round(ms / 3600_000)}h ago`;
|
|
99
|
+
return `${Math.round(ms / 86_400_000)}d ago`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const cwd = process.cwd();
|
|
104
|
+
const repoRoot = git(["rev-parse", "--show-toplevel"], { cwd }) || cwd;
|
|
105
|
+
const planningDir = path.join(repoRoot, ".planning");
|
|
106
|
+
|
|
107
|
+
// Skip if not a Qualia project (no .planning/ dir) — nothing to snapshot.
|
|
108
|
+
if (!fs.existsSync(planningDir)) {
|
|
109
|
+
_trace("skip", { reason: "no-planning-dir" });
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
const tracking = readJson(path.join(planningDir, "tracking.json")) || {};
|
|
115
|
+
const stateText = (() => {
|
|
116
|
+
try { return fs.readFileSync(path.join(planningDir, "STATE.md"), "utf8"); }
|
|
117
|
+
catch { return ""; }
|
|
118
|
+
})();
|
|
119
|
+
|
|
120
|
+
// State header line (one line from STATE.md, or fall back to tracking.json).
|
|
121
|
+
const stateLine = (() => {
|
|
122
|
+
const phaseLine = stateText.split("\n").find((l) => /^Phase:/i.test(l));
|
|
123
|
+
const statusLine = stateText.split("\n").find((l) => /^Status:/i.test(l));
|
|
124
|
+
if (phaseLine || statusLine) return [phaseLine, statusLine].filter(Boolean).join(" — ");
|
|
125
|
+
const p = tracking.phase || "?";
|
|
126
|
+
const total = tracking.total_phases || tracking.phase_total || "?";
|
|
127
|
+
const status = tracking.status || "?";
|
|
128
|
+
return `Phase: ${p} of ${total} — ${status}`;
|
|
129
|
+
})();
|
|
130
|
+
|
|
131
|
+
// Work in flight: uncommitted modified files.
|
|
132
|
+
const modified = (() => {
|
|
133
|
+
const out = git(["diff", "--name-only", "HEAD"], { cwd: repoRoot });
|
|
134
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
135
|
+
})();
|
|
136
|
+
const staged = (() => {
|
|
137
|
+
const out = git(["diff", "--name-only", "--cached"], { cwd: repoRoot });
|
|
138
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
139
|
+
})();
|
|
140
|
+
const inFlight = [...new Set([...modified, ...staged])].slice(0, 10);
|
|
141
|
+
|
|
142
|
+
// Recent commits — last 5, single-line.
|
|
143
|
+
const recentCommits = (() => {
|
|
144
|
+
const out = git(["log", "--pretty=%h %s", "-5"], { cwd: repoRoot });
|
|
145
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
146
|
+
})();
|
|
147
|
+
|
|
148
|
+
// Active planning artifacts: phase-*-* files modified in last 24h.
|
|
149
|
+
const activePlanning = (() => {
|
|
150
|
+
try {
|
|
151
|
+
const files = fs.readdirSync(planningDir).filter((f) => /^phase-\d+/.test(f) && f.endsWith(".md"));
|
|
152
|
+
const day = 24 * 3600_000;
|
|
153
|
+
return files
|
|
154
|
+
.map((f) => ({ name: f, age: now - safeStatM(path.join(planningDir, f)) }))
|
|
155
|
+
.filter((x) => x.age < day)
|
|
156
|
+
.sort((a, b) => a.age - b.age)
|
|
157
|
+
.slice(0, 5);
|
|
158
|
+
} catch { return []; }
|
|
159
|
+
})();
|
|
160
|
+
|
|
161
|
+
// Open deviations / verification FAILs in current phase.
|
|
162
|
+
const phaseNum = tracking.phase || 0;
|
|
163
|
+
const verifyFails = (() => {
|
|
164
|
+
if (!phaseNum) return [];
|
|
165
|
+
const vPath = path.join(planningDir, `phase-${phaseNum}-verification.md`);
|
|
166
|
+
if (!fs.existsSync(vPath)) return [];
|
|
167
|
+
try {
|
|
168
|
+
const v = fs.readFileSync(vPath, "utf8");
|
|
169
|
+
const fails = (v.match(/^[^\n]*\bFAIL\b[^\n]*$/gm) || []).slice(0, 5);
|
|
170
|
+
const insufficient = (v.match(/^[^\n]*INSUFFICIENT EVIDENCE[^\n]*$/gm) || []).slice(0, 3);
|
|
171
|
+
return [...fails, ...insufficient];
|
|
172
|
+
} catch { return []; }
|
|
173
|
+
})();
|
|
174
|
+
|
|
175
|
+
// Compose snapshot. Markdown so the next session can render it as-is.
|
|
176
|
+
const ts = new Date().toISOString();
|
|
177
|
+
const lines = [
|
|
178
|
+
`# Pre-compaction snapshot — ${ts}`,
|
|
179
|
+
"",
|
|
180
|
+
"_Written by `hooks/pre-compact.js` immediately before Claude Code compacted the context window. `/qualia-resume` and session-start surface this so the next session sees what was active._",
|
|
181
|
+
"",
|
|
182
|
+
"## State",
|
|
183
|
+
stateLine || "(no state info)",
|
|
184
|
+
"",
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
lines.push("## Work in flight");
|
|
188
|
+
if (inFlight.length === 0) {
|
|
189
|
+
lines.push("(no uncommitted changes)");
|
|
190
|
+
} else {
|
|
191
|
+
lines.push(`${inFlight.length} file(s):`);
|
|
192
|
+
for (const f of inFlight) lines.push(`- ${f}`);
|
|
193
|
+
}
|
|
194
|
+
lines.push("");
|
|
195
|
+
|
|
196
|
+
lines.push("## Recent commits");
|
|
197
|
+
if (recentCommits.length === 0) {
|
|
198
|
+
lines.push("(no commit history)");
|
|
199
|
+
} else {
|
|
200
|
+
for (const c of recentCommits) lines.push(`- ${c}`);
|
|
201
|
+
}
|
|
202
|
+
lines.push("");
|
|
203
|
+
|
|
204
|
+
lines.push("## Active planning artifacts (modified in last 24h)");
|
|
205
|
+
if (activePlanning.length === 0) {
|
|
206
|
+
lines.push("(none)");
|
|
207
|
+
} else {
|
|
208
|
+
for (const p of activePlanning) lines.push(`- ${p.name} — ${humanAge(p.age)}`);
|
|
209
|
+
}
|
|
210
|
+
lines.push("");
|
|
211
|
+
|
|
212
|
+
if (verifyFails.length > 0) {
|
|
213
|
+
lines.push("## Open verification issues");
|
|
214
|
+
for (const f of verifyFails) lines.push(`- ${f.trim()}`);
|
|
215
|
+
lines.push("");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
lines.push("---");
|
|
219
|
+
lines.push("_Sidecar file. Not committed to git. Read by `/qualia-resume` and `session-start` to restore in-flight context after compaction._");
|
|
220
|
+
|
|
221
|
+
fs.writeFileSync(path.join(planningDir, ".compaction-snapshot.md"), lines.join("\n") + "\n");
|
|
222
|
+
_trace("snapshot", {
|
|
223
|
+
in_flight: inFlight.length,
|
|
224
|
+
commits: recentCommits.length,
|
|
225
|
+
active_planning: activePlanning.length,
|
|
226
|
+
verify_fails: verifyFails.length,
|
|
227
|
+
});
|
|
228
|
+
process.exit(0);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
_trace("error", { error: err && err.message ? err.message : String(err) });
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|