qualia-framework 6.9.0 → 6.14.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/CHANGELOG.md +88 -0
- package/agents/verifier.md +1 -1
- package/bin/agent-status.js +251 -0
- package/bin/analyze-gate.js +318 -0
- package/bin/command-surface.js +1 -0
- package/bin/install.js +31 -4
- package/bin/report-payload.js +19 -0
- package/bin/runtime-manifest.js +2 -0
- package/bin/state.js +145 -11
- package/docs/EMPLOYEE-QUICKSTART.md +5 -3
- package/docs/erp-contract.md +33 -1
- package/docs/qualia-manual.html +396 -0
- package/hooks/branch-guard.js +133 -63
- package/hooks/pre-deploy-gate.js +38 -0
- package/hooks/session-start.js +24 -4
- package/hooks/task-write-guard.js +165 -0
- package/hooks/usage-capture.js +108 -0
- package/package.json +2 -1
- package/skills/qualia-build/SKILL.md +30 -1
- package/skills/qualia-report/SKILL.md +3 -0
- package/skills/qualia-ship/SKILL.md +3 -0
- package/skills/qualia-update/SKILL.md +96 -0
- package/skills/qualia-verify/SKILL.md +7 -1
- package/templates/journey.md +1 -1
- package/templates/planning.gitignore +3 -0
- package/tests/agent-status.test.sh +138 -0
- package/tests/analyze-gate.test.sh +170 -0
- package/tests/bin.test.sh +6 -4
- package/tests/hooks.test.sh +250 -17
- package/tests/install-smoke.test.sh +5 -3
- package/tests/lib.test.sh +2 -2
- package/tests/run-all.sh +2 -0
- package/tests/runner.js +3 -2
- package/tests/state.test.sh +95 -0
- package/skills/qualia-discuss/SKILL.md +0 -222
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// analyze-gate.js — cross-artifact consistency + coverage gate (Spec-Kit's
|
|
3
|
+
// most-copied feature). Runs BETWEEN plan and build: diffs the plan contract
|
|
4
|
+
// against the scope's acceptance criteria and the CONTEXT.md glossary, and
|
|
5
|
+
// flags requirements the plan under-covers plus contradictions it introduces.
|
|
6
|
+
//
|
|
7
|
+
// WHY: Qualia validates each artifact in isolation — plan-contract.js proves the
|
|
8
|
+
// contract is internally well-formed, harness-eval scores the built phase — but
|
|
9
|
+
// nothing diffs scope ↔ plan. That's exactly where a junior's idea silently
|
|
10
|
+
// loses intent: the scope asks for X, the plan quietly drops it, and no
|
|
11
|
+
// deterministic check notices. This is that check.
|
|
12
|
+
//
|
|
13
|
+
// DETERMINISTIC: keyword/token coverage, not an LLM. Same inputs → same output.
|
|
14
|
+
// Heuristic by nature (token overlap), so it is a *flag-for-review* gate: the
|
|
15
|
+
// caller decides hard-block (strict profile) vs advisory (standard). Exit 0 =
|
|
16
|
+
// clean, 1 = findings, 2 = invocation error.
|
|
17
|
+
//
|
|
18
|
+
// Inputs (auto-discovered from --phase, or passed explicitly):
|
|
19
|
+
// contract .planning/phase-{N}-contract.json (required)
|
|
20
|
+
// scope .planning/phase-{N}-context.md (optional)
|
|
21
|
+
// context .planning/CONTEXT.md (optional, glossary)
|
|
22
|
+
//
|
|
23
|
+
// Zero npm dependencies. Library + CLI.
|
|
24
|
+
|
|
25
|
+
const fs = require("fs");
|
|
26
|
+
const path = require("path");
|
|
27
|
+
const pc = require("./plan-contract.js");
|
|
28
|
+
|
|
29
|
+
// Tokens shorter than this, or in the stop list, carry no coverage signal.
|
|
30
|
+
const MIN_TOKEN_LEN = 4;
|
|
31
|
+
const STOPWORDS = new Set([
|
|
32
|
+
"the", "and", "for", "with", "that", "this", "from", "into", "your", "you",
|
|
33
|
+
"are", "was", "were", "will", "shall", "should", "must", "when", "then",
|
|
34
|
+
"each", "every", "any", "all", "not", "but", "via", "per", "page", "user",
|
|
35
|
+
"users", "data", "system", "feature", "support", "able", "ensure", "show",
|
|
36
|
+
"display", "have", "has", "can", "its", "their", "they", "them", "which",
|
|
37
|
+
"where", "what", "who", "how", "use", "uses", "used", "using", "new",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// A requirement is "covered" when at least this fraction of its significant
|
|
41
|
+
// tokens appear somewhere in the plan corpus, OR ABS_OVERLAP distinct tokens do.
|
|
42
|
+
const COVERAGE_RATIO = 0.5;
|
|
43
|
+
const ABS_OVERLAP = 3;
|
|
44
|
+
|
|
45
|
+
function tokenize(text) {
|
|
46
|
+
return String(text || "")
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.split(/[^a-z0-9]+/)
|
|
49
|
+
.filter((t) => t.length >= MIN_TOKEN_LEN && !STOPWORDS.has(t));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function significantTokens(text) {
|
|
53
|
+
return new Set(tokenize(text));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// covered ⇔ ≥COVERAGE_RATIO of the requirement's tokens are in the corpus, or
|
|
57
|
+
// ≥ABS_OVERLAP of them are. Requirements with no significant tokens are treated
|
|
58
|
+
// as covered (nothing to match — e.g. a one-word "Works.").
|
|
59
|
+
function coverage(reqText, corpusTokenSet) {
|
|
60
|
+
const reqTokens = [...significantTokens(reqText)];
|
|
61
|
+
if (reqTokens.length === 0) return { covered: true, overlap: 0, total: 0, ratio: 1 };
|
|
62
|
+
const overlap = reqTokens.filter((t) => corpusTokenSet.has(t)).length;
|
|
63
|
+
const ratio = overlap / reqTokens.length;
|
|
64
|
+
return {
|
|
65
|
+
covered: ratio >= COVERAGE_RATIO || overlap >= ABS_OVERLAP,
|
|
66
|
+
overlap,
|
|
67
|
+
total: reqTokens.length,
|
|
68
|
+
ratio,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Scope parsing ───────────────────────────────────────────────────────
|
|
73
|
+
// Pull the bullets under "## Acceptance Criteria" (testable observables) from a
|
|
74
|
+
// phase-context / scope markdown file. Bullets look like "- AC1 — {criterion}"
|
|
75
|
+
// or plain "- {criterion}". Stops at the next "## " heading.
|
|
76
|
+
function parseScopeAcceptanceCriteria(md) {
|
|
77
|
+
if (!md) return [];
|
|
78
|
+
const lines = md.split(/\r?\n/);
|
|
79
|
+
const out = [];
|
|
80
|
+
let inSection = false;
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
if (/^##\s+/.test(line)) {
|
|
83
|
+
inSection = /^##\s+Acceptance Criteria/i.test(line);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!inSection) continue;
|
|
87
|
+
const m = line.match(/^\s*[-*]\s+(.*\S)\s*$/);
|
|
88
|
+
if (!m) continue;
|
|
89
|
+
// Strip a leading "AC12 —"/"AC12:"/"AC12 -" label so it isn't matched as a token.
|
|
90
|
+
const text = m[1].replace(/^AC\d+\s*[—:\-]\s*/i, "").trim();
|
|
91
|
+
if (text) out.push(text);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Pull "Avoid:" aliases from CONTEXT.md glossary lines. The glossary bans
|
|
97
|
+
// alternative terms ("**Avoid:** AuthUser vs Customer"); using a banned alias in
|
|
98
|
+
// the plan is a genuine, deterministic cross-artifact contradiction.
|
|
99
|
+
function parseGlossaryBannedTerms(md) {
|
|
100
|
+
if (!md) return [];
|
|
101
|
+
const banned = new Set();
|
|
102
|
+
const lines = md.split(/\r?\n/);
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
const m = line.match(/\*{0,2}Avoid:?\*{0,2}\s*(.+)$/i);
|
|
105
|
+
if (!m) continue;
|
|
106
|
+
// "AuthUser vs Customer (unless disambiguated)" → ["AuthUser", "Customer"]
|
|
107
|
+
const body = m[1].replace(/\([^)]*\)/g, " ");
|
|
108
|
+
for (const part of body.split(/\bvs\b|,|\/|;/i)) {
|
|
109
|
+
const term = part.trim().replace(/[.*_`]/g, "");
|
|
110
|
+
// Only meaningful identifier-like aliases (skip prose).
|
|
111
|
+
if (/^[A-Za-z][A-Za-z0-9 _-]{2,}$/.test(term) && !STOPWORDS.has(term.toLowerCase())) {
|
|
112
|
+
banned.add(term.trim());
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [...banned];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Core analysis ─────────────────────────────────────────────────────────
|
|
120
|
+
function analyze({ contract, scopeMd, contextMd }) {
|
|
121
|
+
const findings = [];
|
|
122
|
+
|
|
123
|
+
const tasks = (contract && contract.tasks) || [];
|
|
124
|
+
const successCriteria = (contract && contract.success_criteria) || [];
|
|
125
|
+
|
|
126
|
+
// The plan corpus = everything the plan promises to do.
|
|
127
|
+
const planText = [
|
|
128
|
+
contract.goal,
|
|
129
|
+
contract.why,
|
|
130
|
+
...successCriteria,
|
|
131
|
+
...tasks.map((t) => t.title),
|
|
132
|
+
...tasks.map((t) => t.action),
|
|
133
|
+
...tasks.flatMap((t) => t.acceptance_criteria || []),
|
|
134
|
+
].filter(Boolean).join("\n");
|
|
135
|
+
const planTokens = significantTokens(planText);
|
|
136
|
+
|
|
137
|
+
// Per-task corpora for success-criterion → task mapping.
|
|
138
|
+
const taskCorpora = tasks.map((t) => ({
|
|
139
|
+
id: t.id,
|
|
140
|
+
tokens: significantTokens([t.title, t.action, ...(t.acceptance_criteria || [])].filter(Boolean).join("\n")),
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
// 1. Scope acceptance criteria under-covered by the plan (the core check).
|
|
144
|
+
const scopeACs = parseScopeAcceptanceCriteria(scopeMd);
|
|
145
|
+
for (const ac of scopeACs) {
|
|
146
|
+
const cov = coverage(ac, planTokens);
|
|
147
|
+
if (!cov.covered) {
|
|
148
|
+
findings.push({
|
|
149
|
+
type: "uncovered-scope-ac",
|
|
150
|
+
severity: "HIGH",
|
|
151
|
+
message: `Scope acceptance criterion under-covered by the plan: "${ac}"`,
|
|
152
|
+
detail: `${cov.overlap}/${cov.total} key terms found in the contract (need ≥${Math.ceil(cov.total * COVERAGE_RATIO)} or ${ABS_OVERLAP}). The plan may have dropped this requirement.`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 2. Contract success criteria with no task home (orphan requirement).
|
|
158
|
+
for (const sc of successCriteria) {
|
|
159
|
+
const best = taskCorpora
|
|
160
|
+
.map((tc) => ({ id: tc.id, cov: coverage(sc, tc.tokens) }))
|
|
161
|
+
.sort((a, b) => b.cov.ratio - a.cov.ratio)[0];
|
|
162
|
+
if (!best || !best.cov.covered) {
|
|
163
|
+
findings.push({
|
|
164
|
+
type: "uncovered-success-criterion",
|
|
165
|
+
severity: "MEDIUM",
|
|
166
|
+
message: `Success criterion maps to no task: "${sc}"`,
|
|
167
|
+
detail: "No task's title/action/acceptance-criteria covers this success criterion. Either a task is missing or the criterion is unowned.",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 3. Glossary contradictions — a banned alias used in the plan text.
|
|
173
|
+
const banned = parseGlossaryBannedTerms(contextMd);
|
|
174
|
+
for (const term of banned) {
|
|
175
|
+
const re = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
176
|
+
if (re.test(planText)) {
|
|
177
|
+
findings.push({
|
|
178
|
+
type: "glossary-violation",
|
|
179
|
+
severity: "MEDIUM",
|
|
180
|
+
message: `Plan uses a term CONTEXT.md bans: "${term}"`,
|
|
181
|
+
detail: "The glossary lists this under Avoid:. Use the canonical term so spec, plan, and code agree.",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 4. Scope-reduction phrases anywhere in the plan's free text (reuse the
|
|
187
|
+
// plan-contract scanner — surfaced here as part of the cross-artifact gate).
|
|
188
|
+
for (const t of tasks) {
|
|
189
|
+
const fields = [["action", t.action], ...(t.acceptance_criteria || []).map((a, i) => [`acceptance_criteria[${i}]`, a])];
|
|
190
|
+
for (const [field, val] of fields) {
|
|
191
|
+
const hits = pc.findScopeReductionPhrases(val);
|
|
192
|
+
if (hits.length) {
|
|
193
|
+
findings.push({
|
|
194
|
+
type: "scope-reduction",
|
|
195
|
+
severity: "HIGH",
|
|
196
|
+
message: `Task ${t.id} ${field} contains scope-reduction language: ${hits.join(", ")}`,
|
|
197
|
+
detail: "The plan waters down the spec. Deliver the actual requirement or split it via the locked-decision channel.",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const bySeverity = (s) => findings.filter((f) => f.severity === s).length;
|
|
204
|
+
return {
|
|
205
|
+
ok: findings.length === 0,
|
|
206
|
+
phase: contract.phase,
|
|
207
|
+
counts: {
|
|
208
|
+
total: findings.length,
|
|
209
|
+
high: bySeverity("HIGH"),
|
|
210
|
+
medium: bySeverity("MEDIUM"),
|
|
211
|
+
scope_acs_checked: scopeACs.length,
|
|
212
|
+
success_criteria_checked: successCriteria.length,
|
|
213
|
+
glossary_terms_checked: banned.length,
|
|
214
|
+
},
|
|
215
|
+
scope_present: scopeMd != null,
|
|
216
|
+
context_present: contextMd != null,
|
|
217
|
+
findings,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── CLI ───────────────────────────────────────────────────────────────────
|
|
222
|
+
function readMaybe(p) {
|
|
223
|
+
try { return fs.readFileSync(p, "utf8"); } catch { return null; }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseArgs(argv) {
|
|
227
|
+
const args = { _: [] };
|
|
228
|
+
for (let i = 2; i < argv.length; i++) {
|
|
229
|
+
const a = argv[i];
|
|
230
|
+
if (a === "--json") args.json = true;
|
|
231
|
+
else if (a === "--cwd") args.cwd = argv[++i];
|
|
232
|
+
else if (a.startsWith("--cwd=")) args.cwd = a.slice(6);
|
|
233
|
+
else if (a === "--contract") args.contract = argv[++i];
|
|
234
|
+
else if (a.startsWith("--contract=")) args.contract = a.slice(11);
|
|
235
|
+
else if (a === "--scope") args.scope = argv[++i];
|
|
236
|
+
else if (a.startsWith("--scope=")) args.scope = a.slice(8);
|
|
237
|
+
else if (a === "--context") args.context = argv[++i];
|
|
238
|
+
else if (a.startsWith("--context=")) args.context = a.slice(10);
|
|
239
|
+
else if (a === "--phase") args.phase = argv[++i];
|
|
240
|
+
else if (a.startsWith("--phase=")) args.phase = a.slice(8);
|
|
241
|
+
else args._.push(a);
|
|
242
|
+
}
|
|
243
|
+
if (args.phase == null && args._.length && /^\d+$/.test(args._[0])) args.phase = args._[0];
|
|
244
|
+
return args;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function usage() {
|
|
248
|
+
console.error([
|
|
249
|
+
"Usage:",
|
|
250
|
+
" analyze-gate.js <phase> [--cwd DIR] [--json]",
|
|
251
|
+
" analyze-gate.js --contract <c.json> [--scope <s.md>] [--context <ctx.md>] [--json]",
|
|
252
|
+
"",
|
|
253
|
+
"Cross-artifact coverage gate between plan and build.",
|
|
254
|
+
"Exit 0 = clean, 1 = findings, 2 = invocation error.",
|
|
255
|
+
].join("\n"));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function main(argv) {
|
|
259
|
+
const args = parseArgs(argv);
|
|
260
|
+
const root = path.resolve(args.cwd || process.cwd());
|
|
261
|
+
const planning = path.join(root, ".planning");
|
|
262
|
+
|
|
263
|
+
let contractPath = args.contract;
|
|
264
|
+
let scopePath = args.scope;
|
|
265
|
+
let contextPath = args.context;
|
|
266
|
+
if (!contractPath && args.phase != null) {
|
|
267
|
+
contractPath = path.join(planning, `phase-${args.phase}-contract.json`);
|
|
268
|
+
if (scopePath == null) scopePath = path.join(planning, `phase-${args.phase}-context.md`);
|
|
269
|
+
if (contextPath == null) contextPath = path.join(planning, "CONTEXT.md");
|
|
270
|
+
}
|
|
271
|
+
if (!contractPath) { usage(); return 2; }
|
|
272
|
+
|
|
273
|
+
const loaded = pc.readContractFile(contractPath);
|
|
274
|
+
if (!loaded.ok) {
|
|
275
|
+
if (args.json) console.log(JSON.stringify({ ok: false, ...loaded }, null, 2));
|
|
276
|
+
else console.error(`${loaded.error}: ${loaded.message}`);
|
|
277
|
+
return 2;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result = analyze({
|
|
281
|
+
contract: loaded.contract,
|
|
282
|
+
scopeMd: scopePath ? readMaybe(scopePath) : null,
|
|
283
|
+
contextMd: contextPath ? readMaybe(contextPath) : null,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (args.json) {
|
|
287
|
+
console.log(JSON.stringify(result, null, 2));
|
|
288
|
+
return result.ok ? 0 : 1;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const c = result.counts;
|
|
292
|
+
if (result.ok) {
|
|
293
|
+
console.log(`ANALYZE PASS phase ${result.phase}: scope↔plan consistent ` +
|
|
294
|
+
`(${c.scope_acs_checked} scope AC, ${c.success_criteria_checked} success criteria, ${c.glossary_terms_checked} glossary terms checked)`);
|
|
295
|
+
if (!result.scope_present) console.log(" note: no scope file (phase-N-context.md) — scope-coverage check skipped");
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
console.error(`ANALYZE FINDINGS phase ${result.phase}: ${c.total} (${c.high} HIGH, ${c.medium} MEDIUM)`);
|
|
300
|
+
if (!result.scope_present) console.error(" note: no scope file — scope-coverage check was skipped");
|
|
301
|
+
for (const f of result.findings) {
|
|
302
|
+
console.error(` [${f.severity}] ${f.message}`);
|
|
303
|
+
console.error(` ${f.detail}`);
|
|
304
|
+
}
|
|
305
|
+
return 1;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
analyze,
|
|
310
|
+
coverage,
|
|
311
|
+
tokenize,
|
|
312
|
+
parseScopeAcceptanceCriteria,
|
|
313
|
+
parseGlossaryBannedTerms,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
if (require.main === module) {
|
|
317
|
+
process.exit(main(process.argv));
|
|
318
|
+
}
|
package/bin/command-surface.js
CHANGED
package/bin/install.js
CHANGED
|
@@ -555,8 +555,9 @@ function askCode() {
|
|
|
555
555
|
// ─── Employee mode (no team code) ────────────────────────
|
|
556
556
|
// A new employee may not have a QS-NAME-## code yet. Typing "EMPLOYEE" at the
|
|
557
557
|
// install-code prompt installs the full framework at the least-privilege role:
|
|
558
|
-
// feature branches
|
|
559
|
-
//
|
|
558
|
+
// feature branches by default; main pushes are allowed but recorded by
|
|
559
|
+
// branch-guard (counted locally + reported to the ERP, which trusts the role bit
|
|
560
|
+
// in the 0o600 config). ERP reporting stays OFF until a real team
|
|
560
561
|
// code is set, so /qualia-report degrades to a local-only report.
|
|
561
562
|
//
|
|
562
563
|
// Defaulting to EMPLOYEE (not OWNER) on a missing/keyword code is the secure
|
|
@@ -1140,6 +1141,10 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1140
1141
|
enabled: !employeeMode,
|
|
1141
1142
|
url: "https://portal.qualiasolutions.net",
|
|
1142
1143
|
api_key_file: ".erp-api-key",
|
|
1144
|
+
// Performance-audit telemetry. command_usage (counts) is always on.
|
|
1145
|
+
// capturePrompts records real prompt text for the prompt-quality judge —
|
|
1146
|
+
// written explicitly (not implied) so engineers can see and flip it.
|
|
1147
|
+
capturePrompts: true,
|
|
1143
1148
|
},
|
|
1144
1149
|
};
|
|
1145
1150
|
// mode 0o600: this file holds the role bit (OWNER vs EMPLOYEE) which the
|
|
@@ -1272,9 +1277,11 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1272
1277
|
"session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
|
|
1273
1278
|
"pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
|
|
1274
1279
|
"git-guardrails.js", "stop-session-log.js",
|
|
1275
|
-
"fawzi-approval-guard.js",
|
|
1280
|
+
"fawzi-approval-guard.js", "task-write-guard.js",
|
|
1276
1281
|
// v5.0 — insights-driven destructive-op + wrong-account guards
|
|
1277
1282
|
"vercel-account-guard.js", "env-empty-guard.js", "supabase-destructive-guard.js",
|
|
1283
|
+
// performance-audit telemetry capture (UserPromptSubmit)
|
|
1284
|
+
"usage-capture.js",
|
|
1278
1285
|
]);
|
|
1279
1286
|
const isQualiaHookCmd = (cmd) => {
|
|
1280
1287
|
if (typeof cmd !== "string") return false;
|
|
@@ -1299,7 +1306,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1299
1306
|
{ type: "command", command: nodeCmd("auto-update.js"), timeout: 5 },
|
|
1300
1307
|
{ type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "⬢ Checking git safety..." },
|
|
1301
1308
|
{ type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
|
|
1302
|
-
{ type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢
|
|
1309
|
+
{ type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Recording branch activity..." },
|
|
1303
1310
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
|
|
1304
1311
|
{ type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 600, statusMessage: "⬢ Running quality gates..." },
|
|
1305
1312
|
// v5.0 hooks — insights-driven friction prevention
|
|
@@ -1312,6 +1319,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1312
1319
|
matcher: "Edit|Write",
|
|
1313
1320
|
hooks: [
|
|
1314
1321
|
{ type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
|
|
1322
|
+
{ type: "command", command: nodeCmd("task-write-guard.js"), timeout: 5, statusMessage: "⬢ Checking plan-contract file scope..." },
|
|
1315
1323
|
{ type: "command", if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)", command: nodeCmd("migration-guard.js"), timeout: 10, statusMessage: "⬢ Checking migration safety..." },
|
|
1316
1324
|
],
|
|
1317
1325
|
},
|
|
@@ -1338,6 +1346,16 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
1338
1346
|
],
|
|
1339
1347
|
},
|
|
1340
1348
|
],
|
|
1349
|
+
// Performance-audit telemetry — record qualia-command usage + (opt-in)
|
|
1350
|
+
// prompt samples per session for the ERP clock-out payload.
|
|
1351
|
+
UserPromptSubmit: [
|
|
1352
|
+
{
|
|
1353
|
+
matcher: ".*",
|
|
1354
|
+
hooks: [
|
|
1355
|
+
{ type: "command", command: nodeCmd("usage-capture.js"), timeout: 5 },
|
|
1356
|
+
],
|
|
1357
|
+
},
|
|
1358
|
+
],
|
|
1341
1359
|
};
|
|
1342
1360
|
|
|
1343
1361
|
// Merge user hooks: strip Qualia-owned commands, preserve everything else.
|
|
@@ -1519,6 +1537,13 @@ function printSummary({ member, target, claudeInstalled }) {
|
|
|
1519
1537
|
console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(shift submission)${RESET}`);
|
|
1520
1538
|
console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
|
|
1521
1539
|
console.log("");
|
|
1540
|
+
console.log(
|
|
1541
|
+
` ${DIM}Telemetry${RESET} ${DIM}your /qualia command usage + prompt samples feed the team${RESET}`
|
|
1542
|
+
);
|
|
1543
|
+
console.log(
|
|
1544
|
+
` ${DIM} performance audit. Opt out:${RESET} ${TEAL}erp.capturePrompts=false${RESET} ${DIM}in ~/.claude/.qualia-config.json${RESET}`
|
|
1545
|
+
);
|
|
1546
|
+
console.log("");
|
|
1522
1547
|
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
1523
1548
|
console.log(` ${TEAL}${BOLD}Welcome to the future with Qualia.${RESET}`);
|
|
1524
1549
|
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
@@ -1626,6 +1651,8 @@ async function installCodex(member, target, employeeMode = false) {
|
|
|
1626
1651
|
enabled: !employeeMode,
|
|
1627
1652
|
url: "https://portal.qualiasolutions.net",
|
|
1628
1653
|
api_key_file: ".erp-api-key",
|
|
1654
|
+
// See ~/.claude config above — explicit so engineers can see/flip it.
|
|
1655
|
+
capturePrompts: true,
|
|
1629
1656
|
},
|
|
1630
1657
|
};
|
|
1631
1658
|
atomicWrite(path.join(CODEX_DIR, ".qualia-config.json"), JSON.stringify(codexConfig, null, 2) + "\n", 0o600);
|
package/bin/report-payload.js
CHANGED
|
@@ -89,6 +89,16 @@ function buildPayload(options = {}) {
|
|
|
89
89
|
const latestHarnessEval = harnessEval.latestEval(cwd);
|
|
90
90
|
const workPacket = readLocalWorkPacket(cwd);
|
|
91
91
|
|
|
92
|
+
// Performance-audit telemetry captured during the session by the
|
|
93
|
+
// usage-capture UserPromptSubmit hook. Cleared by /qualia-report after upload.
|
|
94
|
+
const usagePath = options.usagePath || path.join(cwd, ".planning", ".session-usage.json");
|
|
95
|
+
const usage = readJson(usagePath, {});
|
|
96
|
+
const commandUsage =
|
|
97
|
+
usage && typeof usage.command_usage === "object" && usage.command_usage
|
|
98
|
+
? usage.command_usage
|
|
99
|
+
: {};
|
|
100
|
+
const promptSamples = Array.isArray(usage.prompt_samples) ? usage.prompt_samples : [];
|
|
101
|
+
|
|
92
102
|
return {
|
|
93
103
|
project: tracking.project || path.basename(cwd),
|
|
94
104
|
project_id: projectKey,
|
|
@@ -112,12 +122,21 @@ function buildPayload(options = {}) {
|
|
|
112
122
|
phase_name: tracking.phase_name,
|
|
113
123
|
total_phases: tracking.total_phases,
|
|
114
124
|
status: tracking.status,
|
|
125
|
+
// v7 lifecycle: "build" (milestone journey) or "operate" (post-launch update
|
|
126
|
+
// stream). The ERP uses this to count updates vs milestones and to stop
|
|
127
|
+
// expecting a "handoff" for a launched product. lifetime.updates_completed
|
|
128
|
+
// rides along in `lifetime` below.
|
|
129
|
+
lifecycle: tracking.lifecycle || "build",
|
|
130
|
+
...(tracking.launched_at ? { launched_at: tracking.launched_at } : {}),
|
|
131
|
+
...(tracking.launch_source ? { launch_source: tracking.launch_source } : {}),
|
|
115
132
|
tasks_done: tracking.tasks_done || 0,
|
|
116
133
|
tasks_total: tracking.tasks_total || 0,
|
|
117
134
|
verification: tracking.verification || "pending",
|
|
118
135
|
gap_cycles: (tracking.gap_cycles || {})[String(phase)] || 0,
|
|
119
136
|
build_count: tracking.build_count || 0,
|
|
120
137
|
deploy_count: tracking.deploy_count || 0,
|
|
138
|
+
command_usage: commandUsage,
|
|
139
|
+
prompt_samples: promptSamples,
|
|
121
140
|
deployed_url: tracking.deployed_url || "",
|
|
122
141
|
...(tracking.session_started_at ? { session_started_at: tracking.session_started_at } : {}),
|
|
123
142
|
...(tracking.last_pushed_at ? { last_pushed_at: tracking.last_pushed_at } : {}),
|
package/bin/runtime-manifest.js
CHANGED
|
@@ -13,6 +13,8 @@ const RUNTIME_BIN_SCRIPTS = [
|
|
|
13
13
|
{ file: "state-ledger.js", label: "state-ledger.js (hash-chained state event ledger)" },
|
|
14
14
|
{ file: "plan-contract.js", label: "plan-contract.js (plan JSON validator)" },
|
|
15
15
|
{ file: "contract-runner.js", label: "contract-runner.js (contract evidence runner)" },
|
|
16
|
+
{ file: "agent-status.js", label: "agent-status.js (per-task build status + wave fan-in barrier)" },
|
|
17
|
+
{ file: "analyze-gate.js", label: "analyze-gate.js (cross-artifact scope↔plan coverage gate)" },
|
|
16
18
|
{ file: "agent-runs.js", label: "agent-runs.js (agent telemetry writer)" },
|
|
17
19
|
{ file: "slop-detect.mjs", label: "slop-detect.mjs (anti-pattern scanner — runs pre-commit on frontend builds)" },
|
|
18
20
|
{ file: "erp-retry.js", label: "erp-retry.js (ERP report retry queue — drained by session-start hook and erp-flush CLI)" },
|