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.
Files changed (58) hide show
  1. package/AGENTS.md +1 -0
  2. package/CLAUDE.md +1 -0
  3. package/README.md +16 -23
  4. package/bin/cli.js +49 -2
  5. package/bin/command-surface.js +71 -0
  6. package/bin/harness-eval.js +296 -0
  7. package/bin/install.js +17 -20
  8. package/bin/knowledge-flush.js +21 -10
  9. package/bin/knowledge.js +1 -1
  10. package/bin/project-snapshot.js +20 -0
  11. package/bin/report-payload.js +18 -0
  12. package/bin/runtime-manifest.js +3 -0
  13. package/bin/state.js +31 -0
  14. package/bin/trust-score.js +3 -11
  15. package/bin/work-packet.js +228 -0
  16. package/docs/erp-contract.md +81 -1
  17. package/docs/onboarding.html +0 -11
  18. package/guide.md +14 -15
  19. package/hooks/fawzi-approval-guard.js +143 -0
  20. package/hooks/pre-deploy-gate.js +74 -1
  21. package/hooks/session-start.js +29 -1
  22. package/package.json +1 -1
  23. package/qualia-design/frontend.md +2 -2
  24. package/rules/codex-goal.md +1 -1
  25. package/rules/one-opinion.md +2 -2
  26. package/rules/speed.md +0 -1
  27. package/skills/qualia/SKILL.md +4 -4
  28. package/skills/qualia-feature/SKILL.md +1 -1
  29. package/skills/qualia-fix/SKILL.md +4 -4
  30. package/skills/qualia-learn/SKILL.md +1 -1
  31. package/skills/qualia-polish/REFERENCE.md +1 -1
  32. package/skills/qualia-polish/SKILL.md +19 -4
  33. package/skills/{qualia-vibe/scripts/extract.mjs → qualia-polish/scripts/vibe-extract.mjs} +4 -4
  34. package/skills/{qualia-vibe/scripts/tokens.mjs → qualia-polish/scripts/vibe-tokens.mjs} +6 -6
  35. package/skills/qualia-road/SKILL.md +15 -20
  36. package/skills/qualia-ship/SKILL.md +12 -5
  37. package/skills/qualia-verify/SKILL.md +9 -1
  38. package/templates/help.html +1 -12
  39. package/tests/bin.test.sh +144 -72
  40. package/tests/hooks.test.sh +81 -1
  41. package/tests/install-smoke.test.sh +13 -3
  42. package/tests/lib.test.sh +145 -3
  43. package/tests/published-install-smoke.test.sh +4 -3
  44. package/tests/refs.test.sh +9 -4
  45. package/tests/runner.js +29 -28
  46. package/tests/state.test.sh +68 -0
  47. package/skills/qualia-debug/SKILL.md +0 -193
  48. package/skills/qualia-flush/SKILL.md +0 -198
  49. package/skills/qualia-help/SKILL.md +0 -74
  50. package/skills/qualia-hook-gen/SKILL.md +0 -206
  51. package/skills/qualia-idk/SKILL.md +0 -166
  52. package/skills/qualia-issues/SKILL.md +0 -151
  53. package/skills/qualia-pause/SKILL.md +0 -68
  54. package/skills/qualia-resume/SKILL.md +0 -52
  55. package/skills/qualia-skill-new/SKILL.md +0 -173
  56. package/skills/qualia-triage/SKILL.md +0 -152
  57. package/skills/qualia-vibe/SKILL.md +0 -229
  58. 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-01": {
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
- "qualia-task", // v5.7.0 — folded into qualia-feature
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 "qs-fawzi-01", "QS-FAWZI-01", "QS-FAWZI-O1" (letter O in the
495
- // numeric suffix), and returns the canonical key if found, else null.
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-01${RESET}`);
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-FAWZI-O1\` → \`QS-FAWZI-01\`. The normalization only touches the segment after the last dash, so \`QS-MOAYAD-03\` (real O in name) is preserved.
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 skillsDir = path.join(FRAMEWORK_DIR, "skills");
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 fs.readdirSync(skillsSrc)) {
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
  },
@@ -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
- // Wraps `/qualia-flush` so it can run from cron (or systemd timer, or any
5
- // CI/scheduled job) without an interactive Claude Code session. Closes the
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
- // /qualia-flush → promotes raw → curated tier
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` all reach the
90
- // skill). We don't parse them ourselves the skill is the source of truth
91
- // for argument semantics. We only use `--days` locally to short-circuit when
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. The skill body invocation matches what the user would
132
- // type at the prompt.
133
- const prompt = `/qualia-flush ${argv.join(" ")}`.trim();
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 /qualia-flush can write to concepts/voice-agent-call-state.md
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) {
@@ -5,6 +5,8 @@ const https = require("https");
5
5
  const os = require("os");
6
6
  const path = require("path");
7
7
  const { spawnSync } = require("child_process");
8
+ const harnessEval = require("./harness-eval.js");
9
+ const { readLocalWorkPacket } = require("./work-packet.js");
8
10
 
9
11
  function readJson(file, fallback = {}) {
10
12
  try {
@@ -90,6 +92,8 @@ function buildSnapshot(options = {}) {
90
92
  const projectId = tracking.project_id || repoSlug(gitRemote) || path.basename(cwd);
91
93
  const currentMilestone = Number(tracking.milestone || 1);
92
94
  const currentPhase = Number(tracking.phase || 0);
95
+ const latestHarnessEval = harnessEval.latestEval(cwd);
96
+ const workPacket = readLocalWorkPacket(cwd);
93
97
  const totalPhases = Number(tracking.total_phases || 0);
94
98
  const lifetime = tracking.lifetime && typeof tracking.lifetime === "object" ? tracking.lifetime : {};
95
99
  const closedMilestones = Array.isArray(tracking.milestones) ? tracking.milestones : [];
@@ -118,6 +122,11 @@ function buildSnapshot(options = {}) {
118
122
  ...(uuid(tracking.erp_project_id) ? { erp_project_id: uuid(tracking.erp_project_id) } : {}),
119
123
  ...(uuid(tracking.client_id) ? { client_id: uuid(tracking.client_id) } : {}),
120
124
  ...(uuid(tracking.workspace_id) ? { workspace_id: uuid(tracking.workspace_id) } : {}),
125
+ ...(workPacket ? { work_packet_id: workPacket.id } : {}),
126
+ ...(workPacket && workPacket.assignment_id ? { assignment_id: workPacket.assignment_id } : {}),
127
+ ...(workPacket && workPacket.deadline_date
128
+ ? { assignment_deadline: workPacket.deadline_date }
129
+ : {}),
121
130
  },
122
131
  project: {
123
132
  name: tracking.project || path.basename(cwd),
@@ -137,6 +146,17 @@ function buildSnapshot(options = {}) {
137
146
  verification: tracking.verification || "pending",
138
147
  gap_cycles: (tracking.gap_cycles || {})[String(currentPhase)] || 0,
139
148
  },
149
+ quality: {
150
+ harness_eval: latestHarnessEval
151
+ ? {
152
+ status: latestHarnessEval.status,
153
+ score: latestHarnessEval.score,
154
+ phase: latestHarnessEval.phase,
155
+ generated_at: latestHarnessEval.generated_at,
156
+ artifact: latestHarnessEval.artifacts && latestHarnessEval.artifacts.json,
157
+ }
158
+ : null,
159
+ },
140
160
  journey: {
141
161
  total_milestones: journeyTotal,
142
162
  milestones: journey.map((milestone) => ({
@@ -3,6 +3,8 @@ const fs = require("fs");
3
3
  const os = require("os");
4
4
  const path = require("path");
5
5
  const { spawnSync } = require("child_process");
6
+ const harnessEval = require("./harness-eval.js");
7
+ const { readLocalWorkPacket } = require("./work-packet.js");
6
8
 
7
9
  function readJson(file, fallback = {}) {
8
10
  try {
@@ -84,6 +86,8 @@ function buildPayload(options = {}) {
84
86
  const gitRemote = tracking.git_remote || git(["config", "--get", "remote.origin.url"], cwd);
85
87
  const projectKey = tracking.project_id || repoSlug(gitRemote) || path.basename(cwd);
86
88
  const phase = tracking.phase;
89
+ const latestHarnessEval = harnessEval.latestEval(cwd);
90
+ const workPacket = readLocalWorkPacket(cwd);
87
91
 
88
92
  return {
89
93
  project: tracking.project || path.basename(cwd),
@@ -93,6 +97,11 @@ function buildPayload(options = {}) {
93
97
  ...(uuid(tracking.erp_project_id) ? { erp_project_id: uuid(tracking.erp_project_id) } : {}),
94
98
  ...(uuid(tracking.client_id) ? { client_id: uuid(tracking.client_id) } : {}),
95
99
  ...(uuid(tracking.workspace_id) ? { workspace_id: uuid(tracking.workspace_id) } : {}),
100
+ ...(workPacket ? { work_packet_id: workPacket.id } : {}),
101
+ ...(workPacket && workPacket.assignment_id ? { assignment_id: workPacket.assignment_id } : {}),
102
+ ...(workPacket && workPacket.deadline_date
103
+ ? { assignment_deadline: workPacket.deadline_date }
104
+ : {}),
96
105
  client: tracking.client || "",
97
106
  client_report_id: env.CLIENT_REPORT_ID || "",
98
107
  framework_version: config.version || "",
@@ -114,6 +123,15 @@ function buildPayload(options = {}) {
114
123
  ...(tracking.last_pushed_at ? { last_pushed_at: tracking.last_pushed_at } : {}),
115
124
  session_duration_minutes: sessionDurationMinutes(tracking.session_started_at, submittedAt),
116
125
  lifetime: tracking.lifetime || {},
126
+ ...(latestHarnessEval ? {
127
+ harness_eval: {
128
+ status: latestHarnessEval.status,
129
+ score: latestHarnessEval.score,
130
+ phase: latestHarnessEval.phase,
131
+ generated_at: latestHarnessEval.generated_at,
132
+ artifact: latestHarnessEval.artifacts && latestHarnessEval.artifacts.json,
133
+ },
134
+ } : {}),
117
135
  commits: recentCommitHashes(cwd),
118
136
  notes,
119
137
  submitted_by: env.SUBMITTED_BY || "unknown",
@@ -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(), {
@@ -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
+ };