qualia-framework 6.2.9 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/AGENTS.md +1 -0
  2. package/CLAUDE.md +1 -0
  3. package/README.md +26 -30
  4. package/agents/builder.md +7 -7
  5. package/agents/planner.md +39 -3
  6. package/agents/research-synthesizer.md +1 -1
  7. package/agents/researcher.md +3 -3
  8. package/agents/roadmapper.md +7 -7
  9. package/agents/verifier.md +18 -6
  10. package/agents/visual-evaluator.md +8 -7
  11. package/bin/cli.js +160 -16
  12. package/bin/command-surface.js +71 -0
  13. package/bin/contract-runner.js +219 -0
  14. package/bin/harness-eval.js +296 -0
  15. package/bin/host-adapters.js +66 -0
  16. package/bin/install.js +116 -172
  17. package/bin/knowledge-flush.js +21 -10
  18. package/bin/knowledge.js +1 -1
  19. package/bin/plan-contract.js +99 -2
  20. package/bin/planning-hygiene.js +262 -0
  21. package/bin/project-snapshot.js +20 -0
  22. package/bin/report-payload.js +18 -0
  23. package/bin/runtime-manifest.js +35 -0
  24. package/bin/state-ledger.js +184 -0
  25. package/bin/state.js +330 -20
  26. package/bin/trust-score.js +268 -0
  27. package/bin/work-packet.js +228 -0
  28. package/docs/erp-contract.md +81 -1
  29. package/docs/onboarding.html +4 -14
  30. package/guide.md +16 -16
  31. package/hooks/fawzi-approval-guard.js +143 -0
  32. package/hooks/pre-deploy-gate.js +74 -1
  33. package/hooks/session-start.js +29 -1
  34. package/package.json +1 -1
  35. package/qualia-design/design-rubric.md +17 -5
  36. package/qualia-design/frontend.md +6 -2
  37. package/qualia-design/graphics.md +47 -0
  38. package/rules/codex-goal.md +1 -1
  39. package/rules/command-output.md +35 -0
  40. package/rules/one-opinion.md +2 -2
  41. package/rules/speed.md +0 -1
  42. package/skills/qualia/SKILL.md +12 -12
  43. package/skills/qualia-build/SKILL.md +20 -14
  44. package/skills/qualia-discuss/SKILL.md +10 -10
  45. package/skills/qualia-doctor/SKILL.md +140 -0
  46. package/skills/qualia-feature/SKILL.md +24 -22
  47. package/skills/qualia-fix/SKILL.md +216 -0
  48. package/skills/qualia-handoff/SKILL.md +9 -9
  49. package/skills/qualia-learn/SKILL.md +11 -11
  50. package/skills/qualia-map/SKILL.md +2 -2
  51. package/skills/qualia-milestone/SKILL.md +15 -15
  52. package/skills/qualia-new/REFERENCE.md +9 -9
  53. package/skills/qualia-new/SKILL.md +14 -14
  54. package/skills/qualia-optimize/REFERENCE.md +1 -1
  55. package/skills/qualia-optimize/SKILL.md +23 -16
  56. package/skills/qualia-plan/SKILL.md +23 -13
  57. package/skills/qualia-polish/REFERENCE.md +15 -15
  58. package/skills/qualia-polish/SKILL.md +81 -21
  59. package/skills/qualia-polish/scripts/loop.mjs +3 -3
  60. package/skills/qualia-polish/scripts/score.mjs +9 -3
  61. package/skills/{qualia-vibe/scripts/extract.mjs → qualia-polish/scripts/vibe-extract.mjs} +5 -5
  62. package/skills/{qualia-vibe/scripts/tokens.mjs → qualia-polish/scripts/vibe-tokens.mjs} +6 -6
  63. package/skills/qualia-postmortem/SKILL.md +9 -9
  64. package/skills/qualia-report/SKILL.md +23 -23
  65. package/skills/qualia-research/SKILL.md +5 -5
  66. package/skills/qualia-review/SKILL.md +28 -12
  67. package/skills/qualia-road/SKILL.md +30 -22
  68. package/skills/qualia-ship/SKILL.md +31 -24
  69. package/skills/qualia-test/SKILL.md +5 -5
  70. package/skills/qualia-verify/SKILL.md +45 -23
  71. package/skills/zoho-workflow/SKILL.md +1 -1
  72. package/templates/help.html +11 -20
  73. package/tests/bin.test.sh +178 -76
  74. package/tests/hooks.test.sh +81 -1
  75. package/tests/install-smoke.test.sh +35 -5
  76. package/tests/lib.test.sh +432 -0
  77. package/tests/published-install-smoke.test.sh +4 -3
  78. package/tests/refs.test.sh +9 -4
  79. package/tests/runner.js +32 -28
  80. package/tests/skills.test.sh +4 -4
  81. package/tests/state.test.sh +133 -3
  82. package/skills/qualia-debug/SKILL.md +0 -185
  83. package/skills/qualia-flush/SKILL.md +0 -198
  84. package/skills/qualia-help/SKILL.md +0 -74
  85. package/skills/qualia-hook-gen/SKILL.md +0 -206
  86. package/skills/qualia-idk/SKILL.md +0 -166
  87. package/skills/qualia-issues/SKILL.md +0 -151
  88. package/skills/qualia-pause/SKILL.md +0 -68
  89. package/skills/qualia-resume/SKILL.md +0 -52
  90. package/skills/qualia-skill-new/SKILL.md +0 -173
  91. package/skills/qualia-triage/SKILL.md +0 -152
  92. package/skills/qualia-vibe/SKILL.md +0 -226
  93. package/skills/qualia-zoom/SKILL.md +0 -51
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // Plan contract validator + helpers. See docs/plan-contract.md.
3
3
  //
4
- // Pure library no CLI dispatch. Required by state.js and by skills that
5
- // emit/consume `.planning/phase-{N}-contract.json`.
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
+ };
@@ -5,6 +5,8 @@ const https = require("https");
5
5
  const os = require("os");
6
6
  const path = require("path");
7
7
  const { spawnSync } = require("child_process");
8
+ const harnessEval = require("./harness-eval.js");
9
+ const { readLocalWorkPacket } = require("./work-packet.js");
8
10
 
9
11
  function readJson(file, fallback = {}) {
10
12
  try {
@@ -90,6 +92,8 @@ function buildSnapshot(options = {}) {
90
92
  const projectId = tracking.project_id || repoSlug(gitRemote) || path.basename(cwd);
91
93
  const currentMilestone = Number(tracking.milestone || 1);
92
94
  const currentPhase = Number(tracking.phase || 0);
95
+ const latestHarnessEval = harnessEval.latestEval(cwd);
96
+ const workPacket = readLocalWorkPacket(cwd);
93
97
  const totalPhases = Number(tracking.total_phases || 0);
94
98
  const lifetime = tracking.lifetime && typeof tracking.lifetime === "object" ? tracking.lifetime : {};
95
99
  const closedMilestones = Array.isArray(tracking.milestones) ? tracking.milestones : [];
@@ -118,6 +122,11 @@ function buildSnapshot(options = {}) {
118
122
  ...(uuid(tracking.erp_project_id) ? { erp_project_id: uuid(tracking.erp_project_id) } : {}),
119
123
  ...(uuid(tracking.client_id) ? { client_id: uuid(tracking.client_id) } : {}),
120
124
  ...(uuid(tracking.workspace_id) ? { workspace_id: uuid(tracking.workspace_id) } : {}),
125
+ ...(workPacket ? { work_packet_id: workPacket.id } : {}),
126
+ ...(workPacket && workPacket.assignment_id ? { assignment_id: workPacket.assignment_id } : {}),
127
+ ...(workPacket && workPacket.deadline_date
128
+ ? { assignment_deadline: workPacket.deadline_date }
129
+ : {}),
121
130
  },
122
131
  project: {
123
132
  name: tracking.project || path.basename(cwd),
@@ -137,6 +146,17 @@ function buildSnapshot(options = {}) {
137
146
  verification: tracking.verification || "pending",
138
147
  gap_cycles: (tracking.gap_cycles || {})[String(currentPhase)] || 0,
139
148
  },
149
+ quality: {
150
+ harness_eval: latestHarnessEval
151
+ ? {
152
+ status: latestHarnessEval.status,
153
+ score: latestHarnessEval.score,
154
+ phase: latestHarnessEval.phase,
155
+ generated_at: latestHarnessEval.generated_at,
156
+ artifact: latestHarnessEval.artifacts && latestHarnessEval.artifacts.json,
157
+ }
158
+ : null,
159
+ },
140
160
  journey: {
141
161
  total_milestones: journeyTotal,
142
162
  milestones: journey.map((milestone) => ({
@@ -3,6 +3,8 @@ const fs = require("fs");
3
3
  const os = require("os");
4
4
  const path = require("path");
5
5
  const { spawnSync } = require("child_process");
6
+ const harnessEval = require("./harness-eval.js");
7
+ const { readLocalWorkPacket } = require("./work-packet.js");
6
8
 
7
9
  function readJson(file, fallback = {}) {
8
10
  try {
@@ -84,6 +86,8 @@ function buildPayload(options = {}) {
84
86
  const gitRemote = tracking.git_remote || git(["config", "--get", "remote.origin.url"], cwd);
85
87
  const projectKey = tracking.project_id || repoSlug(gitRemote) || path.basename(cwd);
86
88
  const phase = tracking.phase;
89
+ const latestHarnessEval = harnessEval.latestEval(cwd);
90
+ const workPacket = readLocalWorkPacket(cwd);
87
91
 
88
92
  return {
89
93
  project: tracking.project || path.basename(cwd),
@@ -93,6 +97,11 @@ function buildPayload(options = {}) {
93
97
  ...(uuid(tracking.erp_project_id) ? { erp_project_id: uuid(tracking.erp_project_id) } : {}),
94
98
  ...(uuid(tracking.client_id) ? { client_id: uuid(tracking.client_id) } : {}),
95
99
  ...(uuid(tracking.workspace_id) ? { workspace_id: uuid(tracking.workspace_id) } : {}),
100
+ ...(workPacket ? { work_packet_id: workPacket.id } : {}),
101
+ ...(workPacket && workPacket.assignment_id ? { assignment_id: workPacket.assignment_id } : {}),
102
+ ...(workPacket && workPacket.deadline_date
103
+ ? { assignment_deadline: workPacket.deadline_date }
104
+ : {}),
96
105
  client: tracking.client || "",
97
106
  client_report_id: env.CLIENT_REPORT_ID || "",
98
107
  framework_version: config.version || "",
@@ -114,6 +123,15 @@ function buildPayload(options = {}) {
114
123
  ...(tracking.last_pushed_at ? { last_pushed_at: tracking.last_pushed_at } : {}),
115
124
  session_duration_minutes: sessionDurationMinutes(tracking.session_started_at, submittedAt),
116
125
  lifetime: tracking.lifetime || {},
126
+ ...(latestHarnessEval ? {
127
+ harness_eval: {
128
+ status: latestHarnessEval.status,
129
+ score: latestHarnessEval.score,
130
+ phase: latestHarnessEval.phase,
131
+ generated_at: latestHarnessEval.generated_at,
132
+ artifact: latestHarnessEval.artifacts && latestHarnessEval.artifacts.json,
133
+ },
134
+ } : {}),
117
135
  commits: recentCommitHashes(cwd),
118
136
  notes,
119
137
  submitted_by: env.SUBMITTED_BY || "unknown",
@@ -0,0 +1,35 @@
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: "command-surface.js", label: "command-surface.js (active/deprecated skill manifest)" },
7
+ { file: "host-adapters.js", label: "host-adapters.js (Claude/Codex path renderer)" },
8
+ { file: "state.js", label: "state.js (state machine)" },
9
+ { file: "qualia-ui.js", label: "qualia-ui.js (cosmetics library)" },
10
+ { file: "statusline.js", label: "statusline.js (status bar renderer)" },
11
+ { file: "knowledge.js", label: "knowledge.js (memory-layer loader)" },
12
+ { file: "knowledge-flush.js", label: "knowledge-flush.js (cron-runnable flush)" },
13
+ { file: "state-ledger.js", label: "state-ledger.js (hash-chained state event ledger)" },
14
+ { file: "plan-contract.js", label: "plan-contract.js (plan JSON validator)" },
15
+ { file: "contract-runner.js", label: "contract-runner.js (contract evidence runner)" },
16
+ { file: "agent-runs.js", label: "agent-runs.js (agent telemetry writer)" },
17
+ { file: "slop-detect.mjs", label: "slop-detect.mjs (anti-pattern scanner — runs pre-commit on frontend builds)" },
18
+ { file: "erp-retry.js", label: "erp-retry.js (ERP report retry queue — drained by session-start hook and erp-flush CLI)" },
19
+ { file: "work-packet.js", label: "work-packet.js (ERP mission/work packet pull + local reader)" },
20
+ { file: "report-payload.js", label: "report-payload.js (Framework -> ERP report payload builder)" },
21
+ { file: "project-snapshot.js", label: "project-snapshot.js (ERP/admin project progress snapshot)" },
22
+ { file: "trust-score.js", label: "trust-score.js (harness health scoring)" },
23
+ { file: "harness-eval.js", label: "harness-eval.js (project eval scoring + evidence artifact)" },
24
+ { file: "codex-goal.js", label: "codex-goal.js (Codex /goal objective + token-budget suggester)" },
25
+ { file: "planning-hygiene.js", label: "planning-hygiene.js (.planning organization scanner)" },
26
+ ];
27
+
28
+ function binFiles() {
29
+ return RUNTIME_BIN_SCRIPTS.map((script) => script.file);
30
+ }
31
+
32
+ module.exports = {
33
+ RUNTIME_BIN_SCRIPTS,
34
+ binFiles,
35
+ };
@@ -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));