qualia-framework 6.2.10 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -7
- package/CLAUDE.md +5 -4
- package/README.md +27 -56
- package/bin/cli.js +113 -18
- package/bin/command-surface.js +75 -0
- package/bin/harness-eval.js +296 -0
- package/bin/install.js +43 -31
- package/bin/knowledge-flush.js +21 -10
- package/bin/knowledge.js +1 -1
- package/bin/learning-candidates.js +217 -0
- package/bin/project-snapshot.js +20 -0
- package/bin/prune-deprecated.js +64 -0
- package/bin/report-payload.js +18 -0
- package/bin/runtime-manifest.js +7 -0
- package/bin/security-scan.js +409 -0
- package/bin/state.js +31 -0
- package/bin/status-snapshot.js +363 -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 +15 -38
- package/hooks/fawzi-approval-guard.js +143 -0
- package/hooks/pre-compact.js +232 -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-build/SKILL.md +1 -1
- package/skills/qualia-discuss/SKILL.md +1 -1
- package/skills/qualia-doctor/SKILL.md +1 -1
- package/skills/qualia-feature/SKILL.md +2 -2
- package/skills/qualia-fix/SKILL.md +4 -4
- package/skills/qualia-idk/SKILL.md +133 -54
- package/skills/qualia-learn/SKILL.md +2 -2
- package/skills/qualia-map/SKILL.md +1 -1
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-new/SKILL.md +1 -1
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-plan/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-postmortem/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +1 -1
- package/skills/qualia-research/SKILL.md +1 -1
- package/skills/qualia-review/SKILL.md +1 -1
- package/skills/qualia-road/SKILL.md +15 -20
- package/skills/qualia-secure/SKILL.md +105 -0
- package/skills/qualia-ship/SKILL.md +12 -5
- package/skills/qualia-test/SKILL.md +1 -1
- package/skills/qualia-verify/SKILL.md +10 -2
- package/skills/zoho-workflow/SKILL.md +1 -1
- package/templates/help.html +1 -12
- package/tests/bin.test.sh +147 -75
- package/tests/hooks.test.sh +81 -1
- package/tests/install-smoke.test.sh +14 -4
- package/tests/lib.test.sh +145 -3
- package/tests/published-install-smoke.test.sh +5 -4
- package/tests/refs.test.sh +32 -20
- package/tests/runner.js +30 -29
- package/tests/state.test.sh +106 -7
- 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-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
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Qualia harness eval — deterministic project scoring + evidence artifact.
|
|
3
|
+
//
|
|
4
|
+
// This is the mechanical layer above trust-score and contract-runner. It writes
|
|
5
|
+
// one JSON/Markdown artifact that can be attached to reports, snapshots, and
|
|
6
|
+
// state transitions instead of relying on a prose "looks good" verdict.
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { spawnSync } = require("child_process");
|
|
11
|
+
const pc = require("./plan-contract.js");
|
|
12
|
+
const contractRunner = require("./contract-runner.js");
|
|
13
|
+
const trust = require("./trust-score.js");
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const args = { cwd: process.cwd() };
|
|
17
|
+
for (let i = 2; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a === "--json") args.json = true;
|
|
20
|
+
else if (a === "--run") args.run = true;
|
|
21
|
+
else if (a === "--write") args.write = true;
|
|
22
|
+
else if (a === "--no-write") args.no_write = true;
|
|
23
|
+
else if (a === "--phase") args.phase = Number(argv[++i]);
|
|
24
|
+
else if (a.startsWith("--phase=")) args.phase = Number(a.slice("--phase=".length));
|
|
25
|
+
else if (a === "--cwd") args.cwd = argv[++i];
|
|
26
|
+
else if (a.startsWith("--cwd=")) args.cwd = a.slice("--cwd=".length);
|
|
27
|
+
}
|
|
28
|
+
return args;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readJson(file, fallback = null) {
|
|
32
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return fallback; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readText(file, fallback = "") {
|
|
36
|
+
try { return fs.readFileSync(file, "utf8"); } catch { return fallback; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rel(root, file) {
|
|
40
|
+
return path.relative(root, file).replace(/\\/g, "/");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stateCheck(cwd) {
|
|
44
|
+
const r = spawnSync(process.execPath, [path.join(__dirname, "state.js"), "check"], {
|
|
45
|
+
cwd,
|
|
46
|
+
encoding: "utf8",
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
timeout: 5000,
|
|
49
|
+
});
|
|
50
|
+
const parsed = readJsonFromString(r.stdout);
|
|
51
|
+
return {
|
|
52
|
+
ok: r.status === 0 && parsed && parsed.ok === true,
|
|
53
|
+
status: r.status,
|
|
54
|
+
stdout: r.stdout,
|
|
55
|
+
stderr: r.stderr,
|
|
56
|
+
parsed,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readJsonFromString(text) {
|
|
61
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function latestEval(cwd) {
|
|
65
|
+
const dir = path.join(cwd, ".planning", "evals");
|
|
66
|
+
try {
|
|
67
|
+
const files = fs.readdirSync(dir)
|
|
68
|
+
.filter((f) => /^harness-eval-.*\.json$/.test(f))
|
|
69
|
+
.map((f) => path.join(dir, f))
|
|
70
|
+
.sort();
|
|
71
|
+
if (!files.length) return null;
|
|
72
|
+
return readJson(files[files.length - 1], null);
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addCheck(checks, name, weight, status, evidence, data) {
|
|
79
|
+
checks.push({
|
|
80
|
+
name,
|
|
81
|
+
weight,
|
|
82
|
+
status,
|
|
83
|
+
score: status === "pass" || status === "not_applicable" ? weight : status === "warn" ? Math.floor(weight / 2) : 0,
|
|
84
|
+
evidence,
|
|
85
|
+
...(data && typeof data === "object" ? data : {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function verificationVerdict(text) {
|
|
90
|
+
if (!text) return "";
|
|
91
|
+
const explicit = text.match(/^result:\s*(PASS|FAIL)$/im);
|
|
92
|
+
if (explicit) return explicit[1].toLowerCase();
|
|
93
|
+
const md = text.match(/\bVerdict\b.*\b(PASS|FAIL)\b/i);
|
|
94
|
+
if (md) return md[1].toLowerCase();
|
|
95
|
+
if (/PHASE\s+PASS|ALL\s+CRITERIA\s+PASSED/i.test(text)) return "pass";
|
|
96
|
+
if (/PHASE\s+FAIL|FAILED\s+CRITERIA|GAPS\s+FOUND/i.test(text)) return "fail";
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function writeArtifacts(cwd, result) {
|
|
101
|
+
const dir = path.join(cwd, ".planning", "evals");
|
|
102
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
103
|
+
const stamp = result.generated_at.replace(/[:.]/g, "-");
|
|
104
|
+
const jsonPath = path.join(dir, `harness-eval-${stamp}.json`);
|
|
105
|
+
const mdPath = path.join(dir, `harness-eval-${stamp}.md`);
|
|
106
|
+
fs.writeFileSync(jsonPath, JSON.stringify(result, null, 2) + "\n");
|
|
107
|
+
const lines = [
|
|
108
|
+
`# Harness Eval`,
|
|
109
|
+
``,
|
|
110
|
+
`**Generated:** ${result.generated_at}`,
|
|
111
|
+
`**Phase:** ${result.phase || "n/a"}`,
|
|
112
|
+
`**Status:** ${result.status}`,
|
|
113
|
+
`**Score:** ${result.score}/100`,
|
|
114
|
+
``,
|
|
115
|
+
`## Checks`,
|
|
116
|
+
``,
|
|
117
|
+
`| Check | Status | Score | Evidence |`,
|
|
118
|
+
`|---|---:|---:|---|`,
|
|
119
|
+
...result.checks.map((c) => `| ${c.name} | ${c.status} | ${c.score}/${c.weight} | ${String(c.evidence || "").replace(/\|/g, "\\|")} |`),
|
|
120
|
+
``,
|
|
121
|
+
];
|
|
122
|
+
fs.writeFileSync(mdPath, lines.join("\n"));
|
|
123
|
+
result.artifacts = {
|
|
124
|
+
json: rel(cwd, jsonPath),
|
|
125
|
+
markdown: rel(cwd, mdPath),
|
|
126
|
+
};
|
|
127
|
+
fs.writeFileSync(jsonPath, JSON.stringify(result, null, 2) + "\n");
|
|
128
|
+
return result.artifacts;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildHarnessEval(options = {}) {
|
|
132
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
133
|
+
const planning = path.join(cwd, ".planning");
|
|
134
|
+
const generatedAt = options.now || new Date().toISOString();
|
|
135
|
+
const checks = [];
|
|
136
|
+
|
|
137
|
+
const state = stateCheck(cwd);
|
|
138
|
+
if (!fs.existsSync(planning)) {
|
|
139
|
+
addCheck(checks, "planning_state", 15, "fail", "No .planning directory; run /qualia-new");
|
|
140
|
+
return finalize({ cwd, generatedAt, phase: options.phase || 0, checks, statusOverride: "FAIL" }, options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const tracking = readJson(path.join(planning, "tracking.json"), {});
|
|
144
|
+
const phase = Number(options.phase || state.parsed?.phase || tracking.phase || 0);
|
|
145
|
+
addCheck(
|
|
146
|
+
checks,
|
|
147
|
+
"planning_state",
|
|
148
|
+
15,
|
|
149
|
+
state.ok ? "pass" : "fail",
|
|
150
|
+
state.ok ? "state.js check returned ok" : "state.js check failed",
|
|
151
|
+
{ state: state.parsed || null }
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const ledgerResult = (() => {
|
|
155
|
+
try {
|
|
156
|
+
const stateLedger = require("./state-ledger.js");
|
|
157
|
+
return stateLedger.validate(cwd);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return { ok: false, errors: [e.message] };
|
|
160
|
+
}
|
|
161
|
+
})();
|
|
162
|
+
addCheck(
|
|
163
|
+
checks,
|
|
164
|
+
"state_ledger",
|
|
165
|
+
10,
|
|
166
|
+
ledgerResult.ok ? "pass" : "fail",
|
|
167
|
+
ledgerResult.ok ? `${ledgerResult.count || 0} hash-chained event(s)` : (ledgerResult.errors || []).join("; ")
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const planPath = path.join(planning, `phase-${phase}-plan.md`);
|
|
171
|
+
const contractPath = path.join(planning, `phase-${phase}-contract.json`);
|
|
172
|
+
const hasPlan = fs.existsSync(planPath);
|
|
173
|
+
const hasContract = fs.existsSync(contractPath);
|
|
174
|
+
let loadedContract = null;
|
|
175
|
+
|
|
176
|
+
if (!hasPlan && !hasContract) {
|
|
177
|
+
addCheck(checks, "plan_contract", 20, "not_applicable", `No current phase plan/contract for phase ${phase}`);
|
|
178
|
+
} else if (!hasContract) {
|
|
179
|
+
addCheck(checks, "plan_contract", 20, "fail", `Missing ${rel(cwd, contractPath)}`);
|
|
180
|
+
} else {
|
|
181
|
+
const loaded = pc.readContractFile(contractPath);
|
|
182
|
+
loadedContract = loaded.ok ? loaded.contract : null;
|
|
183
|
+
const errors = loaded.ok ? pc.validate(loaded.contract) : [loaded.message || loaded.error];
|
|
184
|
+
const drift = hasPlan ? pc.checkDrift(contractPath, planPath) : { ok: true, drift: false };
|
|
185
|
+
const ok = loaded.ok && errors.length === 0 && !(drift.ok && drift.drift);
|
|
186
|
+
addCheck(
|
|
187
|
+
checks,
|
|
188
|
+
"plan_contract",
|
|
189
|
+
20,
|
|
190
|
+
ok ? "pass" : "fail",
|
|
191
|
+
ok ? `${rel(cwd, contractPath)} valid and in sync` : [...errors, drift.drift ? "contract drifted from plan" : ""].filter(Boolean).join("; "),
|
|
192
|
+
{ contract: rel(cwd, contractPath) }
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const evidencePath = path.join(planning, "evidence", `phase-${phase}-contract-run.json`);
|
|
197
|
+
let evidence = readJson(evidencePath, null);
|
|
198
|
+
if (loadedContract && options.run) {
|
|
199
|
+
evidence = contractRunner.runContract(loadedContract, { cwd });
|
|
200
|
+
}
|
|
201
|
+
if (!loadedContract) {
|
|
202
|
+
addCheck(checks, "machine_evidence", 20, "not_applicable", "No contract to execute");
|
|
203
|
+
} else if (evidence && evidence.ok === true) {
|
|
204
|
+
addCheck(checks, "machine_evidence", 20, "pass", `${rel(cwd, evidencePath)} passed ${evidence.checked || 0} check(s)`);
|
|
205
|
+
} else {
|
|
206
|
+
addCheck(
|
|
207
|
+
checks,
|
|
208
|
+
"machine_evidence",
|
|
209
|
+
20,
|
|
210
|
+
"fail",
|
|
211
|
+
evidence ? `${evidence.failed || "unknown"} failing machine check(s)` : `Missing ${rel(cwd, evidencePath)}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const verificationPath = path.join(planning, `phase-${phase}-verification.md`);
|
|
216
|
+
const verification = readText(verificationPath, "");
|
|
217
|
+
const ieCount = (verification.match(/INSUFFICIENT EVIDENCE/g) || []).length;
|
|
218
|
+
const verdict = verificationVerdict(verification);
|
|
219
|
+
if (!verification) {
|
|
220
|
+
addCheck(checks, "verification_report", 15, "fail", `Missing ${rel(cwd, verificationPath)}`);
|
|
221
|
+
} else if (ieCount > 0) {
|
|
222
|
+
addCheck(checks, "verification_report", 15, "fail", `${ieCount} insufficient-evidence marker(s)`);
|
|
223
|
+
} else if (verdict === "pass" || verdict === "fail") {
|
|
224
|
+
addCheck(checks, "verification_report", 15, verdict === "pass" ? "pass" : "warn", `${rel(cwd, verificationPath)} verdict=${verdict}`);
|
|
225
|
+
} else {
|
|
226
|
+
addCheck(checks, "verification_report", 15, "warn", `${rel(cwd, verificationPath)} has no machine-readable verdict`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const trustScore = trust.buildTrustScore(cwd);
|
|
230
|
+
addCheck(
|
|
231
|
+
checks,
|
|
232
|
+
"framework_trust_score",
|
|
233
|
+
10,
|
|
234
|
+
trustScore.status === "FAIL" ? "fail" : trustScore.status === "DEGRADED" ? "warn" : "pass",
|
|
235
|
+
`trust-score=${trustScore.score}/100 status=${trustScore.status}`,
|
|
236
|
+
{ trust_score: trustScore.score }
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const hasErpId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tracking.erp_project_id || "");
|
|
240
|
+
const hasProjectKey = !!(tracking.project_id || tracking.project);
|
|
241
|
+
addCheck(
|
|
242
|
+
checks,
|
|
243
|
+
"erp_linkage",
|
|
244
|
+
10,
|
|
245
|
+
hasErpId ? "pass" : hasProjectKey ? "warn" : "fail",
|
|
246
|
+
hasErpId ? "tracking.json has erp_project_id UUID" : hasProjectKey ? "project key exists, ERP UUID missing" : "missing project/project_id for ERP correlation"
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return finalize({ cwd, generatedAt, phase, checks }, options);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function finalize(base, options) {
|
|
253
|
+
const score = Math.max(0, Math.min(100, base.checks.reduce((n, c) => n + (c.score || 0), 0)));
|
|
254
|
+
const failed = base.checks.filter((c) => c.status === "fail").length;
|
|
255
|
+
const warned = base.checks.filter((c) => c.status === "warn").length;
|
|
256
|
+
const status = base.statusOverride || (failed ? "FAIL" : score >= 85 ? "PASS" : warned ? "WARN" : "PASS");
|
|
257
|
+
const result = {
|
|
258
|
+
ok: status !== "FAIL",
|
|
259
|
+
status,
|
|
260
|
+
score,
|
|
261
|
+
phase: base.phase,
|
|
262
|
+
generated_at: base.generatedAt,
|
|
263
|
+
checks: base.checks,
|
|
264
|
+
};
|
|
265
|
+
if (options.write && !options.no_write && fs.existsSync(path.join(base.cwd, ".planning"))) {
|
|
266
|
+
writeArtifacts(base.cwd, result);
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function printHuman(result) {
|
|
272
|
+
console.log(`Harness eval: ${result.score}/100 (${result.status})`);
|
|
273
|
+
for (const c of result.checks) {
|
|
274
|
+
console.log(`${c.name}: ${c.status} (${c.score}/${c.weight}) — ${c.evidence}`);
|
|
275
|
+
}
|
|
276
|
+
if (result.artifacts) {
|
|
277
|
+
console.log(`Artifacts: ${result.artifacts.json}, ${result.artifacts.markdown}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function main(argv) {
|
|
282
|
+
const args = parseArgs(argv);
|
|
283
|
+
const result = buildHarnessEval(args);
|
|
284
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
285
|
+
else printHuman(result);
|
|
286
|
+
return result.ok ? 0 : 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
buildHarnessEval,
|
|
291
|
+
latestEval,
|
|
292
|
+
verificationVerdict,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (require.main === module) process.exit(main(process.argv));
|
|
296
|
+
|
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);
|
|
@@ -666,12 +661,18 @@ async function main() {
|
|
|
666
661
|
} catch {}
|
|
667
662
|
// Purge deprecated hooks from existing installs on upgrade.
|
|
668
663
|
// - block-env-edit.js (v3.2.0): team now has full read/write on .env*.
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
|
|
664
|
+
//
|
|
665
|
+
// Note: pre-compact.js was removed in v6.2.0 (it bot-committed STATE.md +
|
|
666
|
+
// tracking.json, which added no durability over state.js's atomic writes
|
|
667
|
+
// + journal). v6.3.2 reintroduces pre-compact.js but with a fundamentally
|
|
668
|
+
// different mechanism — it writes a markdown SIDECAR to
|
|
669
|
+
// .planning/.compaction-snapshot.md (no git, no state.js writes) so the
|
|
670
|
+
// next session can see what was in flight when compaction wiped context.
|
|
671
|
+
// Safe to ship as a fresh install; the v6.2.0 stripping logic in cli.js
|
|
672
|
+
// doctor remains, but only strips the OLD legacy command (which our v2
|
|
673
|
+
// hook does not match — different content, same filename is fine because
|
|
674
|
+
// install always overwrites).
|
|
675
|
+
const DEPRECATED_HOOKS = ["block-env-edit.js"];
|
|
675
676
|
for (const f of DEPRECATED_HOOKS) {
|
|
676
677
|
const p = path.join(hooksDest, f);
|
|
677
678
|
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
|
|
@@ -868,7 +869,7 @@ Recurring issues and their solutions.
|
|
|
868
869
|
## Install code "Invalid" — user typed letter O instead of digit 0
|
|
869
870
|
**Symptom:** \`npx qualia-framework install\` rejects \`QS-NAME-O1\` (letter O in suffix).
|
|
870
871
|
**Cause:** Team codes use digit zero (\`-01\`, \`-02\`, etc.), not letter O.
|
|
871
|
-
**Fix:** Since v2.8.1, install.js auto-normalizes: \`QS-
|
|
872
|
+
**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
873
|
**Framework version:** Fixed in v2.8.1.
|
|
873
874
|
|
|
874
875
|
---
|
|
@@ -1061,6 +1062,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1061
1062
|
"session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
|
|
1062
1063
|
"pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
|
|
1063
1064
|
"git-guardrails.js", "stop-session-log.js",
|
|
1065
|
+
"fawzi-approval-guard.js",
|
|
1064
1066
|
// v5.0 — insights-driven destructive-op + wrong-account guards
|
|
1065
1067
|
"vercel-account-guard.js", "env-empty-guard.js", "supabase-destructive-guard.js",
|
|
1066
1068
|
]);
|
|
@@ -1086,6 +1088,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1086
1088
|
hooks: [
|
|
1087
1089
|
{ type: "command", command: nodeCmd("auto-update.js"), timeout: 5 },
|
|
1088
1090
|
{ type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "⬢ Checking git safety..." },
|
|
1091
|
+
{ type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
|
|
1089
1092
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
|
|
1090
1093
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
|
|
1091
1094
|
{ type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
|
|
@@ -1098,15 +1101,25 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1098
1101
|
{
|
|
1099
1102
|
matcher: "Edit|Write",
|
|
1100
1103
|
hooks: [
|
|
1104
|
+
{ type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
|
|
1101
1105
|
{ type: "command", if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)", command: nodeCmd("migration-guard.js"), timeout: 10, statusMessage: "⬢ Checking migration safety..." },
|
|
1102
1106
|
],
|
|
1103
1107
|
},
|
|
1104
1108
|
],
|
|
1105
|
-
// v6.2
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1109
|
-
|
|
1109
|
+
// v6.3.2: PreCompact reintroduced with a NEW mechanism. The old hook
|
|
1110
|
+
// (removed in v6.2.0) bot-committed STATE.md + tracking.json. This v2
|
|
1111
|
+
// hook writes a markdown SIDECAR to .planning/.compaction-snapshot.md
|
|
1112
|
+
// (no git, no state.js writes). The qualiaHooks loop below still
|
|
1113
|
+
// iterates over this key, so any leftover legacy pre-compact.js wiring
|
|
1114
|
+
// is replaced by the new one cleanly.
|
|
1115
|
+
PreCompact: [
|
|
1116
|
+
{
|
|
1117
|
+
matcher: ".*",
|
|
1118
|
+
hooks: [
|
|
1119
|
+
{ type: "command", command: nodeCmd("pre-compact.js"), timeout: 10 },
|
|
1120
|
+
],
|
|
1121
|
+
},
|
|
1122
|
+
],
|
|
1110
1123
|
Stop: [
|
|
1111
1124
|
{
|
|
1112
1125
|
matcher: ".*",
|
|
@@ -1171,7 +1184,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1171
1184
|
fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
|
|
1172
1185
|
fs.renameSync(settingsTmp, settingsPath);
|
|
1173
1186
|
|
|
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");
|
|
1187
|
+
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
1188
|
ok("Status line + spinner configured");
|
|
1176
1189
|
ok("Environment variables + permissions");
|
|
1177
1190
|
|
|
@@ -1208,10 +1221,7 @@ function printSummary({ member, target, claudeInstalled }) {
|
|
|
1208
1221
|
const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
|
|
1209
1222
|
const rulesDir = path.join(FRAMEWORK_DIR, "rules");
|
|
1210
1223
|
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;
|
|
1224
|
+
const skillCount = ACTIVE_SKILLS.length;
|
|
1215
1225
|
const agentCount = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md")).length;
|
|
1216
1226
|
const hookCount = fs.readdirSync(hooksSource).length;
|
|
1217
1227
|
const ruleCount = fs.readdirSync(rulesDir).length;
|
|
@@ -1450,9 +1460,9 @@ async function installCodex(member, target) {
|
|
|
1450
1460
|
const skillsDest = path.join(CODEX_DIR, "skills");
|
|
1451
1461
|
const codexPruned = pruneDeprecatedSkills(CODEX_DIR);
|
|
1452
1462
|
for (const name of codexPruned) ok(`pruned deprecated: ${name}`);
|
|
1453
|
-
for (const skill of
|
|
1463
|
+
for (const skill of ACTIVE_SKILLS) {
|
|
1454
1464
|
const src = path.join(skillsSrc, skill);
|
|
1455
|
-
if (!fs.statSync(src).isDirectory()) continue;
|
|
1465
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) continue;
|
|
1456
1466
|
copyTreeTransform(src, path.join(skillsDest, skill), codexText);
|
|
1457
1467
|
}
|
|
1458
1468
|
ok("skills/");
|
|
@@ -1529,6 +1539,7 @@ async function installCodex(member, target) {
|
|
|
1529
1539
|
hooks: [
|
|
1530
1540
|
{ type: "command", command: nodeCmd("auto-update.js"), timeout: 5, statusMessage: "Qualia update check..." },
|
|
1531
1541
|
{ type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "Qualia git safety..." },
|
|
1542
|
+
{ type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
|
|
1532
1543
|
{ type: "command", command: nodeCmd("branch-guard.js"), timeout: 5 },
|
|
1533
1544
|
{ type: "command", command: nodeCmd("pre-push.js"), timeout: 15 },
|
|
1534
1545
|
{ type: "command", command: nodeCmd("pre-deploy-gate.js"), timeout: 180 },
|
|
@@ -1540,6 +1551,7 @@ async function installCodex(member, target) {
|
|
|
1540
1551
|
{
|
|
1541
1552
|
matcher: "Edit|Write",
|
|
1542
1553
|
hooks: [
|
|
1554
|
+
{ type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
|
|
1543
1555
|
{ type: "command", command: nodeCmd("migration-guard.js"), timeout: 10 },
|
|
1544
1556
|
],
|
|
1545
1557
|
},
|
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) {
|