qualia-framework 6.2.10 → 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.
Files changed (78) hide show
  1. package/AGENTS.md +8 -7
  2. package/CLAUDE.md +5 -4
  3. package/README.md +27 -56
  4. package/bin/cli.js +113 -18
  5. package/bin/command-surface.js +75 -0
  6. package/bin/harness-eval.js +296 -0
  7. package/bin/install.js +43 -31
  8. package/bin/knowledge-flush.js +21 -10
  9. package/bin/knowledge.js +1 -1
  10. package/bin/learning-candidates.js +217 -0
  11. package/bin/project-snapshot.js +20 -0
  12. package/bin/prune-deprecated.js +64 -0
  13. package/bin/report-payload.js +18 -0
  14. package/bin/runtime-manifest.js +7 -0
  15. package/bin/security-scan.js +409 -0
  16. package/bin/state.js +31 -0
  17. package/bin/status-snapshot.js +363 -0
  18. package/bin/trust-score.js +3 -11
  19. package/bin/work-packet.js +228 -0
  20. package/docs/erp-contract.md +81 -1
  21. package/docs/onboarding.html +0 -11
  22. package/guide.md +15 -38
  23. package/hooks/fawzi-approval-guard.js +143 -0
  24. package/hooks/pre-compact.js +232 -0
  25. package/hooks/pre-deploy-gate.js +74 -1
  26. package/hooks/session-start.js +29 -1
  27. package/package.json +1 -1
  28. package/qualia-design/frontend.md +2 -2
  29. package/rules/codex-goal.md +1 -1
  30. package/rules/one-opinion.md +2 -2
  31. package/rules/speed.md +0 -1
  32. package/skills/qualia/SKILL.md +4 -4
  33. package/skills/qualia-build/SKILL.md +1 -1
  34. package/skills/qualia-discuss/SKILL.md +1 -1
  35. package/skills/qualia-doctor/SKILL.md +1 -1
  36. package/skills/qualia-feature/SKILL.md +2 -2
  37. package/skills/qualia-fix/SKILL.md +4 -4
  38. package/skills/qualia-idk/SKILL.md +133 -54
  39. package/skills/qualia-learn/SKILL.md +2 -2
  40. package/skills/qualia-map/SKILL.md +1 -1
  41. package/skills/qualia-milestone/SKILL.md +1 -1
  42. package/skills/qualia-new/SKILL.md +1 -1
  43. package/skills/qualia-optimize/SKILL.md +1 -1
  44. package/skills/qualia-plan/SKILL.md +1 -1
  45. package/skills/qualia-polish/REFERENCE.md +1 -1
  46. package/skills/qualia-polish/SKILL.md +19 -4
  47. package/skills/{qualia-vibe/scripts/extract.mjs → qualia-polish/scripts/vibe-extract.mjs} +4 -4
  48. package/skills/{qualia-vibe/scripts/tokens.mjs → qualia-polish/scripts/vibe-tokens.mjs} +6 -6
  49. package/skills/qualia-postmortem/SKILL.md +1 -1
  50. package/skills/qualia-report/SKILL.md +1 -1
  51. package/skills/qualia-research/SKILL.md +1 -1
  52. package/skills/qualia-review/SKILL.md +1 -1
  53. package/skills/qualia-road/SKILL.md +15 -20
  54. package/skills/qualia-secure/SKILL.md +105 -0
  55. package/skills/qualia-ship/SKILL.md +12 -5
  56. package/skills/qualia-test/SKILL.md +1 -1
  57. package/skills/qualia-verify/SKILL.md +10 -2
  58. package/skills/zoho-workflow/SKILL.md +1 -1
  59. package/templates/help.html +1 -12
  60. package/tests/bin.test.sh +147 -75
  61. package/tests/hooks.test.sh +81 -1
  62. package/tests/install-smoke.test.sh +14 -4
  63. package/tests/lib.test.sh +145 -3
  64. package/tests/published-install-smoke.test.sh +5 -4
  65. package/tests/refs.test.sh +32 -20
  66. package/tests/runner.js +30 -29
  67. package/tests/state.test.sh +106 -7
  68. package/skills/qualia-debug/SKILL.md +0 -193
  69. package/skills/qualia-flush/SKILL.md +0 -198
  70. package/skills/qualia-help/SKILL.md +0 -74
  71. package/skills/qualia-hook-gen/SKILL.md +0 -206
  72. package/skills/qualia-issues/SKILL.md +0 -151
  73. package/skills/qualia-pause/SKILL.md +0 -68
  74. package/skills/qualia-resume/SKILL.md +0 -52
  75. package/skills/qualia-skill-new/SKILL.md +0 -173
  76. package/skills/qualia-triage/SKILL.md +0 -152
  77. package/skills/qualia-vibe/SKILL.md +0 -229
  78. package/skills/qualia-zoom/SKILL.md +0 -51
@@ -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
+ }
@@ -7,6 +7,7 @@ const os = require("os");
7
7
  const pc = require("./plan-contract.js");
8
8
  const ledger = require("./state-ledger.js");
9
9
  const { binFiles } = require("./runtime-manifest.js");
10
+ const { ACTIVE_SKILLS } = require("./command-surface.js");
10
11
 
11
12
  const HOMES = [
12
13
  { name: "Claude", dir: path.join(os.homedir(), ".claude") },
@@ -18,7 +19,7 @@ const REQUIRED_BIN = binFiles();
18
19
  const REQUIRED_HOOKS = [
19
20
  "session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
20
21
  "pre-deploy-gate.js", "migration-guard.js", "git-guardrails.js",
21
- "stop-session-log.js", "vercel-account-guard.js", "env-empty-guard.js",
22
+ "stop-session-log.js", "fawzi-approval-guard.js", "vercel-account-guard.js", "env-empty-guard.js",
22
23
  "supabase-destructive-guard.js",
23
24
  ];
24
25
 
@@ -32,13 +33,7 @@ const REQUIRED_DESIGN_FILES = [
32
33
  "graphics.md",
33
34
  ];
34
35
 
35
- const REQUIRED_EMPLOYEE_SKILLS = [
36
- "qualia-doctor",
37
- "qualia-road",
38
- "qualia-resume",
39
- "qualia-pause",
40
- "qualia-report",
41
- ];
36
+ const REQUIRED_EMPLOYEE_SKILLS = ACTIVE_SKILLS;
42
37
 
43
38
  function exists(p) {
44
39
  try { return fs.existsSync(p); } catch { return false; }
@@ -169,9 +164,6 @@ function inspectDesign(homes) {
169
164
  if (!exists(path.join(home.dir, "skills", "qualia-polish", "SKILL.md"))) {
170
165
  issues.push(`${home.name}: missing qualia-polish skill`);
171
166
  }
172
- if (!exists(path.join(home.dir, "skills", "qualia-vibe", "SKILL.md"))) {
173
- issues.push(`${home.name}: missing qualia-vibe skill`);
174
- }
175
167
  if (!exists(path.join(home.dir, "agents", home.name === "Codex" ? "visual-evaluator.toml" : "visual-evaluator.md"))) {
176
168
  issues.push(`${home.name}: missing visual-evaluator agent`);
177
169
  }
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ const fs = require("fs");
3
+ const http = require("http");
4
+ const https = require("https");
5
+ const os = require("os");
6
+ const path = require("path");
7
+
8
+ const WORK_PACKET_FILE = path.join(".planning", "work-packet.json");
9
+
10
+ function readJson(file, fallback = {}) {
11
+ try {
12
+ return JSON.parse(fs.readFileSync(file, "utf8"));
13
+ } catch {
14
+ return fallback;
15
+ }
16
+ }
17
+
18
+ function readText(file, fallback = "") {
19
+ try {
20
+ return fs.readFileSync(file, "utf8");
21
+ } catch {
22
+ return fallback;
23
+ }
24
+ }
25
+
26
+ function qualiaHome(home = os.homedir()) {
27
+ if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
28
+ const parent = path.basename(path.dirname(__dirname));
29
+ if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
30
+ const claude = path.join(home, ".claude");
31
+ if (fs.existsSync(path.join(claude, ".qualia-config.json"))) return claude;
32
+ return claude;
33
+ }
34
+
35
+ function uuid(value) {
36
+ if (typeof value !== "string") return "";
37
+ const trimmed = value.trim();
38
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)
39
+ ? trimmed
40
+ : "";
41
+ }
42
+
43
+ function localWorkPacketPath(cwd = process.cwd()) {
44
+ return path.join(cwd, WORK_PACKET_FILE);
45
+ }
46
+
47
+ function normalizePacket(raw) {
48
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
49
+ const packet = raw.work_packet && typeof raw.work_packet === "object" ? raw.work_packet : raw;
50
+ if (!packet || typeof packet !== "object" || Array.isArray(packet)) return null;
51
+ const id = uuid(packet.id || packet.work_packet_id);
52
+ const projectId = uuid(packet.project_id || packet.erp_project_id);
53
+ if (!id || !projectId) return null;
54
+ return {
55
+ id,
56
+ project_id: projectId,
57
+ assignment_id: uuid(packet.assignment_id) || null,
58
+ employee_id: uuid(packet.employee_id) || null,
59
+ deadline_date: typeof packet.deadline_date === "string" ? packet.deadline_date : null,
60
+ current_milestone: Number.isFinite(packet.current_milestone) ? packet.current_milestone : null,
61
+ current_milestone_name:
62
+ typeof packet.current_milestone_name === "string" ? packet.current_milestone_name : null,
63
+ current_phase: Number.isFinite(packet.current_phase) ? packet.current_phase : null,
64
+ current_phase_name: typeof packet.current_phase_name === "string" ? packet.current_phase_name : null,
65
+ next_command: typeof packet.next_command === "string" ? packet.next_command : "/qualia",
66
+ definition_of_done:
67
+ typeof packet.definition_of_done === "string" ? packet.definition_of_done : null,
68
+ blockers: Array.isArray(packet.blockers) ? packet.blockers.filter((b) => typeof b === "string") : [],
69
+ repo_url: typeof packet.repo_url === "string" ? packet.repo_url : null,
70
+ vercel_url: typeof packet.vercel_url === "string" ? packet.vercel_url : null,
71
+ framework_status:
72
+ typeof packet.framework_status === "string" ? packet.framework_status : null,
73
+ verification: typeof packet.verification === "string" ? packet.verification : null,
74
+ snapshot_generated_at:
75
+ typeof packet.snapshot_generated_at === "string" ? packet.snapshot_generated_at : null,
76
+ last_report_at: typeof packet.last_report_at === "string" ? packet.last_report_at : null,
77
+ status: typeof packet.status === "string" ? packet.status : "active",
78
+ updated_at: typeof packet.updated_at === "string" ? packet.updated_at : null,
79
+ employee: packet.employee && typeof packet.employee === "object" ? packet.employee : null,
80
+ project: packet.project && typeof packet.project === "object" ? packet.project : null,
81
+ mission_url: typeof packet.mission_url === "string" ? packet.mission_url : null,
82
+ };
83
+ }
84
+
85
+ function readLocalWorkPacket(cwd = process.cwd()) {
86
+ return normalizePacket(readJson(localWorkPacketPath(cwd), null));
87
+ }
88
+
89
+ function writeLocalWorkPacket(packet, options = {}) {
90
+ const cwd = options.cwd || process.cwd();
91
+ const file = options.file || localWorkPacketPath(cwd);
92
+ fs.mkdirSync(path.dirname(file), { recursive: true });
93
+ fs.writeFileSync(file, `${JSON.stringify(packet, null, 2)}\n`);
94
+ return file;
95
+ }
96
+
97
+ function erpConfig(options = {}) {
98
+ const home = options.home || os.homedir();
99
+ const installHome = options.qualiaHome || qualiaHome(home);
100
+ const config = readJson(path.join(installHome, ".qualia-config.json"), {});
101
+ const erp = config.erp || {};
102
+ const url = (erp.url || "https://portal.qualiasolutions.net").replace(/\/+$/, "");
103
+ const keyFile = path.join(installHome, erp.api_key_file || ".erp-api-key");
104
+ const key = readText(keyFile, "").trim();
105
+ return {
106
+ enabled: erp.enabled !== false,
107
+ url,
108
+ key,
109
+ };
110
+ }
111
+
112
+ function trackingProjectId(cwd = process.cwd()) {
113
+ const tracking = readJson(path.join(cwd, ".planning", "tracking.json"), {});
114
+ return uuid(tracking.erp_project_id);
115
+ }
116
+
117
+ function fetchJson(url, key, options = {}) {
118
+ const endpoint = new URL(url);
119
+ const transport = endpoint.protocol === "http:" ? http : https;
120
+ return new Promise((resolve, reject) => {
121
+ const req = transport.request(
122
+ endpoint,
123
+ {
124
+ method: "GET",
125
+ headers: {
126
+ Authorization: `Bearer ${key}`,
127
+ Accept: "application/json",
128
+ "User-Agent": "qualia-framework-work-packet",
129
+ },
130
+ timeout: options.timeout || 15000,
131
+ },
132
+ (res) => {
133
+ let body = "";
134
+ res.setEncoding("utf8");
135
+ res.on("data", (chunk) => {
136
+ body += chunk;
137
+ });
138
+ res.on("end", () => {
139
+ let parsed = null;
140
+ try {
141
+ parsed = body ? JSON.parse(body) : null;
142
+ } catch {
143
+ parsed = body;
144
+ }
145
+ if (res.statusCode >= 200 && res.statusCode < 300) {
146
+ resolve(parsed);
147
+ } else {
148
+ const message =
149
+ parsed && typeof parsed === "object" && parsed.message
150
+ ? parsed.message
151
+ : body || `HTTP ${res.statusCode}`;
152
+ reject(new Error(`ERP work packet pull failed (${res.statusCode}): ${message}`));
153
+ }
154
+ });
155
+ }
156
+ );
157
+ req.on("timeout", () => req.destroy(new Error("ERP work packet pull timed out")));
158
+ req.on("error", reject);
159
+ req.end();
160
+ });
161
+ }
162
+
163
+ async function pullWorkPacket(options = {}) {
164
+ const cwd = options.cwd || process.cwd();
165
+ const cfg = options.erp || erpConfig(options);
166
+ if (!cfg.enabled) throw new Error("ERP disabled in Qualia config");
167
+ if (!cfg.key) throw new Error("ERP API key missing in Qualia install");
168
+ const projectId = uuid(options.projectId) || trackingProjectId(cwd);
169
+ if (!projectId) {
170
+ throw new Error("ERP project UUID required. Pass --project <uuid> or set tracking.erp_project_id");
171
+ }
172
+ const endpoint = new URL("/api/v1/work-packets", cfg.url);
173
+ endpoint.searchParams.set("project_id", projectId);
174
+ const response = await fetchJson(endpoint.toString(), cfg.key, options);
175
+ const packet = normalizePacket(response);
176
+ if (!packet) throw new Error("ERP returned an invalid work packet");
177
+ return packet;
178
+ }
179
+
180
+ function parseArgs(argv) {
181
+ const args = {
182
+ action: "show",
183
+ projectId: "",
184
+ write: false,
185
+ pretty: false,
186
+ output: "",
187
+ };
188
+ const rest = [...argv];
189
+ if (rest[0] && !rest[0].startsWith("-")) args.action = rest.shift();
190
+ for (let i = 0; i < rest.length; i += 1) {
191
+ const arg = rest[i];
192
+ if (arg === "--project" || arg === "-p") args.projectId = rest[++i] || "";
193
+ else if (arg === "--write") args.write = true;
194
+ else if (arg === "--pretty") args.pretty = true;
195
+ else if (arg === "--output" || arg === "-o") args.output = rest[++i] || "";
196
+ }
197
+ return args;
198
+ }
199
+
200
+ if (require.main === module) {
201
+ (async () => {
202
+ const args = parseArgs(process.argv.slice(2));
203
+ if (args.action === "pull") {
204
+ const packet = await pullWorkPacket({ projectId: args.projectId });
205
+ const file = writeLocalWorkPacket(packet, { file: args.output || undefined });
206
+ process.stdout.write(args.pretty ? `${JSON.stringify(packet, null, 2)}\n` : `${file}\n`);
207
+ return;
208
+ }
209
+ const packet = readLocalWorkPacket();
210
+ if (!packet) throw new Error("No local .planning/work-packet.json found");
211
+ process.stdout.write(`${JSON.stringify(packet, null, args.pretty ? 2 : 0)}\n`);
212
+ })().catch((error) => {
213
+ console.error(`work-packet failed: ${error.message}`);
214
+ process.exit(1);
215
+ });
216
+ }
217
+
218
+ module.exports = {
219
+ WORK_PACKET_FILE,
220
+ erpConfig,
221
+ localWorkPacketPath,
222
+ normalizePacket,
223
+ pullWorkPacket,
224
+ readLocalWorkPacket,
225
+ trackingProjectId,
226
+ uuid,
227
+ writeLocalWorkPacket,
228
+ };