qualia-framework 6.2.9 → 6.2.10
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 +14 -11
- package/agents/builder.md +7 -7
- package/agents/planner.md +39 -3
- package/agents/research-synthesizer.md +1 -1
- package/agents/researcher.md +3 -3
- package/agents/roadmapper.md +7 -7
- package/agents/verifier.md +18 -6
- package/agents/visual-evaluator.md +8 -7
- package/bin/cli.js +111 -14
- package/bin/contract-runner.js +219 -0
- package/bin/host-adapters.js +66 -0
- package/bin/install.js +99 -152
- package/bin/plan-contract.js +99 -2
- package/bin/planning-hygiene.js +262 -0
- package/bin/runtime-manifest.js +32 -0
- package/bin/state-ledger.js +184 -0
- package/bin/state.js +299 -20
- package/bin/trust-score.js +276 -0
- package/docs/onboarding.html +5 -4
- package/guide.md +3 -2
- package/package.json +1 -1
- package/qualia-design/design-rubric.md +17 -5
- package/qualia-design/frontend.md +5 -1
- package/qualia-design/graphics.md +47 -0
- package/rules/command-output.md +35 -0
- package/skills/qualia/SKILL.md +10 -10
- package/skills/qualia-build/SKILL.md +20 -14
- package/skills/qualia-debug/SKILL.md +16 -8
- package/skills/qualia-discuss/SKILL.md +10 -10
- package/skills/qualia-doctor/SKILL.md +140 -0
- package/skills/qualia-feature/SKILL.md +23 -21
- package/skills/qualia-fix/SKILL.md +216 -0
- package/skills/qualia-flush/SKILL.md +9 -9
- package/skills/qualia-handoff/SKILL.md +9 -9
- package/skills/qualia-help/SKILL.md +3 -3
- package/skills/qualia-hook-gen/SKILL.md +1 -1
- package/skills/qualia-idk/SKILL.md +4 -4
- package/skills/qualia-issues/SKILL.md +2 -2
- package/skills/qualia-learn/SKILL.md +10 -10
- package/skills/qualia-map/SKILL.md +2 -2
- package/skills/qualia-milestone/SKILL.md +15 -15
- package/skills/qualia-new/REFERENCE.md +9 -9
- package/skills/qualia-new/SKILL.md +14 -14
- package/skills/qualia-optimize/REFERENCE.md +1 -1
- package/skills/qualia-optimize/SKILL.md +23 -16
- package/skills/qualia-pause/SKILL.md +2 -2
- package/skills/qualia-plan/SKILL.md +23 -13
- package/skills/qualia-polish/REFERENCE.md +14 -14
- package/skills/qualia-polish/SKILL.md +64 -19
- package/skills/qualia-polish/scripts/loop.mjs +3 -3
- package/skills/qualia-polish/scripts/score.mjs +9 -3
- package/skills/qualia-postmortem/SKILL.md +9 -9
- package/skills/qualia-report/SKILL.md +23 -23
- package/skills/qualia-research/SKILL.md +5 -5
- package/skills/qualia-resume/SKILL.md +4 -4
- package/skills/qualia-review/SKILL.md +28 -12
- package/skills/qualia-road/SKILL.md +18 -5
- package/skills/qualia-ship/SKILL.md +22 -22
- package/skills/qualia-skill-new/SKILL.md +13 -13
- package/skills/qualia-test/SKILL.md +5 -5
- package/skills/qualia-triage/SKILL.md +1 -1
- package/skills/qualia-verify/SKILL.md +37 -23
- package/skills/qualia-vibe/SKILL.md +13 -10
- package/skills/qualia-vibe/scripts/extract.mjs +1 -1
- package/skills/zoho-workflow/SKILL.md +1 -1
- package/templates/help.html +12 -10
- package/tests/bin.test.sh +34 -4
- package/tests/install-smoke.test.sh +22 -2
- package/tests/lib.test.sh +290 -0
- package/tests/runner.js +3 -0
- package/tests/skills.test.sh +4 -4
- package/tests/state.test.sh +65 -3
package/bin/plan-contract.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Plan contract validator + helpers. See docs/plan-contract.md.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Library + tiny CLI. Required by state.js and by skills that emit/consume
|
|
5
|
+
// `.planning/phase-{N}-contract.json`.
|
|
6
6
|
//
|
|
7
7
|
// Zero npm dependencies. Hand-rolled validator, ~100 LOC.
|
|
8
8
|
|
|
@@ -241,11 +241,108 @@ function checkDrift(contractPath, planMdPath) {
|
|
|
241
241
|
return { ok: true, drift: stored !== current, stored, current };
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
function readContractFile(contractPath) {
|
|
245
|
+
if (!fs.existsSync(contractPath)) {
|
|
246
|
+
return { ok: false, error: "CONTRACT_MISSING", message: `Contract file not found: ${contractPath}` };
|
|
247
|
+
}
|
|
248
|
+
const parsed = parseSafely(fs.readFileSync(contractPath, "utf8"));
|
|
249
|
+
if (!parsed.ok) {
|
|
250
|
+
return { ok: false, error: "CONTRACT_UNPARSEABLE", message: parsed.error };
|
|
251
|
+
}
|
|
252
|
+
return { ok: true, contract: parsed.value };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function cliUsage() {
|
|
256
|
+
console.error([
|
|
257
|
+
"Usage:",
|
|
258
|
+
" plan-contract.js validate <contract.json> [--json]",
|
|
259
|
+
" plan-contract.js drift <contract.json> <plan.md> [--json]",
|
|
260
|
+
" plan-contract.js hash <plan.md>",
|
|
261
|
+
].join("\n"));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function printResult(payload, asJson) {
|
|
265
|
+
if (asJson) {
|
|
266
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (payload.ok) {
|
|
270
|
+
if (payload.action === "validate") console.log(`VALID ${payload.path}`);
|
|
271
|
+
else if (payload.action === "drift") console.log(payload.drift ? "DRIFT" : "NO_DRIFT");
|
|
272
|
+
else if (payload.action === "hash") console.log(payload.hash);
|
|
273
|
+
else console.log("OK");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
console.error(`${payload.error || "ERROR"}: ${payload.message || (payload.errors || []).join("; ")}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function main(argv) {
|
|
280
|
+
const cmd = argv[2];
|
|
281
|
+
const asJson = argv.includes("--json");
|
|
282
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
283
|
+
cliUsage();
|
|
284
|
+
return 2;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (cmd === "validate") {
|
|
288
|
+
const contractPath = argv[3];
|
|
289
|
+
if (!contractPath || contractPath.startsWith("--")) {
|
|
290
|
+
cliUsage();
|
|
291
|
+
return 2;
|
|
292
|
+
}
|
|
293
|
+
const loaded = readContractFile(contractPath);
|
|
294
|
+
if (!loaded.ok) {
|
|
295
|
+
printResult({ ok: false, action: "validate", path: contractPath, ...loaded }, asJson);
|
|
296
|
+
return 2;
|
|
297
|
+
}
|
|
298
|
+
const errors = validate(loaded.contract);
|
|
299
|
+
const payload = { ok: errors.length === 0, action: "validate", path: contractPath, errors };
|
|
300
|
+
printResult(payload, asJson);
|
|
301
|
+
return payload.ok ? 0 : 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (cmd === "drift") {
|
|
305
|
+
const contractPath = argv[3];
|
|
306
|
+
const planPath = argv[4];
|
|
307
|
+
if (!contractPath || !planPath || contractPath.startsWith("--") || planPath.startsWith("--")) {
|
|
308
|
+
cliUsage();
|
|
309
|
+
return 2;
|
|
310
|
+
}
|
|
311
|
+
const result = checkDrift(contractPath, planPath);
|
|
312
|
+
const payload = { ok: !!result.ok && !result.drift, action: "drift", path: contractPath, plan: planPath, ...result };
|
|
313
|
+
printResult(payload, asJson);
|
|
314
|
+
return payload.ok ? 0 : 1;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (cmd === "hash") {
|
|
318
|
+
const planPath = argv[3];
|
|
319
|
+
if (!planPath || planPath.startsWith("--")) {
|
|
320
|
+
cliUsage();
|
|
321
|
+
return 2;
|
|
322
|
+
}
|
|
323
|
+
if (!fs.existsSync(planPath)) {
|
|
324
|
+
printResult({ ok: false, action: "hash", error: "PLAN_MISSING", message: `Plan file not found: ${planPath}` }, asJson);
|
|
325
|
+
return 2;
|
|
326
|
+
}
|
|
327
|
+
const hash = hashPlan(fs.readFileSync(planPath, "utf8"));
|
|
328
|
+
printResult({ ok: true, action: "hash", path: planPath, hash }, asJson);
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
cliUsage();
|
|
333
|
+
return 2;
|
|
334
|
+
}
|
|
335
|
+
|
|
244
336
|
module.exports = {
|
|
245
337
|
SCHEMA_VERSION,
|
|
246
338
|
validate,
|
|
247
339
|
parseSafely,
|
|
248
340
|
hashPlan,
|
|
249
341
|
checkDrift,
|
|
342
|
+
readContractFile,
|
|
250
343
|
findScopeReductionPhrases,
|
|
251
344
|
};
|
|
345
|
+
|
|
346
|
+
if (require.main === module) {
|
|
347
|
+
process.exit(main(process.argv));
|
|
348
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Scan and organize `.planning/` so project state stays navigable.
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const ROOT_EXACT = new Set([
|
|
8
|
+
"PROJECT.md",
|
|
9
|
+
"PRODUCT.md",
|
|
10
|
+
"DESIGN.md",
|
|
11
|
+
"CONTEXT.md",
|
|
12
|
+
"JOURNEY.md",
|
|
13
|
+
"ROADMAP.md",
|
|
14
|
+
"REQUIREMENTS.md",
|
|
15
|
+
"STATE.md",
|
|
16
|
+
"tracking.json",
|
|
17
|
+
"project-discovery.md",
|
|
18
|
+
"agent-runs.jsonl",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const ROOT_PATTERNS = [
|
|
22
|
+
/^phase-\d+(-gaps)?-plan\.md$/,
|
|
23
|
+
/^phase-\d+-contract\.json$/,
|
|
24
|
+
/^phase-\d+-verification\.md$/,
|
|
25
|
+
/^phase-\d+-context\.md$/,
|
|
26
|
+
/^phase-\d+-research\.md$/,
|
|
27
|
+
/^phase-\d+-postmortem\.md$/,
|
|
28
|
+
/^phase-\d+-deviations\.json$/,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const ALLOWED_DIRS = new Set([
|
|
32
|
+
"archive",
|
|
33
|
+
"assets",
|
|
34
|
+
"agent-runs",
|
|
35
|
+
"decisions",
|
|
36
|
+
"design",
|
|
37
|
+
"evidence",
|
|
38
|
+
"reports",
|
|
39
|
+
"research",
|
|
40
|
+
"snapshots",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function usage() {
|
|
44
|
+
return `planning-hygiene.js — keep .planning organized
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
node planning-hygiene.js scan [--json] [--planning-dir .planning]
|
|
48
|
+
node planning-hygiene.js organize --write [--planning-dir .planning]
|
|
49
|
+
|
|
50
|
+
scan is read-only. organize requires --write and only moves loose artifacts.`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const args = argv.slice(2);
|
|
55
|
+
const opts = {
|
|
56
|
+
command: args.find((arg) => !arg.startsWith("-")) || "scan",
|
|
57
|
+
json: args.includes("--json"),
|
|
58
|
+
write: args.includes("--write"),
|
|
59
|
+
planningDir: ".planning",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const dirIndex = args.indexOf("--planning-dir");
|
|
63
|
+
if (dirIndex !== -1 && args[dirIndex + 1]) {
|
|
64
|
+
opts.planningDir = args[dirIndex + 1];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
68
|
+
opts.help = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return opts;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isAllowedRootFile(name) {
|
|
75
|
+
return ROOT_EXACT.has(name) || ROOT_PATTERNS.some((pattern) => pattern.test(name));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function routeForLooseFile(name) {
|
|
79
|
+
if (/^DEBUG-\d{4}-\d{2}-\d{2}/.test(name)) return path.join("reports", "debug", name);
|
|
80
|
+
if (/^FIX-\d{4}-\d{2}-\d{2}/.test(name)) return path.join("reports", "fix", name);
|
|
81
|
+
if (name === "REVIEW.md" || /^REVIEW-/.test(name)) return path.join("reports", "review", name);
|
|
82
|
+
if (name === "OPTIMIZE.md" || /^OPTIMIZE-/.test(name)) return path.join("reports", "optimize", name);
|
|
83
|
+
if (/^REFACTOR-/.test(name)) return path.join("reports", "refactor", name);
|
|
84
|
+
if (/^polish-critique-/.test(name)) return path.join("reports", "polish", name);
|
|
85
|
+
if (/^vibe-after\./.test(name)) return path.join("assets", "vibe", name);
|
|
86
|
+
if (name === "DESIGN-extracted.md") return path.join("design", name);
|
|
87
|
+
return path.join("archive", "loose", name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function listPlanningRoot(planningDir) {
|
|
91
|
+
if (!fs.existsSync(planningDir)) {
|
|
92
|
+
return { exists: false, entries: [] };
|
|
93
|
+
}
|
|
94
|
+
const entries = fs.readdirSync(planningDir, { withFileTypes: true })
|
|
95
|
+
.filter((entry) => entry.name !== ".DS_Store")
|
|
96
|
+
.map((entry) => ({
|
|
97
|
+
name: entry.name,
|
|
98
|
+
type: entry.isDirectory() ? "dir" : "file",
|
|
99
|
+
}))
|
|
100
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
101
|
+
|
|
102
|
+
return { exists: true, entries };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function scan(planningDir) {
|
|
106
|
+
const root = listPlanningRoot(planningDir);
|
|
107
|
+
if (!root.exists) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
planning_dir: planningDir,
|
|
111
|
+
status: "missing",
|
|
112
|
+
loose: [],
|
|
113
|
+
unknown_dirs: [],
|
|
114
|
+
message: ".planning directory not found",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const loose = [];
|
|
119
|
+
const unknownDirs = [];
|
|
120
|
+
|
|
121
|
+
for (const entry of root.entries) {
|
|
122
|
+
if (entry.type === "dir") {
|
|
123
|
+
if (!ALLOWED_DIRS.has(entry.name)) {
|
|
124
|
+
unknownDirs.push({
|
|
125
|
+
path: entry.name,
|
|
126
|
+
suggested_path: path.join("archive", "loose", entry.name),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!isAllowedRootFile(entry.name)) {
|
|
133
|
+
loose.push({
|
|
134
|
+
path: entry.name,
|
|
135
|
+
suggested_path: routeForLooseFile(entry.name),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ok: loose.length === 0 && unknownDirs.length === 0,
|
|
142
|
+
planning_dir: planningDir,
|
|
143
|
+
status: loose.length === 0 && unknownDirs.length === 0 ? "clean" : "needs_organizing",
|
|
144
|
+
loose,
|
|
145
|
+
unknown_dirs: unknownDirs,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function uniqueDestination(planningDir, relPath) {
|
|
150
|
+
const parsed = path.parse(relPath);
|
|
151
|
+
let candidate = relPath;
|
|
152
|
+
let index = 2;
|
|
153
|
+
while (fs.existsSync(path.join(planningDir, candidate))) {
|
|
154
|
+
candidate = path.join(parsed.dir, `${parsed.name}-${index}${parsed.ext}`);
|
|
155
|
+
index += 1;
|
|
156
|
+
}
|
|
157
|
+
return candidate;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function moveEntry(planningDir, fromRel, toRel) {
|
|
161
|
+
const from = path.join(planningDir, fromRel);
|
|
162
|
+
const safeToRel = uniqueDestination(planningDir, toRel);
|
|
163
|
+
const to = path.join(planningDir, safeToRel);
|
|
164
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
165
|
+
fs.renameSync(from, to);
|
|
166
|
+
return safeToRel;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function organize(planningDir, write) {
|
|
170
|
+
const result = scan(planningDir);
|
|
171
|
+
if (!result.ok && result.status === "missing") {
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const moves = [];
|
|
176
|
+
for (const item of [...result.loose, ...result.unknown_dirs]) {
|
|
177
|
+
if (!write) {
|
|
178
|
+
moves.push({ from: item.path, to: item.suggested_path, moved: false });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const actual = moveEntry(planningDir, item.path, item.suggested_path);
|
|
182
|
+
moves.push({ from: item.path, to: actual, moved: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
planning_dir: planningDir,
|
|
188
|
+
status: write ? "organized" : "dry_run",
|
|
189
|
+
moves,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function printHuman(result) {
|
|
194
|
+
if (result.status === "missing") {
|
|
195
|
+
console.log(`planning hygiene: ${result.message}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (result.status === "clean") {
|
|
200
|
+
console.log(`planning hygiene: clean (${result.planning_dir})`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (result.moves) {
|
|
205
|
+
console.log(`planning hygiene: ${result.status} (${result.moves.length} move${result.moves.length === 1 ? "" : "s"})`);
|
|
206
|
+
for (const move of result.moves) {
|
|
207
|
+
console.log(` ${move.moved ? "moved" : "would move"} ${move.from} -> ${move.to}`);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const count = result.loose.length + result.unknown_dirs.length;
|
|
213
|
+
console.log(`planning hygiene: needs organizing (${count} item${count === 1 ? "" : "s"})`);
|
|
214
|
+
for (const item of result.loose) {
|
|
215
|
+
console.log(` loose file ${item.path} -> ${item.suggested_path}`);
|
|
216
|
+
}
|
|
217
|
+
for (const item of result.unknown_dirs) {
|
|
218
|
+
console.log(` loose dir ${item.path} -> ${item.suggested_path}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function main() {
|
|
223
|
+
const opts = parseArgs(process.argv);
|
|
224
|
+
if (opts.help) {
|
|
225
|
+
console.log(usage());
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let result;
|
|
230
|
+
if (opts.command === "scan") {
|
|
231
|
+
result = scan(opts.planningDir);
|
|
232
|
+
} else if (opts.command === "organize" || opts.command === "fix") {
|
|
233
|
+
result = organize(opts.planningDir, opts.write);
|
|
234
|
+
} else {
|
|
235
|
+
console.error(usage());
|
|
236
|
+
return 2;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (opts.json) {
|
|
240
|
+
console.log(JSON.stringify(result, null, 2));
|
|
241
|
+
} else {
|
|
242
|
+
printHuman(result);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (result.status === "missing") return 1;
|
|
246
|
+
if (opts.command === "scan" && !result.ok) return 1;
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (require.main === module) {
|
|
251
|
+
process.exitCode = main();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
ALLOWED_DIRS,
|
|
256
|
+
ROOT_EXACT,
|
|
257
|
+
ROOT_PATTERNS,
|
|
258
|
+
isAllowedRootFile,
|
|
259
|
+
routeForLooseFile,
|
|
260
|
+
scan,
|
|
261
|
+
organize,
|
|
262
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Shared runtime manifest for installed Qualia bin scripts.
|
|
3
|
+
|
|
4
|
+
const RUNTIME_BIN_SCRIPTS = [
|
|
5
|
+
{ file: "runtime-manifest.js", label: "runtime-manifest.js (shared install manifest)" },
|
|
6
|
+
{ file: "host-adapters.js", label: "host-adapters.js (Claude/Codex path renderer)" },
|
|
7
|
+
{ file: "state.js", label: "state.js (state machine)" },
|
|
8
|
+
{ file: "qualia-ui.js", label: "qualia-ui.js (cosmetics library)" },
|
|
9
|
+
{ file: "statusline.js", label: "statusline.js (status bar renderer)" },
|
|
10
|
+
{ file: "knowledge.js", label: "knowledge.js (memory-layer loader)" },
|
|
11
|
+
{ file: "knowledge-flush.js", label: "knowledge-flush.js (cron-runnable flush)" },
|
|
12
|
+
{ file: "state-ledger.js", label: "state-ledger.js (hash-chained state event ledger)" },
|
|
13
|
+
{ file: "plan-contract.js", label: "plan-contract.js (plan JSON validator)" },
|
|
14
|
+
{ file: "contract-runner.js", label: "contract-runner.js (contract evidence runner)" },
|
|
15
|
+
{ file: "agent-runs.js", label: "agent-runs.js (agent telemetry writer)" },
|
|
16
|
+
{ file: "slop-detect.mjs", label: "slop-detect.mjs (anti-pattern scanner — runs pre-commit on frontend builds)" },
|
|
17
|
+
{ file: "erp-retry.js", label: "erp-retry.js (ERP report retry queue — drained by session-start hook and erp-flush CLI)" },
|
|
18
|
+
{ file: "report-payload.js", label: "report-payload.js (Framework -> ERP report payload builder)" },
|
|
19
|
+
{ file: "project-snapshot.js", label: "project-snapshot.js (ERP/admin project progress snapshot)" },
|
|
20
|
+
{ file: "trust-score.js", label: "trust-score.js (harness health scoring)" },
|
|
21
|
+
{ file: "codex-goal.js", label: "codex-goal.js (Codex /goal objective + token-budget suggester)" },
|
|
22
|
+
{ file: "planning-hygiene.js", label: "planning-hygiene.js (.planning organization scanner)" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function binFiles() {
|
|
26
|
+
return RUNTIME_BIN_SCRIPTS.map((script) => script.file);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
RUNTIME_BIN_SCRIPTS,
|
|
31
|
+
binFiles,
|
|
32
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// State ledger — append-only, hash-chained events for Qualia state mutations.
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
|
|
8
|
+
const SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
function qualiaDir(cwd = process.cwd()) {
|
|
11
|
+
return path.join(cwd, ".planning", "qualia");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ledgerPath(cwd = process.cwd()) {
|
|
15
|
+
return path.join(qualiaDir(cwd), "state.jsonl");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureDir(p) {
|
|
19
|
+
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stable(value) {
|
|
23
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
24
|
+
if (value && typeof value === "object") {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const key of Object.keys(value).sort()) out[key] = stable(value[key]);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stableJson(value) {
|
|
33
|
+
return JSON.stringify(stable(value));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hash(value) {
|
|
37
|
+
return `sha256:${crypto.createHash("sha256").update(String(value)).digest("hex")}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hashContent(content) {
|
|
41
|
+
if (content == null) return null;
|
|
42
|
+
return hash(content);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function newEventId() {
|
|
46
|
+
const ts = Date.now().toString(36).toUpperCase().padStart(10, "0");
|
|
47
|
+
const rand = crypto.randomBytes(6).toString("hex").toUpperCase();
|
|
48
|
+
return `${ts}${rand}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readEvents(cwd = process.cwd()) {
|
|
52
|
+
const file = ledgerPath(cwd);
|
|
53
|
+
if (!fs.existsSync(file)) return [];
|
|
54
|
+
const events = [];
|
|
55
|
+
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean)) {
|
|
56
|
+
try { events.push(JSON.parse(line)); } catch {}
|
|
57
|
+
}
|
|
58
|
+
return events;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function lastEvent(cwd = process.cwd()) {
|
|
62
|
+
const events = readEvents(cwd);
|
|
63
|
+
return events.length ? events[events.length - 1] : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function actor() {
|
|
67
|
+
return process.env.QUALIA_ACTOR ||
|
|
68
|
+
process.env.CLAUDE_USER_NAME ||
|
|
69
|
+
process.env.USER ||
|
|
70
|
+
process.env.USERNAME ||
|
|
71
|
+
"unknown";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function compactState(state) {
|
|
75
|
+
if (!state) return null;
|
|
76
|
+
return {
|
|
77
|
+
phase: state.phase,
|
|
78
|
+
status: state.status,
|
|
79
|
+
phase_name: state.phase_name,
|
|
80
|
+
total_phases: state.total_phases,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function compactTracking(tracking) {
|
|
85
|
+
if (!tracking) return null;
|
|
86
|
+
return {
|
|
87
|
+
project: tracking.project,
|
|
88
|
+
phase: tracking.phase,
|
|
89
|
+
status: tracking.status,
|
|
90
|
+
milestone: tracking.milestone,
|
|
91
|
+
milestone_name: tracking.milestone_name,
|
|
92
|
+
tasks_done: tracking.tasks_done,
|
|
93
|
+
tasks_total: tracking.tasks_total,
|
|
94
|
+
verification: tracking.verification,
|
|
95
|
+
report_seq: tracking.report_seq,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function eventHash(event) {
|
|
100
|
+
const clean = { ...event };
|
|
101
|
+
delete clean.event_hash;
|
|
102
|
+
return hash(stableJson(clean));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function append(cwd, input) {
|
|
106
|
+
const previous = lastEvent(cwd);
|
|
107
|
+
const event = {
|
|
108
|
+
schema_version: SCHEMA_VERSION,
|
|
109
|
+
event_id: input.event_id || newEventId(),
|
|
110
|
+
timestamp: input.timestamp || new Date().toISOString(),
|
|
111
|
+
actor: input.actor || actor(),
|
|
112
|
+
command: input.command || "",
|
|
113
|
+
action: input.action,
|
|
114
|
+
result: input.result || "success",
|
|
115
|
+
phase_before: input.phase_before,
|
|
116
|
+
phase_after: input.phase_after,
|
|
117
|
+
status_before: input.status_before,
|
|
118
|
+
status_after: input.status_after,
|
|
119
|
+
state_before: compactState(input.state_before),
|
|
120
|
+
state_after: compactState(input.state_after),
|
|
121
|
+
tracking_before: compactTracking(input.tracking_before),
|
|
122
|
+
tracking_after: compactTracking(input.tracking_after),
|
|
123
|
+
state_hash_before: hashContent(input.state_raw_before),
|
|
124
|
+
state_hash_after: hashContent(input.state_raw_after),
|
|
125
|
+
tracking_hash_before: hashContent(input.tracking_raw_before),
|
|
126
|
+
tracking_hash_after: hashContent(input.tracking_raw_after),
|
|
127
|
+
evidence_refs: Array.isArray(input.evidence_refs) ? input.evidence_refs : [],
|
|
128
|
+
previous_event_hash: previous ? previous.event_hash : null,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
for (const key of Object.keys(event)) {
|
|
132
|
+
if (event[key] === undefined) delete event[key];
|
|
133
|
+
}
|
|
134
|
+
event.event_hash = eventHash(event);
|
|
135
|
+
|
|
136
|
+
ensureDir(qualiaDir(cwd));
|
|
137
|
+
fs.appendFileSync(ledgerPath(cwd), JSON.stringify(event) + "\n");
|
|
138
|
+
return { ok: true, event_id: event.event_id, event_hash: event.event_hash };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validate(cwd = process.cwd()) {
|
|
142
|
+
const events = readEvents(cwd);
|
|
143
|
+
const errors = [];
|
|
144
|
+
let previousHash = null;
|
|
145
|
+
events.forEach((event, index) => {
|
|
146
|
+
if (event.previous_event_hash !== previousHash) {
|
|
147
|
+
errors.push(`event ${index + 1}: previous_event_hash mismatch`);
|
|
148
|
+
}
|
|
149
|
+
const expected = eventHash(event);
|
|
150
|
+
if (event.event_hash !== expected) {
|
|
151
|
+
errors.push(`event ${index + 1}: event_hash mismatch`);
|
|
152
|
+
}
|
|
153
|
+
previousHash = event.event_hash || null;
|
|
154
|
+
});
|
|
155
|
+
return { ok: errors.length === 0, count: events.length, errors };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function main(argv) {
|
|
159
|
+
const cmd = argv[2] || "validate";
|
|
160
|
+
if (cmd === "validate") {
|
|
161
|
+
const result = validate(process.cwd());
|
|
162
|
+
console.log(JSON.stringify(result, null, 2));
|
|
163
|
+
return result.ok ? 0 : 1;
|
|
164
|
+
}
|
|
165
|
+
if (cmd === "tail") {
|
|
166
|
+
const limit = parseInt(argv[3] || "10", 10);
|
|
167
|
+
console.log(JSON.stringify(readEvents(process.cwd()).slice(-limit), null, 2));
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
console.error("Usage: state-ledger.js <validate|tail> [limit]");
|
|
171
|
+
return 2;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
SCHEMA_VERSION,
|
|
176
|
+
append,
|
|
177
|
+
validate,
|
|
178
|
+
readEvents,
|
|
179
|
+
ledgerPath,
|
|
180
|
+
hash,
|
|
181
|
+
stableJson,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (require.main === module) process.exit(main(process.argv));
|