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.
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/README.md +26 -30
- 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 +160 -16
- package/bin/command-surface.js +71 -0
- package/bin/contract-runner.js +219 -0
- package/bin/harness-eval.js +296 -0
- package/bin/host-adapters.js +66 -0
- package/bin/install.js +116 -172
- package/bin/knowledge-flush.js +21 -10
- package/bin/knowledge.js +1 -1
- package/bin/plan-contract.js +99 -2
- package/bin/planning-hygiene.js +262 -0
- package/bin/project-snapshot.js +20 -0
- package/bin/report-payload.js +18 -0
- package/bin/runtime-manifest.js +35 -0
- package/bin/state-ledger.js +184 -0
- package/bin/state.js +330 -20
- package/bin/trust-score.js +268 -0
- package/bin/work-packet.js +228 -0
- package/docs/erp-contract.md +81 -1
- package/docs/onboarding.html +4 -14
- package/guide.md +16 -16
- package/hooks/fawzi-approval-guard.js +143 -0
- package/hooks/pre-deploy-gate.js +74 -1
- package/hooks/session-start.js +29 -1
- package/package.json +1 -1
- package/qualia-design/design-rubric.md +17 -5
- package/qualia-design/frontend.md +6 -2
- package/qualia-design/graphics.md +47 -0
- package/rules/codex-goal.md +1 -1
- package/rules/command-output.md +35 -0
- package/rules/one-opinion.md +2 -2
- package/rules/speed.md +0 -1
- package/skills/qualia/SKILL.md +12 -12
- package/skills/qualia-build/SKILL.md +20 -14
- package/skills/qualia-discuss/SKILL.md +10 -10
- package/skills/qualia-doctor/SKILL.md +140 -0
- package/skills/qualia-feature/SKILL.md +24 -22
- package/skills/qualia-fix/SKILL.md +216 -0
- package/skills/qualia-handoff/SKILL.md +9 -9
- package/skills/qualia-learn/SKILL.md +11 -11
- 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-plan/SKILL.md +23 -13
- package/skills/qualia-polish/REFERENCE.md +15 -15
- package/skills/qualia-polish/SKILL.md +81 -21
- package/skills/qualia-polish/scripts/loop.mjs +3 -3
- package/skills/qualia-polish/scripts/score.mjs +9 -3
- package/skills/{qualia-vibe/scripts/extract.mjs → qualia-polish/scripts/vibe-extract.mjs} +5 -5
- package/skills/{qualia-vibe/scripts/tokens.mjs → qualia-polish/scripts/vibe-tokens.mjs} +6 -6
- 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-review/SKILL.md +28 -12
- package/skills/qualia-road/SKILL.md +30 -22
- package/skills/qualia-ship/SKILL.md +31 -24
- package/skills/qualia-test/SKILL.md +5 -5
- package/skills/qualia-verify/SKILL.md +45 -23
- package/skills/zoho-workflow/SKILL.md +1 -1
- package/templates/help.html +11 -20
- package/tests/bin.test.sh +178 -76
- package/tests/hooks.test.sh +81 -1
- package/tests/install-smoke.test.sh +35 -5
- package/tests/lib.test.sh +432 -0
- package/tests/published-install-smoke.test.sh +4 -3
- package/tests/refs.test.sh +9 -4
- package/tests/runner.js +32 -28
- package/tests/skills.test.sh +4 -4
- package/tests/state.test.sh +133 -3
- package/skills/qualia-debug/SKILL.md +0 -185
- package/skills/qualia-flush/SKILL.md +0 -198
- package/skills/qualia-help/SKILL.md +0 -74
- package/skills/qualia-hook-gen/SKILL.md +0 -206
- package/skills/qualia-idk/SKILL.md +0 -166
- package/skills/qualia-issues/SKILL.md +0 -151
- package/skills/qualia-pause/SKILL.md +0 -68
- package/skills/qualia-resume/SKILL.md +0 -52
- package/skills/qualia-skill-new/SKILL.md +0 -173
- package/skills/qualia-triage/SKILL.md +0 -152
- package/skills/qualia-vibe/SKILL.md +0 -226
- package/skills/qualia-zoom/SKILL.md +0 -51
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Qualia trust score — compact harness health summary.
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const pc = require("./plan-contract.js");
|
|
8
|
+
const ledger = require("./state-ledger.js");
|
|
9
|
+
const { binFiles } = require("./runtime-manifest.js");
|
|
10
|
+
const { ACTIVE_SKILLS } = require("./command-surface.js");
|
|
11
|
+
|
|
12
|
+
const HOMES = [
|
|
13
|
+
{ name: "Claude", dir: path.join(os.homedir(), ".claude") },
|
|
14
|
+
{ name: "Codex", dir: path.join(os.homedir(), ".codex") },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const REQUIRED_BIN = binFiles();
|
|
18
|
+
|
|
19
|
+
const REQUIRED_HOOKS = [
|
|
20
|
+
"session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
|
|
21
|
+
"pre-deploy-gate.js", "migration-guard.js", "git-guardrails.js",
|
|
22
|
+
"stop-session-log.js", "fawzi-approval-guard.js", "vercel-account-guard.js", "env-empty-guard.js",
|
|
23
|
+
"supabase-destructive-guard.js",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const REQUIRED_DESIGN_FILES = [
|
|
27
|
+
"design-laws.md",
|
|
28
|
+
"design-rubric.md",
|
|
29
|
+
"design-brand.md",
|
|
30
|
+
"design-product.md",
|
|
31
|
+
"design-reference.md",
|
|
32
|
+
"frontend.md",
|
|
33
|
+
"graphics.md",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const REQUIRED_EMPLOYEE_SKILLS = ACTIVE_SKILLS;
|
|
37
|
+
|
|
38
|
+
function exists(p) {
|
|
39
|
+
try { return fs.existsSync(p); } catch { return false; }
|
|
40
|
+
}
|
|
41
|
+
function readJson(p) {
|
|
42
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
43
|
+
}
|
|
44
|
+
function readText(p) {
|
|
45
|
+
try { return fs.readFileSync(p, "utf8"); } catch { return ""; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function installedHomes() {
|
|
49
|
+
return HOMES.filter((h) => exists(path.join(h.dir, ".qualia-config.json")));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inspectInstall(homes) {
|
|
53
|
+
if (homes.length === 0) {
|
|
54
|
+
return { status: "fail", score: 0, issues: ["no Qualia install config found"], targets: [] };
|
|
55
|
+
}
|
|
56
|
+
const issues = [];
|
|
57
|
+
for (const home of homes) {
|
|
58
|
+
for (const f of REQUIRED_BIN) {
|
|
59
|
+
if (!exists(path.join(home.dir, "bin", f))) issues.push(`${home.name}: missing bin/${f}`);
|
|
60
|
+
}
|
|
61
|
+
if (home.name === "Claude") {
|
|
62
|
+
if (!exists(path.join(home.dir, "CLAUDE.md"))) issues.push("Claude: missing CLAUDE.md");
|
|
63
|
+
if (!exists(path.join(home.dir, "settings.json"))) issues.push("Claude: missing settings.json");
|
|
64
|
+
} else {
|
|
65
|
+
if (!exists(path.join(home.dir, "AGENTS.md"))) issues.push("Codex: missing AGENTS.md");
|
|
66
|
+
if (!exists(path.join(home.dir, "hooks.json"))) issues.push("Codex: missing hooks.json");
|
|
67
|
+
if (!exists(path.join(home.dir, "config.toml"))) issues.push("Codex: missing config.toml");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
status: issues.length ? "degraded" : "pass",
|
|
72
|
+
score: issues.length ? Math.max(6, 20 - issues.length * 2) : 20,
|
|
73
|
+
issues,
|
|
74
|
+
targets: homes.map((h) => h.name),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function inspectHooks(homes) {
|
|
79
|
+
if (homes.length === 0) return { status: "fail", score: 0, issues: ["no install targets"] };
|
|
80
|
+
const issues = [];
|
|
81
|
+
for (const home of homes) {
|
|
82
|
+
for (const h of REQUIRED_HOOKS) {
|
|
83
|
+
if (!exists(path.join(home.dir, "hooks", h))) issues.push(`${home.name}: missing hooks/${h}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
status: issues.length ? "degraded" : "pass",
|
|
88
|
+
score: issues.length ? Math.max(4, 12 - issues.length) : 12,
|
|
89
|
+
issues,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function inspectProject(cwd) {
|
|
94
|
+
const planning = path.join(cwd, ".planning");
|
|
95
|
+
if (!exists(planning)) return { status: "not_applicable", score: 12, issues: [], phase: null };
|
|
96
|
+
const tracking = readJson(path.join(planning, "tracking.json"));
|
|
97
|
+
const stateText = readText(path.join(planning, "STATE.md"));
|
|
98
|
+
const issues = [];
|
|
99
|
+
if (!tracking) issues.push("tracking.json missing or invalid");
|
|
100
|
+
if (!stateText) issues.push("STATE.md missing");
|
|
101
|
+
const phaseMatch = stateText.match(/^Phase:\s*(\d+)/m);
|
|
102
|
+
const statusMatch = stateText.match(/^Status:\s*(.+)$/m);
|
|
103
|
+
const phase = phaseMatch ? Number(phaseMatch[1]) : (tracking && Number(tracking.phase)) || 1;
|
|
104
|
+
if (!phaseMatch && stateText) issues.push("STATE.md phase header missing");
|
|
105
|
+
if (!statusMatch && stateText) issues.push("STATE.md status missing");
|
|
106
|
+
return {
|
|
107
|
+
status: issues.length ? "degraded" : "pass",
|
|
108
|
+
score: issues.length ? Math.max(4, 12 - issues.length * 4) : 12,
|
|
109
|
+
issues,
|
|
110
|
+
phase,
|
|
111
|
+
project_status: statusMatch ? statusMatch[1].trim() : tracking?.status || "",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function inspectContracts(cwd, phase) {
|
|
116
|
+
const planning = path.join(cwd, ".planning");
|
|
117
|
+
if (!exists(planning) || !phase) return { status: "not_applicable", score: 18, issues: [] };
|
|
118
|
+
const planPath = path.join(planning, `phase-${phase}-plan.md`);
|
|
119
|
+
const contractPath = path.join(planning, `phase-${phase}-contract.json`);
|
|
120
|
+
if (!exists(planPath)) return { status: "not_applicable", score: 18, issues: [] };
|
|
121
|
+
if (!exists(contractPath)) return { status: "degraded", score: 6, issues: [`phase ${phase}: JSON contract missing`] };
|
|
122
|
+
const loaded = pc.readContractFile(contractPath);
|
|
123
|
+
if (!loaded.ok) return { status: "fail", score: 0, issues: [`phase ${phase}: contract unreadable (${loaded.message || loaded.error})`] };
|
|
124
|
+
const errors = pc.validate(loaded.contract);
|
|
125
|
+
if (errors.length) return { status: "fail", score: 0, issues: [`phase ${phase}: contract invalid (${errors.length} issue(s))`] };
|
|
126
|
+
const drift = pc.checkDrift(contractPath, planPath);
|
|
127
|
+
if (drift.ok && drift.drift) return { status: "fail", score: 0, issues: [`phase ${phase}: contract drifted from plan`] };
|
|
128
|
+
return { status: "pass", score: 18, issues: [], contract: path.relative(cwd, contractPath) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function inspectLedger(cwd) {
|
|
132
|
+
const planning = path.join(cwd, ".planning");
|
|
133
|
+
if (!exists(planning)) return { status: "not_applicable", score: 10, issues: [] };
|
|
134
|
+
const file = ledger.ledgerPath(cwd);
|
|
135
|
+
if (!exists(file)) return { status: "degraded", score: 4, issues: ["state ledger missing"] };
|
|
136
|
+
const result = ledger.validate(cwd);
|
|
137
|
+
if (!result.ok) return { status: "fail", score: 0, issues: result.errors };
|
|
138
|
+
if (result.count === 0) return { status: "degraded", score: 4, issues: ["state ledger empty"] };
|
|
139
|
+
return { status: "pass", score: 10, issues: [], events: result.count };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function inspectMemory(homes) {
|
|
143
|
+
if (homes.length === 0) return { status: "fail", score: 0, issues: ["no install targets"] };
|
|
144
|
+
const issues = [];
|
|
145
|
+
for (const home of homes) {
|
|
146
|
+
if (!exists(path.join(home.dir, "knowledge", "index.md"))) issues.push(`${home.name}: missing knowledge/index.md`);
|
|
147
|
+
if (!exists(path.join(home.dir, "knowledge", "agents.md"))) issues.push(`${home.name}: missing knowledge/agents.md`);
|
|
148
|
+
if (!exists(path.join(home.dir, "knowledge", "daily-log"))) issues.push(`${home.name}: missing knowledge/daily-log`);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
status: issues.length ? "degraded" : "pass",
|
|
152
|
+
score: issues.length ? Math.max(2, 5 - issues.length) : 5,
|
|
153
|
+
issues,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function inspectDesign(homes) {
|
|
158
|
+
if (homes.length === 0) return { status: "fail", score: 0, issues: ["no install targets"] };
|
|
159
|
+
const issues = [];
|
|
160
|
+
for (const home of homes) {
|
|
161
|
+
for (const f of REQUIRED_DESIGN_FILES) {
|
|
162
|
+
if (!exists(path.join(home.dir, "qualia-design", f))) issues.push(`${home.name}: missing qualia-design/${f}`);
|
|
163
|
+
}
|
|
164
|
+
if (!exists(path.join(home.dir, "skills", "qualia-polish", "SKILL.md"))) {
|
|
165
|
+
issues.push(`${home.name}: missing qualia-polish skill`);
|
|
166
|
+
}
|
|
167
|
+
if (!exists(path.join(home.dir, "agents", home.name === "Codex" ? "visual-evaluator.toml" : "visual-evaluator.md"))) {
|
|
168
|
+
issues.push(`${home.name}: missing visual-evaluator agent`);
|
|
169
|
+
}
|
|
170
|
+
if (!exists(path.join(home.dir, "bin", "slop-detect.mjs"))) {
|
|
171
|
+
issues.push(`${home.name}: missing slop-detect.mjs`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
status: issues.length ? "degraded" : "pass",
|
|
176
|
+
score: issues.length ? Math.max(2, 8 - issues.length) : 8,
|
|
177
|
+
issues,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function inspectEmployeeExperience(homes) {
|
|
182
|
+
if (homes.length === 0) return { status: "fail", score: 0, issues: ["no install targets"] };
|
|
183
|
+
const issues = [];
|
|
184
|
+
for (const home of homes) {
|
|
185
|
+
for (const skill of REQUIRED_EMPLOYEE_SKILLS) {
|
|
186
|
+
if (!exists(path.join(home.dir, "skills", skill, "SKILL.md"))) issues.push(`${home.name}: missing ${skill} skill`);
|
|
187
|
+
}
|
|
188
|
+
if (!exists(path.join(home.dir, "qualia-guide.md"))) issues.push(`${home.name}: missing qualia-guide.md`);
|
|
189
|
+
if (!exists(path.join(home.dir, "qualia-templates", "help.html"))) issues.push(`${home.name}: missing help.html`);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
status: issues.length ? "degraded" : "pass",
|
|
193
|
+
score: issues.length ? Math.max(1, 5 - issues.length) : 5,
|
|
194
|
+
issues,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function inspectErp(homes) {
|
|
199
|
+
if (homes.length === 0) return { status: "not_applicable", score: 10, issues: [] };
|
|
200
|
+
const issues = [];
|
|
201
|
+
let enabled = false;
|
|
202
|
+
let queueCount = 0;
|
|
203
|
+
for (const home of homes) {
|
|
204
|
+
const cfg = readJson(path.join(home.dir, ".qualia-config.json")) || {};
|
|
205
|
+
if (cfg.erp && cfg.erp.enabled !== false) {
|
|
206
|
+
enabled = true;
|
|
207
|
+
const keyFile = path.join(home.dir, cfg.erp.api_key_file || ".erp-api-key");
|
|
208
|
+
if (!exists(keyFile)) issues.push(`${home.name}: ERP enabled but API key missing`);
|
|
209
|
+
}
|
|
210
|
+
const queue = readJson(path.join(home.dir, ".erp-retry-queue.json"));
|
|
211
|
+
if (Array.isArray(queue)) queueCount += queue.length;
|
|
212
|
+
}
|
|
213
|
+
if (!enabled) return { status: "not_applicable", score: 10, issues: [], queue_count: queueCount };
|
|
214
|
+
if (queueCount > 0) issues.push(`ERP retry queue has ${queueCount} item(s)`);
|
|
215
|
+
return {
|
|
216
|
+
status: issues.length ? "degraded" : "pass",
|
|
217
|
+
score: issues.length ? Math.max(3, 10 - issues.length * 3) : 10,
|
|
218
|
+
issues,
|
|
219
|
+
queue_count: queueCount,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildTrustScore(cwd = process.cwd()) {
|
|
224
|
+
const homes = installedHomes();
|
|
225
|
+
const install = inspectInstall(homes);
|
|
226
|
+
const hooks = inspectHooks(homes);
|
|
227
|
+
const project = inspectProject(cwd);
|
|
228
|
+
const contracts = inspectContracts(cwd, project.phase);
|
|
229
|
+
const state_ledger = inspectLedger(cwd);
|
|
230
|
+
const memory = inspectMemory(homes);
|
|
231
|
+
const erp = inspectErp(homes);
|
|
232
|
+
const design = inspectDesign(homes);
|
|
233
|
+
const employee_experience = inspectEmployeeExperience(homes);
|
|
234
|
+
const checks = { install, hooks, project, contracts, state_ledger, memory, erp, design, employee_experience };
|
|
235
|
+
const score = Math.max(0, Math.min(100, Object.values(checks).reduce((n, c) => n + (c.score || 0), 0)));
|
|
236
|
+
const failCount = Object.values(checks).filter((c) => c.status === "fail").length;
|
|
237
|
+
const degradedCount = Object.values(checks).filter((c) => c.status === "degraded").length;
|
|
238
|
+
const status = failCount ? "FAIL" : degradedCount ? "DEGRADED" : "PASS";
|
|
239
|
+
return { ok: failCount === 0, status, score, generated_at: new Date().toISOString(), checks };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function printHuman(result) {
|
|
243
|
+
console.log(`Trust score: ${result.score}/100 (${result.status})`);
|
|
244
|
+
for (const [name, check] of Object.entries(result.checks)) {
|
|
245
|
+
const label = `${name[0].toUpperCase()}${name.slice(1)}`;
|
|
246
|
+
console.log(`${label}: ${check.status} (${check.score})`);
|
|
247
|
+
for (const issue of check.issues || []) console.log(` - ${issue}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function main(argv) {
|
|
252
|
+
const result = buildTrustScore(process.cwd());
|
|
253
|
+
if (argv.includes("--json")) console.log(JSON.stringify(result, null, 2));
|
|
254
|
+
else printHuman(result);
|
|
255
|
+
return result.status === "FAIL" ? 1 : 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = {
|
|
259
|
+
buildTrustScore,
|
|
260
|
+
inspectInstall,
|
|
261
|
+
inspectProject,
|
|
262
|
+
inspectContracts,
|
|
263
|
+
inspectLedger,
|
|
264
|
+
inspectDesign,
|
|
265
|
+
inspectEmployeeExperience,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
if (require.main === module) process.exit(main(process.argv));
|
|
@@ -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
|
+
};
|
package/docs/erp-contract.md
CHANGED
|
@@ -31,6 +31,29 @@ The API key is read from `.erp-api-key` in the same install home (file mode 0600
|
|
|
31
31
|
|
|
32
32
|
## Endpoints
|
|
33
33
|
|
|
34
|
+
### POST /api/v1/policy-events
|
|
35
|
+
|
|
36
|
+
Silently record framework policy violations that should not interrupt the
|
|
37
|
+
employee's active session, such as proxy owner-approval claims ("Fawzi said OK").
|
|
38
|
+
|
|
39
|
+
**Request Body:**
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"type": "proxy_owner_approval_claim",
|
|
43
|
+
"actor_code": "QS-HASAN-02",
|
|
44
|
+
"actor_name": "Hasan",
|
|
45
|
+
"actor_role": "EMPLOYEE",
|
|
46
|
+
"count": 3,
|
|
47
|
+
"sample": "Fawzi said ok",
|
|
48
|
+
"project": "client-project",
|
|
49
|
+
"cwd": "/path/to/client-project",
|
|
50
|
+
"recorded_at": "2026-05-23T10:00:00.000Z"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The ERP should increment or store by `(type, actor_code)` so Fawzi can see how
|
|
55
|
+
many times each employee attempted to use proxy approval.
|
|
56
|
+
|
|
34
57
|
### POST /api/v1/reports
|
|
35
58
|
|
|
36
59
|
Upload a session report.
|
|
@@ -59,6 +82,9 @@ below).
|
|
|
59
82
|
"team_id": "qualia-solutions",
|
|
60
83
|
"workspace_id": "2af02a2d-6f1f-4d43-a6cb-6a1e7e09ac43",
|
|
61
84
|
"git_remote": "github.com/QualiasolutionsCY/acme-portal",
|
|
85
|
+
"work_packet_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
86
|
+
"assignment_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
|
87
|
+
"assignment_deadline": "2026-06-01",
|
|
62
88
|
"client": "Client Name",
|
|
63
89
|
"client_report_id": "QS-REPORT-03",
|
|
64
90
|
"milestone": 2,
|
|
@@ -90,6 +116,13 @@ below).
|
|
|
90
116
|
"total_phases": 8,
|
|
91
117
|
"last_closed_milestone": 1
|
|
92
118
|
},
|
|
119
|
+
"harness_eval": {
|
|
120
|
+
"status": "PASS",
|
|
121
|
+
"score": 92,
|
|
122
|
+
"phase": 2,
|
|
123
|
+
"generated_at": "2026-05-23T10:15:00.000Z",
|
|
124
|
+
"artifact": ".planning/evals/harness-eval-2026-05-23T10-15-00-000Z.json"
|
|
125
|
+
},
|
|
93
126
|
"session_started_at": "2026-04-12T13:45:00Z",
|
|
94
127
|
"session_duration_minutes": 45,
|
|
95
128
|
"last_pushed_at": "2026-04-12T14:25:00Z",
|
|
@@ -173,6 +206,37 @@ Authorization: Bearer <api-key>
|
|
|
173
206
|
}
|
|
174
207
|
```
|
|
175
208
|
|
|
209
|
+
## Work Packet Pull
|
|
210
|
+
|
|
211
|
+
`qualia-framework work-packet pull --project <erp_project_id>` reads the active
|
|
212
|
+
ERP mission packet and writes it to:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
.planning/work-packet.json
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The Framework treats this as session context, not as a new source of business
|
|
219
|
+
truth. ERP owns the employee, deadline, assignment, review state, and owner
|
|
220
|
+
approval. Framework owns build, verification, snapshot, and report proof.
|
|
221
|
+
|
|
222
|
+
Local packet shape:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
227
|
+
"project_id": "7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef",
|
|
228
|
+
"assignment_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
|
229
|
+
"deadline_date": "2026-06-01",
|
|
230
|
+
"next_command": "/qualia-verify",
|
|
231
|
+
"mission_url": "https://portal.qualiasolutions.net/projects/7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef/mission"
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Session start reads this file and shows the ERP deadline + next Framework
|
|
236
|
+
command. `/qualia-report` sends `work_packet_id`, `assignment_id`, and
|
|
237
|
+
`assignment_deadline`; project snapshots include the same fields under
|
|
238
|
+
`identifiers`.
|
|
239
|
+
|
|
176
240
|
## Project Snapshot Export
|
|
177
241
|
|
|
178
242
|
`qualia-framework project-snapshot --write` creates a local project-level rollup at:
|
|
@@ -206,7 +270,10 @@ Snapshot shape:
|
|
|
206
270
|
"team_id": "qualia-solutions",
|
|
207
271
|
"git_remote": "github.com/QualiasolutionsCY/acme-portal",
|
|
208
272
|
"erp_project_id": "7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef",
|
|
209
|
-
"client_id": "5f5a8d8e-8c58-4c30-9b76-13a08f0d0d8a"
|
|
273
|
+
"client_id": "5f5a8d8e-8c58-4c30-9b76-13a08f0d0d8a",
|
|
274
|
+
"work_packet_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
275
|
+
"assignment_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
|
276
|
+
"assignment_deadline": "2026-06-01"
|
|
210
277
|
},
|
|
211
278
|
"project": {
|
|
212
279
|
"name": "acme-portal",
|
|
@@ -226,6 +293,15 @@ Snapshot shape:
|
|
|
226
293
|
"verification": "pending",
|
|
227
294
|
"gap_cycles": 1
|
|
228
295
|
},
|
|
296
|
+
"quality": {
|
|
297
|
+
"harness_eval": {
|
|
298
|
+
"status": "PASS",
|
|
299
|
+
"score": 92,
|
|
300
|
+
"phase": 2,
|
|
301
|
+
"generated_at": "2026-05-23T10:15:00.000Z",
|
|
302
|
+
"artifact": ".planning/evals/harness-eval-2026-05-23T10-15-00-000Z.json"
|
|
303
|
+
}
|
|
304
|
+
},
|
|
229
305
|
"journey": {
|
|
230
306
|
"total_milestones": 3,
|
|
231
307
|
"milestones": [
|
|
@@ -287,11 +363,15 @@ Snapshot shape:
|
|
|
287
363
|
| milestone_name | string | recommended (v4+) | Human name of the current milestone — from JOURNEY.md / tracking.json |
|
|
288
364
|
| milestones | array | recommended (v4+) | Array of closed milestone summaries: `{num, name, closed_at, phases_completed, tasks_completed}`. Renders the journey tree on the ERP. |
|
|
289
365
|
| lifetime | object | recommended | Cumulative counters — tasks_completed, phases_completed, milestones_completed, total_phases, last_closed_milestone |
|
|
366
|
+
| harness_eval | object | recommended (v6.3+) | Latest deterministic harness eval summary: status, score, phase, generated_at, and local artifact path. |
|
|
290
367
|
| project_id | string | recommended (v3.6+) | Stable per-project identifier — preferred dedupe key over `project` slug. Survives directory renames. |
|
|
291
368
|
| team_id | string | recommended (v3.6+) | Installation's team identifier. Composite `(team_id, project_id)` is the canonical project key. |
|
|
292
369
|
| erp_project_id | UUID string | optional | ERP's canonical project UUID when known. Strongest direct link for admin dashboards; `/qualia-report` only sends UUID-shaped values. |
|
|
293
370
|
| client_id | UUID string | optional | ERP/client canonical UUID when known. Lets reports attach to the client without guessing from a display name; slug-like values are omitted to avoid ERP validation failures. |
|
|
294
371
|
| workspace_id | UUID string | optional | Workspace/tenant scope for multi-company or multi-team installs; only UUID-shaped values are sent. |
|
|
372
|
+
| work_packet_id | UUID string | optional (v6.3+) | ERP `project_work_packets.id` read from `.planning/work-packet.json`. Links the report to the exact employee mission packet. |
|
|
373
|
+
| assignment_id | UUID string | optional (v6.3+) | ERP `project_assignments.id` read from `.planning/work-packet.json`. Links the report to the assignment/deadline owner. |
|
|
374
|
+
| assignment_deadline | date string | optional (v6.3+) | ERP-owned assignment deadline (`YYYY-MM-DD`) active when the report was submitted. |
|
|
295
375
|
| git_remote | string | optional (v3.6+) | e.g. `github.com/QualiasolutionsCY/foo`. Lets the ERP correlate tracking with the source repo. |
|
|
296
376
|
| session_started_at | string | optional (v3.6+) | ISO 8601 — when the current Claude Code session began. |
|
|
297
377
|
| last_pushed_at | string | optional (v3.6+) | ISO 8601 — distinct from `last_updated` (which fires on local writes too). |
|