qualia-framework 6.2.10 → 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 +16 -23
- package/bin/cli.js +49 -2
- package/bin/command-surface.js +71 -0
- package/bin/harness-eval.js +296 -0
- package/bin/install.js +17 -20
- package/bin/knowledge-flush.js +21 -10
- package/bin/knowledge.js +1 -1
- package/bin/project-snapshot.js +20 -0
- package/bin/report-payload.js +18 -0
- package/bin/runtime-manifest.js +3 -0
- package/bin/state.js +31 -0
- package/bin/trust-score.js +3 -11
- package/bin/work-packet.js +228 -0
- package/docs/erp-contract.md +81 -1
- package/docs/onboarding.html +0 -11
- package/guide.md +14 -15
- 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/frontend.md +2 -2
- package/rules/codex-goal.md +1 -1
- package/rules/one-opinion.md +2 -2
- package/rules/speed.md +0 -1
- package/skills/qualia/SKILL.md +4 -4
- package/skills/qualia-feature/SKILL.md +1 -1
- package/skills/qualia-fix/SKILL.md +4 -4
- package/skills/qualia-learn/SKILL.md +1 -1
- package/skills/qualia-polish/REFERENCE.md +1 -1
- package/skills/qualia-polish/SKILL.md +19 -4
- package/skills/{qualia-vibe/scripts/extract.mjs → qualia-polish/scripts/vibe-extract.mjs} +4 -4
- package/skills/{qualia-vibe/scripts/tokens.mjs → qualia-polish/scripts/vibe-tokens.mjs} +6 -6
- package/skills/qualia-road/SKILL.md +15 -20
- package/skills/qualia-ship/SKILL.md +12 -5
- package/skills/qualia-verify/SKILL.md +9 -1
- package/templates/help.html +1 -12
- package/tests/bin.test.sh +144 -72
- package/tests/hooks.test.sh +81 -1
- package/tests/install-smoke.test.sh +13 -3
- package/tests/lib.test.sh +145 -3
- package/tests/published-install-smoke.test.sh +4 -3
- package/tests/refs.test.sh +9 -4
- package/tests/runner.js +29 -28
- package/tests/state.test.sh +68 -0
- package/skills/qualia-debug/SKILL.md +0 -193
- 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 -229
- package/skills/qualia-zoom/SKILL.md +0 -51
package/bin/install.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require("path");
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const ui = require("./qualia-ui.js");
|
|
7
7
|
const { RUNTIME_BIN_SCRIPTS, binFiles } = require("./runtime-manifest.js");
|
|
8
|
+
const { ACTIVE_SKILLS, RETIRED_SKILLS } = require("./command-surface.js");
|
|
8
9
|
const { renderText } = require("./host-adapters.js");
|
|
9
10
|
|
|
10
11
|
// ─── Colors (kept for legacy log lines; new sections route through qualia-ui) ─
|
|
@@ -49,7 +50,7 @@ const installStart = Date.now();
|
|
|
49
50
|
|
|
50
51
|
// ─── Team codes ──────────────────────────────────────────
|
|
51
52
|
const DEFAULT_TEAM = {
|
|
52
|
-
"QS-FAWZI-
|
|
53
|
+
"QS-FAWZI-11": {
|
|
53
54
|
name: "Fawzi Goussous",
|
|
54
55
|
role: "OWNER",
|
|
55
56
|
description: "Company owner. Full access. Can push to main, approve deploys, edit secrets.",
|
|
@@ -275,11 +276,7 @@ function renderCodexAgentToml(markdown, filenameFallback) {
|
|
|
275
276
|
// Pruned from BOTH ~/.claude/skills/ and ~/.codex/skills/ on every install run
|
|
276
277
|
// so the active surface matches what the framework currently ships.
|
|
277
278
|
const DEPRECATED_SKILLS = [
|
|
278
|
-
|
|
279
|
-
"qualia-quick", // v5.7.0 — folded into qualia-feature
|
|
280
|
-
"qualia-polish-loop", // v5.8.0 — folded into qualia-polish --loop
|
|
281
|
-
"qualia-design", // v4 wave 2 — folded into scope-adaptive qualia-polish
|
|
282
|
-
"qualia-prd", // v5.8.0 — surface cleanup
|
|
279
|
+
...RETIRED_SKILLS,
|
|
283
280
|
];
|
|
284
281
|
|
|
285
282
|
function pruneDeprecatedSkills(baseDir) {
|
|
@@ -491,8 +488,8 @@ function targetLabel(t) {
|
|
|
491
488
|
}
|
|
492
489
|
|
|
493
490
|
// ─── Resolve team code (tolerates case + O/0 typo in suffix) ─
|
|
494
|
-
// Accepts
|
|
495
|
-
//
|
|
491
|
+
// Accepts lowercase codes and common letter-O typos in numeric suffixes,
|
|
492
|
+
// then returns the canonical key if found, else null.
|
|
496
493
|
// Only normalizes O→0 in the segment AFTER the last dash — "QS-MOAYAD-03"
|
|
497
494
|
// contains a real "O" in the name and must not be mangled.
|
|
498
495
|
function resolveTeamCode(input) {
|
|
@@ -521,7 +518,7 @@ async function main() {
|
|
|
521
518
|
if (!member) {
|
|
522
519
|
console.log("");
|
|
523
520
|
log(`${RED}✗${RESET} Invalid code: "${rawCode}". Get your install code from Fawzi.`);
|
|
524
|
-
log(`${DIM} Tip: codes use digit zero, not letter O. Format: QS-NAME
|
|
521
|
+
log(`${DIM} Tip: codes use digit zero, not letter O. Format: QS-NAME-##${RESET}`);
|
|
525
522
|
console.log("");
|
|
526
523
|
process.exit(1);
|
|
527
524
|
}
|
|
@@ -549,9 +546,7 @@ async function main() {
|
|
|
549
546
|
|
|
550
547
|
// ─── Skills ──────────────────────────────────────────
|
|
551
548
|
const skillsDir = path.join(FRAMEWORK_DIR, "skills");
|
|
552
|
-
const skills = fs
|
|
553
|
-
.readdirSync(skillsDir)
|
|
554
|
-
.filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory());
|
|
549
|
+
const skills = ACTIVE_SKILLS.filter((d) => fs.existsSync(path.join(skillsDir, d, "SKILL.md")));
|
|
555
550
|
|
|
556
551
|
printSection("Skills");
|
|
557
552
|
const claudePruned = pruneDeprecatedSkills(CLAUDE_DIR);
|
|
@@ -868,7 +863,7 @@ Recurring issues and their solutions.
|
|
|
868
863
|
## Install code "Invalid" — user typed letter O instead of digit 0
|
|
869
864
|
**Symptom:** \`npx qualia-framework install\` rejects \`QS-NAME-O1\` (letter O in suffix).
|
|
870
865
|
**Cause:** Team codes use digit zero (\`-01\`, \`-02\`, etc.), not letter O.
|
|
871
|
-
**Fix:** Since v2.8.1, install.js auto-normalizes: \`QS-
|
|
866
|
+
**Fix:** Since v2.8.1, install.js auto-normalizes: \`QS-HASAN-O2\` → \`QS-HASAN-02\`. The normalization only touches the segment after the last dash, so \`QS-MOAYAD-03\` (real O in name) is preserved.
|
|
872
867
|
**Framework version:** Fixed in v2.8.1.
|
|
873
868
|
|
|
874
869
|
---
|
|
@@ -1061,6 +1056,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1061
1056
|
"session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
|
|
1062
1057
|
"pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
|
|
1063
1058
|
"git-guardrails.js", "stop-session-log.js",
|
|
1059
|
+
"fawzi-approval-guard.js",
|
|
1064
1060
|
// v5.0 — insights-driven destructive-op + wrong-account guards
|
|
1065
1061
|
"vercel-account-guard.js", "env-empty-guard.js", "supabase-destructive-guard.js",
|
|
1066
1062
|
]);
|
|
@@ -1086,6 +1082,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1086
1082
|
hooks: [
|
|
1087
1083
|
{ type: "command", command: nodeCmd("auto-update.js"), timeout: 5 },
|
|
1088
1084
|
{ type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "⬢ Checking git safety..." },
|
|
1085
|
+
{ type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
|
|
1089
1086
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
|
|
1090
1087
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
|
|
1091
1088
|
{ type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
|
|
@@ -1098,6 +1095,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1098
1095
|
{
|
|
1099
1096
|
matcher: "Edit|Write",
|
|
1100
1097
|
hooks: [
|
|
1098
|
+
{ type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
|
|
1101
1099
|
{ type: "command", if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)", command: nodeCmd("migration-guard.js"), timeout: 10, statusMessage: "⬢ Checking migration safety..." },
|
|
1102
1100
|
],
|
|
1103
1101
|
},
|
|
@@ -1171,7 +1169,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1171
1169
|
fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
|
|
1172
1170
|
fs.renameSync(settingsTmp, settingsPath);
|
|
1173
1171
|
|
|
1174
|
-
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, git-guardrails, stop-session-log, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
|
|
1172
|
+
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, git-guardrails, stop-session-log, fawzi-approval-guard, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
|
|
1175
1173
|
ok("Status line + spinner configured");
|
|
1176
1174
|
ok("Environment variables + permissions");
|
|
1177
1175
|
|
|
@@ -1208,10 +1206,7 @@ function printSummary({ member, target, claudeInstalled }) {
|
|
|
1208
1206
|
const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
|
|
1209
1207
|
const rulesDir = path.join(FRAMEWORK_DIR, "rules");
|
|
1210
1208
|
const tmplDir = path.join(FRAMEWORK_DIR, "templates");
|
|
1211
|
-
const
|
|
1212
|
-
const skillCount = fs
|
|
1213
|
-
.readdirSync(skillsDir)
|
|
1214
|
-
.filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory()).length;
|
|
1209
|
+
const skillCount = ACTIVE_SKILLS.length;
|
|
1215
1210
|
const agentCount = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md")).length;
|
|
1216
1211
|
const hookCount = fs.readdirSync(hooksSource).length;
|
|
1217
1212
|
const ruleCount = fs.readdirSync(rulesDir).length;
|
|
@@ -1450,9 +1445,9 @@ async function installCodex(member, target) {
|
|
|
1450
1445
|
const skillsDest = path.join(CODEX_DIR, "skills");
|
|
1451
1446
|
const codexPruned = pruneDeprecatedSkills(CODEX_DIR);
|
|
1452
1447
|
for (const name of codexPruned) ok(`pruned deprecated: ${name}`);
|
|
1453
|
-
for (const skill of
|
|
1448
|
+
for (const skill of ACTIVE_SKILLS) {
|
|
1454
1449
|
const src = path.join(skillsSrc, skill);
|
|
1455
|
-
if (!fs.statSync(src).isDirectory()) continue;
|
|
1450
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) continue;
|
|
1456
1451
|
copyTreeTransform(src, path.join(skillsDest, skill), codexText);
|
|
1457
1452
|
}
|
|
1458
1453
|
ok("skills/");
|
|
@@ -1529,6 +1524,7 @@ async function installCodex(member, target) {
|
|
|
1529
1524
|
hooks: [
|
|
1530
1525
|
{ type: "command", command: nodeCmd("auto-update.js"), timeout: 5, statusMessage: "Qualia update check..." },
|
|
1531
1526
|
{ type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "Qualia git safety..." },
|
|
1527
|
+
{ type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
|
|
1532
1528
|
{ type: "command", command: nodeCmd("branch-guard.js"), timeout: 5 },
|
|
1533
1529
|
{ type: "command", command: nodeCmd("pre-push.js"), timeout: 15 },
|
|
1534
1530
|
{ type: "command", command: nodeCmd("pre-deploy-gate.js"), timeout: 180 },
|
|
@@ -1540,6 +1536,7 @@ async function installCodex(member, target) {
|
|
|
1540
1536
|
{
|
|
1541
1537
|
matcher: "Edit|Write",
|
|
1542
1538
|
hooks: [
|
|
1539
|
+
{ type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
|
|
1543
1540
|
{ type: "command", command: nodeCmd("migration-guard.js"), timeout: 10 },
|
|
1544
1541
|
],
|
|
1545
1542
|
},
|
package/bin/knowledge-flush.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// ~/.claude/bin/knowledge-flush.js — non-interactive memory-layer flush.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// CI/scheduled job) without
|
|
4
|
+
// Runs the Qualia memory flush prompt from cron (or systemd timer, or any
|
|
5
|
+
// CI/scheduled job) without installing a separate slash command. Closes the
|
|
6
6
|
// memory loop end-to-end:
|
|
7
7
|
//
|
|
8
8
|
// Stop hook (auto, every turn) → <install-home>/knowledge/daily-log/{date}.md
|
|
9
9
|
// THIS SCRIPT (weekly cron) → spawns the installed agent CLI
|
|
10
|
-
//
|
|
10
|
+
// flush prompt → promotes raw → curated tier
|
|
11
11
|
// bin/knowledge.js (every spawn) → reads index.md → reaches the right file
|
|
12
12
|
//
|
|
13
13
|
// Usage:
|
|
@@ -86,10 +86,9 @@ function which(cmd) {
|
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// Pass-through args (so `--days 14`, `--dry-run`, `--project X`
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// the daily-log is genuinely empty.
|
|
89
|
+
// Pass-through args (so `--days 14`, `--dry-run`, `--project X` are visible to
|
|
90
|
+
// the agent prompt). We only parse `--days` locally to short-circuit when the
|
|
91
|
+
// daily-log is genuinely empty.
|
|
93
92
|
const argv = process.argv.slice(2);
|
|
94
93
|
const flagIdx = argv.indexOf("--days");
|
|
95
94
|
const days = flagIdx >= 0 ? parseInt(argv[flagIdx + 1], 10) || 7 : 7;
|
|
@@ -128,9 +127,21 @@ if (!dailyLogHasRecentEntries(days)) {
|
|
|
128
127
|
|
|
129
128
|
// ── Run ──────────────────────────────────────────────────
|
|
130
129
|
// `claude -p "<prompt>"` and `codex exec "<prompt>"` run a single
|
|
131
|
-
// non-interactive turn.
|
|
132
|
-
//
|
|
133
|
-
const
|
|
130
|
+
// non-interactive turn. Keep the prompt self-contained so no separate flush
|
|
131
|
+
// slash command needs to be installed.
|
|
132
|
+
const argsText = argv.join(" ").trim() || "(none)";
|
|
133
|
+
const prompt = [
|
|
134
|
+
"Run the Qualia memory flush.",
|
|
135
|
+
"",
|
|
136
|
+
`Arguments: ${argsText}`,
|
|
137
|
+
`Install home: ${QUALIA_HOME}`,
|
|
138
|
+
"",
|
|
139
|
+
"Read recent markdown files from knowledge/daily-log under the install home.",
|
|
140
|
+
"Promote recurring patterns, decisions, fixes, and client preferences into the curated knowledge tier using bin/knowledge.js append.",
|
|
141
|
+
"Do not promote one-off noise. If --dry-run is present, report planned promotions without writing.",
|
|
142
|
+
"If --project NAME is present, limit promotions to that project.",
|
|
143
|
+
"Finish with one line starting exactly: ⬢ Flushed daily-log",
|
|
144
|
+
].join("\n");
|
|
134
145
|
|
|
135
146
|
const cliArgs = IS_CODEX_INSTALL ? ["exec", prompt] : ["-p", prompt];
|
|
136
147
|
const result = spawnSync(agentBin, cliArgs, {
|
package/bin/knowledge.js
CHANGED
|
@@ -79,7 +79,7 @@ function readSafe(p) {
|
|
|
79
79
|
// 3. Path with "/" → treat as relative to knowledge dir (concepts/foo)
|
|
80
80
|
// 4. Bare name → look in top-level first; if missing, search known
|
|
81
81
|
// subdirectories (concepts/, daily-log/) for an exact match. This
|
|
82
|
-
// means
|
|
82
|
+
// means the memory flush can write to concepts/voice-agent-call-state.md
|
|
83
83
|
// and skills can later run `knowledge.js load voice-agent-call-state`
|
|
84
84
|
// without knowing it lives in a subdirectory.
|
|
85
85
|
function resolveFile(name) {
|
package/bin/project-snapshot.js
CHANGED
|
@@ -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) => ({
|
package/bin/report-payload.js
CHANGED
|
@@ -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",
|
package/bin/runtime-manifest.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const RUNTIME_BIN_SCRIPTS = [
|
|
5
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)" },
|
|
6
7
|
{ file: "host-adapters.js", label: "host-adapters.js (Claude/Codex path renderer)" },
|
|
7
8
|
{ file: "state.js", label: "state.js (state machine)" },
|
|
8
9
|
{ file: "qualia-ui.js", label: "qualia-ui.js (cosmetics library)" },
|
|
@@ -15,9 +16,11 @@ const RUNTIME_BIN_SCRIPTS = [
|
|
|
15
16
|
{ file: "agent-runs.js", label: "agent-runs.js (agent telemetry writer)" },
|
|
16
17
|
{ file: "slop-detect.mjs", label: "slop-detect.mjs (anti-pattern scanner — runs pre-commit on frontend builds)" },
|
|
17
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)" },
|
|
18
20
|
{ file: "report-payload.js", label: "report-payload.js (Framework -> ERP report payload builder)" },
|
|
19
21
|
{ file: "project-snapshot.js", label: "project-snapshot.js (ERP/admin project progress snapshot)" },
|
|
20
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)" },
|
|
21
24
|
{ file: "codex-goal.js", label: "codex-goal.js (Codex /goal objective + token-budget suggester)" },
|
|
22
25
|
{ file: "planning-hygiene.js", label: "planning-hygiene.js (.planning organization scanner)" },
|
|
23
26
|
];
|
package/bin/state.js
CHANGED
|
@@ -468,6 +468,14 @@ function checkPreconditions(current, target, opts) {
|
|
|
468
468
|
return fail("MISSING_FILE", `Verification file not found: ${vFile}`);
|
|
469
469
|
if (!opts.verification || !["pass", "fail"].includes(opts.verification))
|
|
470
470
|
return fail("MISSING_ARG", "--verification must be 'pass' or 'fail'");
|
|
471
|
+
if (opts.verification === "pass") {
|
|
472
|
+
const vContent = fs.readFileSync(vFile, "utf8");
|
|
473
|
+
if (/\bINSUFFICIENT EVIDENCE\b/.test(vContent)) {
|
|
474
|
+
return fail("INSUFFICIENT_EVIDENCE", `${vFile} contains INSUFFICIENT EVIDENCE; PASS is not allowed`);
|
|
475
|
+
}
|
|
476
|
+
const evidenceCheck = checkMachineEvidence(phase);
|
|
477
|
+
if (!evidenceCheck.ok) return evidenceCheck;
|
|
478
|
+
}
|
|
471
479
|
}
|
|
472
480
|
|
|
473
481
|
if (target === "shipped") {
|
|
@@ -501,6 +509,29 @@ function fail(error, message) {
|
|
|
501
509
|
return { ok: false, error, message };
|
|
502
510
|
}
|
|
503
511
|
|
|
512
|
+
function checkMachineEvidence(phase) {
|
|
513
|
+
const contractFile = path.join(PLANNING, `phase-${phase}-contract.json`);
|
|
514
|
+
if (!fs.existsSync(contractFile)) return { ok: true };
|
|
515
|
+
|
|
516
|
+
const evidenceFile = path.join(PLANNING, "evidence", `phase-${phase}-contract-run.json`);
|
|
517
|
+
if (!fs.existsSync(evidenceFile)) {
|
|
518
|
+
return fail(
|
|
519
|
+
"MISSING_EVIDENCE",
|
|
520
|
+
`Contract exists for phase ${phase}, but machine evidence is missing: ${evidenceFile}. Run contract-runner.js or qualia-framework eval --run --write.`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
let evidence;
|
|
524
|
+
try {
|
|
525
|
+
evidence = JSON.parse(fs.readFileSync(evidenceFile, "utf8"));
|
|
526
|
+
} catch (e) {
|
|
527
|
+
return fail("INVALID_EVIDENCE", `Could not parse ${evidenceFile}: ${e.message}`);
|
|
528
|
+
}
|
|
529
|
+
if (!evidence || evidence.ok !== true) {
|
|
530
|
+
return fail("FAILING_EVIDENCE", `${evidenceFile} does not prove the contract passed`);
|
|
531
|
+
}
|
|
532
|
+
return { ok: true };
|
|
533
|
+
}
|
|
534
|
+
|
|
504
535
|
function recordLedgerEvent(meta) {
|
|
505
536
|
try {
|
|
506
537
|
return stateLedger.append(process.cwd(), {
|
package/bin/trust-score.js
CHANGED
|
@@ -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
|
+
};
|