qualia-framework 7.1.0 → 7.2.1
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 +54 -0
- package/FLAGS.md +10 -0
- package/README.md +8 -8
- package/bin/cli.js +48 -0
- package/bin/host-adapters.js +5 -1
- package/bin/install.js +125 -1
- package/bin/knowledge-flush.js +123 -2
- package/bin/qualia-doctor.js +249 -0
- package/bin/qualia-ui.js +186 -1
- package/bin/runtime-manifest.js +1 -0
- package/bin/statusline.js +7 -1
- package/guide.md +6 -1
- package/hooks/session-start.js +1 -0
- package/package.json +1 -1
- package/rules/infrastructure.md +4 -2
- package/skills/qualia-doctor/SKILL.md +25 -1
- package/skills/qualia-polish/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +11 -0
- package/skills/qualia-scope/SKILL.md +2 -2
- package/skills/qualia-secure/SKILL.md +1 -1
- package/skills/qualia-ship/SKILL.md +15 -0
- package/skills/zoho-workflow/SKILL.md +8 -0
- package/tests/fixtures/r6-golden-fail-panel.json +23 -0
- package/tests/journey-spine.test.sh +171 -0
- package/tests/lib.test.sh +2 -2
- package/tests/memory-loop.test.sh +136 -0
- package/tests/r6-golden.test.sh +66 -0
- package/tests/refs.test.sh +33 -0
- package/tests/run-all.sh +3 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ~/.claude/bin/qualia-doctor.js — memory-loop freshness gates for /qualia-doctor.
|
|
3
|
+
//
|
|
4
|
+
// The memory loop (capture → promote → ingest → export) can silently freeze:
|
|
5
|
+
// the audit found raw/sessions/ stale for 23 days with no monitoring. These
|
|
6
|
+
// gates make staleness loud. Each gate prints PASS / WARN / FAIL with the
|
|
7
|
+
// offending detail, so the operator sees exactly which leg of the loop stopped.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node ~/.claude/bin/qualia-doctor.js # human-readable gates
|
|
11
|
+
// node ~/.claude/bin/qualia-doctor.js --json # machine-readable
|
|
12
|
+
// node ~/.claude/bin/qualia-doctor.js --exit-code # exit 1 if any gate FAILs
|
|
13
|
+
//
|
|
14
|
+
// Gates:
|
|
15
|
+
// 1. daily-log newest ≤ 2 days (capture is alive)
|
|
16
|
+
// 2. flush log last run ≤ 8 days (promote/flush is scheduled & running)
|
|
17
|
+
// 3. wiki/_export rebuilt ≤ 2 days (export is scheduled & running)
|
|
18
|
+
// 4. export concept count == allowed source set (export covers all concepts)
|
|
19
|
+
// 5. tags:deprecated == 0 in _export (no deprecated rows leaked out)
|
|
20
|
+
//
|
|
21
|
+
// Zero-dependency. Resolves the vault from $QUALIA_MEMORY else ~/qualia-memory,
|
|
22
|
+
// and the install home from $QUALIA_HOME else ~/.claude. Missing dirs surface
|
|
23
|
+
// as the gate's offending detail rather than a crash.
|
|
24
|
+
|
|
25
|
+
const fs = require("fs");
|
|
26
|
+
const path = require("path");
|
|
27
|
+
const os = require("os");
|
|
28
|
+
|
|
29
|
+
const HOME = os.homedir();
|
|
30
|
+
|
|
31
|
+
function qualiaHome() {
|
|
32
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
33
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
34
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
35
|
+
return path.join(HOME, ".claude");
|
|
36
|
+
}
|
|
37
|
+
function vaultRoot() {
|
|
38
|
+
return process.env.QUALIA_MEMORY || path.join(HOME, "qualia-memory");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
42
|
+
|
|
43
|
+
function ageDays(ms) {
|
|
44
|
+
return (Date.now() - ms) / DAY_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Newest mtime among files in a dir (optionally filtered). null if dir absent/empty.
|
|
48
|
+
function newestMtime(dir, filterFn) {
|
|
49
|
+
if (!fs.existsSync(dir)) return null;
|
|
50
|
+
let newest = null;
|
|
51
|
+
let stack = [dir];
|
|
52
|
+
while (stack.length) {
|
|
53
|
+
const d = stack.pop();
|
|
54
|
+
let entries;
|
|
55
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
56
|
+
for (const e of entries) {
|
|
57
|
+
const p = path.join(d, e.name);
|
|
58
|
+
if (e.isDirectory()) { stack.push(p); continue; }
|
|
59
|
+
if (filterFn && !filterFn(e.name, p)) continue;
|
|
60
|
+
try {
|
|
61
|
+
const m = fs.statSync(p).mtimeMs;
|
|
62
|
+
if (newest === null || m > newest) newest = m;
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return newest;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Gate definitions ────────────────────────────────────────────────────
|
|
70
|
+
// Each returns { gate, status: PASS|WARN|FAIL, detail }.
|
|
71
|
+
|
|
72
|
+
function gateDailyLog(QUALIA_HOME) {
|
|
73
|
+
const dir = path.join(QUALIA_HOME, "knowledge", "daily-log");
|
|
74
|
+
const newest = newestMtime(dir, (name) => name.endsWith(".md"));
|
|
75
|
+
if (newest === null) {
|
|
76
|
+
return { gate: "daily-log freshness", status: "FAIL", detail: `no daily-log entries at ${dir}` };
|
|
77
|
+
}
|
|
78
|
+
const age = ageDays(newest);
|
|
79
|
+
if (age <= 2) return { gate: "daily-log freshness", status: "PASS", detail: `newest ${age.toFixed(1)}d old (≤2d)` };
|
|
80
|
+
if (age <= 4) return { gate: "daily-log freshness", status: "WARN", detail: `newest ${age.toFixed(1)}d old (>2d)` };
|
|
81
|
+
return { gate: "daily-log freshness", status: "FAIL", detail: `newest ${age.toFixed(1)}d old — capture may be dead` };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function gateFlushLog(QUALIA_HOME) {
|
|
85
|
+
const logFile = path.join(QUALIA_HOME, ".qualia-flush.log");
|
|
86
|
+
if (!fs.existsSync(logFile)) {
|
|
87
|
+
return { gate: "flush last-run", status: "FAIL", detail: `no flush log at ${logFile} — flush never ran` };
|
|
88
|
+
}
|
|
89
|
+
// Last run = last "ok"/"skipped"/"failed" event timestamp in the JSONL log.
|
|
90
|
+
let lastTs = null;
|
|
91
|
+
try {
|
|
92
|
+
const lines = fs.readFileSync(logFile, "utf8").trim().split("\n").filter(Boolean);
|
|
93
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
94
|
+
try {
|
|
95
|
+
const ev = JSON.parse(lines[i]);
|
|
96
|
+
if (ev.timestamp) { lastTs = Date.parse(ev.timestamp); break; }
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
} catch {}
|
|
100
|
+
if (lastTs === null || Number.isNaN(lastTs)) {
|
|
101
|
+
// Fall back to file mtime.
|
|
102
|
+
try { lastTs = fs.statSync(logFile).mtimeMs; } catch { lastTs = null; }
|
|
103
|
+
}
|
|
104
|
+
if (lastTs === null) {
|
|
105
|
+
return { gate: "flush last-run", status: "FAIL", detail: "flush log unreadable" };
|
|
106
|
+
}
|
|
107
|
+
const age = ageDays(lastTs);
|
|
108
|
+
if (age <= 8) return { gate: "flush last-run", status: "PASS", detail: `last run ${age.toFixed(1)}d ago (≤8d)` };
|
|
109
|
+
if (age <= 12) return { gate: "flush last-run", status: "WARN", detail: `last run ${age.toFixed(1)}d ago (>8d)` };
|
|
110
|
+
return { gate: "flush last-run", status: "FAIL", detail: `last run ${age.toFixed(1)}d ago — flush schedule may be broken` };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function gateExportFresh(VAULT) {
|
|
114
|
+
const exportDir = path.join(VAULT, "wiki", "_export");
|
|
115
|
+
const newest = newestMtime(exportDir);
|
|
116
|
+
if (newest === null) {
|
|
117
|
+
return { gate: "team-export freshness", status: "FAIL", detail: `no export at ${exportDir} — export never ran` };
|
|
118
|
+
}
|
|
119
|
+
const age = ageDays(newest);
|
|
120
|
+
if (age <= 2) return { gate: "team-export freshness", status: "PASS", detail: `rebuilt ${age.toFixed(1)}d ago (≤2d)` };
|
|
121
|
+
if (age <= 4) return { gate: "team-export freshness", status: "WARN", detail: `rebuilt ${age.toFixed(1)}d ago (>2d)` };
|
|
122
|
+
return { gate: "team-export freshness", status: "FAIL", detail: `rebuilt ${age.toFixed(1)}d ago — export schedule may be broken` };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Allowed source set = concept markdown in the vault's source concepts tier
|
|
126
|
+
// (wiki/sessions/concepts/ + wiki/concepts/). Export must cover the same count
|
|
127
|
+
// of concept files (under _export/.../concepts or _export wiki concepts).
|
|
128
|
+
function countConceptMd(dir) {
|
|
129
|
+
if (!fs.existsSync(dir)) return 0;
|
|
130
|
+
let n = 0;
|
|
131
|
+
let stack = [dir];
|
|
132
|
+
while (stack.length) {
|
|
133
|
+
const d = stack.pop();
|
|
134
|
+
let entries;
|
|
135
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
136
|
+
for (const e of entries) {
|
|
137
|
+
const p = path.join(d, e.name);
|
|
138
|
+
if (e.isDirectory()) {
|
|
139
|
+
if (/concepts?$/i.test(e.name)) stack.push(p);
|
|
140
|
+
else stack.push(p);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (e.name.endsWith(".md") && /concepts?[\\/]/i.test(path.relative(dir, p) + "/")) n++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return n;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function gateExportCoverage(VAULT) {
|
|
150
|
+
// Source concepts: the curated/promoted concept tier in the vault.
|
|
151
|
+
const sourceDirs = [
|
|
152
|
+
path.join(VAULT, "wiki", "sessions", "concepts"),
|
|
153
|
+
path.join(VAULT, "wiki", "concepts"),
|
|
154
|
+
];
|
|
155
|
+
let sourceCount = 0;
|
|
156
|
+
for (const d of sourceDirs) {
|
|
157
|
+
if (!fs.existsSync(d)) continue;
|
|
158
|
+
try { sourceCount += fs.readdirSync(d).filter((f) => f.endsWith(".md")).length; } catch {}
|
|
159
|
+
}
|
|
160
|
+
const exportRoot = path.join(VAULT, "wiki", "_export");
|
|
161
|
+
if (!fs.existsSync(exportRoot)) {
|
|
162
|
+
return { gate: "export concept coverage", status: "FAIL", detail: `no export dir — source has ${sourceCount} concept(s)` };
|
|
163
|
+
}
|
|
164
|
+
const exportCount = countConceptMd(exportRoot);
|
|
165
|
+
if (sourceCount === 0) {
|
|
166
|
+
return { gate: "export concept coverage", status: "WARN", detail: "no source concepts to cover yet" };
|
|
167
|
+
}
|
|
168
|
+
if (exportCount === sourceCount) {
|
|
169
|
+
return { gate: "export concept coverage", status: "PASS", detail: `${exportCount}/${sourceCount} concepts exported` };
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
gate: "export concept coverage",
|
|
173
|
+
status: "FAIL",
|
|
174
|
+
detail: `export has ${exportCount} concept(s), source has ${sourceCount} — export is stale or partial`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function gateNoDeprecated(VAULT) {
|
|
179
|
+
const exportRoot = path.join(VAULT, "wiki", "_export");
|
|
180
|
+
if (!fs.existsSync(exportRoot)) {
|
|
181
|
+
return { gate: "no deprecated in export", status: "FAIL", detail: "no export dir to scan" };
|
|
182
|
+
}
|
|
183
|
+
const offenders = [];
|
|
184
|
+
let stack = [exportRoot];
|
|
185
|
+
while (stack.length) {
|
|
186
|
+
const d = stack.pop();
|
|
187
|
+
let entries;
|
|
188
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
189
|
+
for (const e of entries) {
|
|
190
|
+
const p = path.join(d, e.name);
|
|
191
|
+
if (e.isDirectory()) { stack.push(p); continue; }
|
|
192
|
+
if (!e.name.endsWith(".md")) continue;
|
|
193
|
+
try {
|
|
194
|
+
const txt = fs.readFileSync(p, "utf8");
|
|
195
|
+
// Match `tags: [..., deprecated, ...]`, `tags:deprecated`, or a `deprecated` tag line.
|
|
196
|
+
if (/tags?\s*:\s*\[?[^\]\n]*\bdeprecated\b/i.test(txt) || /^\s*-\s*deprecated\s*$/im.test(txt)) {
|
|
197
|
+
offenders.push(path.relative(exportRoot, p));
|
|
198
|
+
}
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (offenders.length === 0) {
|
|
203
|
+
return { gate: "no deprecated in export", status: "PASS", detail: "0 deprecated-tagged files" };
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
gate: "no deprecated in export",
|
|
207
|
+
status: "FAIL",
|
|
208
|
+
detail: `${offenders.length} deprecated file(s): ${offenders.slice(0, 3).join(", ")}${offenders.length > 3 ? "…" : ""}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function runGates() {
|
|
213
|
+
const QUALIA_HOME = qualiaHome();
|
|
214
|
+
const VAULT = vaultRoot();
|
|
215
|
+
return [
|
|
216
|
+
gateDailyLog(QUALIA_HOME),
|
|
217
|
+
gateFlushLog(QUALIA_HOME),
|
|
218
|
+
gateExportFresh(VAULT),
|
|
219
|
+
gateExportCoverage(VAULT),
|
|
220
|
+
gateNoDeprecated(VAULT),
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function main() {
|
|
225
|
+
const args = process.argv.slice(2);
|
|
226
|
+
const json = args.includes("--json");
|
|
227
|
+
const exitCode = args.includes("--exit-code");
|
|
228
|
+
const results = runGates();
|
|
229
|
+
|
|
230
|
+
if (json) {
|
|
231
|
+
console.log(JSON.stringify({ gates: results }, null, 2));
|
|
232
|
+
} else {
|
|
233
|
+
console.log("Memory-loop freshness gates");
|
|
234
|
+
for (const r of results) {
|
|
235
|
+
const mark = r.status === "PASS" ? "PASS" : r.status === "WARN" ? "WARN" : "FAIL";
|
|
236
|
+
console.log(` [${mark}] ${r.gate} — ${r.detail}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const anyFail = results.some((r) => r.status === "FAIL");
|
|
241
|
+
if (exitCode && anyFail) process.exit(1);
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (require.main === module) {
|
|
246
|
+
main();
|
|
247
|
+
} else {
|
|
248
|
+
module.exports = { runGates, gateDailyLog, gateFlushLog, gateExportFresh, gateExportCoverage, gateNoDeprecated, qualiaHome, vaultRoot };
|
|
249
|
+
}
|
package/bin/qualia-ui.js
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
// plan-summary <path/to/plan.md> — story-file dashboard for a plan
|
|
21
21
|
// journey-tree [path/to/JOURNEY.md] — ladder view of all milestones, current highlighted
|
|
22
22
|
// milestone-complete <num> <name> <next> — celebration banner on milestone close
|
|
23
|
+
// spine [path/to/JOURNEY.md] — compact horizontal "you are here" journey map (start banner)
|
|
24
|
+
// onboard <name> — first-run milestone mental-model card (post-install, employees)
|
|
25
|
+
// phase-complete <num> <name> [k=v…] — per-phase celebration (tasks, mdone, mtotal, streak, next, nextname)
|
|
26
|
+
// clockout <name> [k=v…] — end-of-day report card (phases, tasks, commits, mdone, mtotal, streak, date, erp)
|
|
23
27
|
|
|
24
28
|
const fs = require("fs");
|
|
25
29
|
const path = require("path");
|
|
@@ -127,6 +131,17 @@ function progressBar(phase, total) {
|
|
|
127
131
|
return `${bar} ${DIM}${pct}%${RESET}`;
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
// Compact filled/empty tick bar (▰▱) for inline use in cards and the journey
|
|
135
|
+
// spine. Scales `done` of `total` onto `width` ticks. Returns "" when total<1.
|
|
136
|
+
function barTicks(done, total, width) {
|
|
137
|
+
const w = width || 5;
|
|
138
|
+
const t = Number(total) || 0;
|
|
139
|
+
if (t < 1) return "";
|
|
140
|
+
const d = Math.max(0, Math.min(t, Number(done) || 0));
|
|
141
|
+
const filled = Math.max(0, Math.min(w, Math.round((d / t) * w)));
|
|
142
|
+
return `${TEAL}${"▰".repeat(filled)}${DIM2}${"▱".repeat(w - filled)}${RESET}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
130
145
|
function colorForStatus(s) {
|
|
131
146
|
const colors = {
|
|
132
147
|
setup: DIM,
|
|
@@ -410,6 +425,166 @@ function cmdMilestoneComplete(num, name, nextName) {
|
|
|
410
425
|
console.log("");
|
|
411
426
|
}
|
|
412
427
|
|
|
428
|
+
// ─── Journey Spine (compact horizontal "you are here") ───
|
|
429
|
+
// One-line milestone ladder with the current milestone marked and a caret
|
|
430
|
+
// beneath it. Rendered by the session-start banner so an employee sees where
|
|
431
|
+
// they are in the arc the instant they open the terminal. Skips itself when
|
|
432
|
+
// there is no real multi-milestone journey to map (nothing to orient against).
|
|
433
|
+
function cmdSpine(journeyPath) {
|
|
434
|
+
const state = readState();
|
|
435
|
+
if (!state || !state.ok) return;
|
|
436
|
+
const current = state.milestone || 1;
|
|
437
|
+
|
|
438
|
+
// Milestone list: JOURNEY.md headers first, then state.milestones, else none.
|
|
439
|
+
let milestones = [];
|
|
440
|
+
const p = journeyPath || ".planning/JOURNEY.md";
|
|
441
|
+
try {
|
|
442
|
+
const content = fs.readFileSync(p, "utf8");
|
|
443
|
+
const re = /^## Milestone (\d+)\s*·\s*(.+?)\s*(?:\[[^\]]*\])?\r?$/gm;
|
|
444
|
+
let m;
|
|
445
|
+
while ((m = re.exec(content)) !== null) {
|
|
446
|
+
milestones.push({ num: parseInt(m[1]), name: m[2].trim() });
|
|
447
|
+
}
|
|
448
|
+
} catch {}
|
|
449
|
+
if (milestones.length === 0 && Array.isArray(state.milestones)) {
|
|
450
|
+
milestones = state.milestones
|
|
451
|
+
.map((x, i) => ({ num: x.num || i + 1, name: x.name || x.milestone_name || "" }));
|
|
452
|
+
}
|
|
453
|
+
// Nothing to orient against on a single-milestone (or unknown) project.
|
|
454
|
+
if (milestones.length < 2) return;
|
|
455
|
+
milestones.sort((a, b) => a.num - b.num);
|
|
456
|
+
|
|
457
|
+
// Build the colored spine while tracking the plain-text column of the current
|
|
458
|
+
// milestone's marker, so the caret lands exactly beneath it.
|
|
459
|
+
const SEP = "──";
|
|
460
|
+
const colored = [];
|
|
461
|
+
const plainSegs = [];
|
|
462
|
+
let caretCol = -1;
|
|
463
|
+
for (let i = 0; i < milestones.length; i++) {
|
|
464
|
+
const ms = milestones[i];
|
|
465
|
+
const isPast = ms.num < current;
|
|
466
|
+
const isCurrent = ms.num === current;
|
|
467
|
+
const marker = isPast ? `${GREEN}●${RESET}`
|
|
468
|
+
: isCurrent ? `${TEAL}${BOLD}◆${RESET}`
|
|
469
|
+
: `${DIM2}○${RESET}`;
|
|
470
|
+
const labelColor = isCurrent ? TEAL + BOLD : isPast ? DIM : WHITE;
|
|
471
|
+
const labelPlain = `M${ms.num} `;
|
|
472
|
+
if (isCurrent) {
|
|
473
|
+
const prefix = plainSegs.length ? plainSegs.join(SEP).length + SEP.length : 0;
|
|
474
|
+
caretCol = prefix + labelPlain.length; // column of the marker glyph
|
|
475
|
+
}
|
|
476
|
+
plainSegs.push(`${labelPlain}●`);
|
|
477
|
+
colored.push(`${labelColor}M${ms.num}${RESET} ${marker}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const LABEL_W = 10; // "Journey" label field width (2-space indent added below)
|
|
481
|
+
console.log("");
|
|
482
|
+
console.log(` ${pad(DIM + "Journey" + RESET, LABEL_W)}${colored.join(`${DIM2}${SEP}${RESET}`)}`);
|
|
483
|
+
if (caretCol >= 0) {
|
|
484
|
+
console.log(` ${" ".repeat(LABEL_W + caretCol)}${TEAL}↑${RESET} ${DIM}you are here${RESET}`);
|
|
485
|
+
}
|
|
486
|
+
const last = (state.last_activity || "").trim();
|
|
487
|
+
if (last && !/regenerated from increments/i.test(last)) {
|
|
488
|
+
const short = last.length > 56 ? last.slice(0, 55) + "…" : last;
|
|
489
|
+
console.log(` ${pad(DIM + "Last" + RESET, LABEL_W)}${DIM}${short}${RESET}`);
|
|
490
|
+
}
|
|
491
|
+
console.log("");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ─── Onboard (first-run mental model for a new employee) ─
|
|
495
|
+
// Shown after install for non-OWNER roles: the milestone mental model + the one
|
|
496
|
+
// first move. Keeps a new hire from staring at a prompt wondering what to type.
|
|
497
|
+
function cmdOnboard(name) {
|
|
498
|
+
const who = name ? ` ${DIM}·${RESET} ${WHITE}welcome aboard, ${name}${RESET}` : ` ${DIM}·${RESET} ${WHITE}welcome aboard${RESET}`;
|
|
499
|
+
console.log("");
|
|
500
|
+
console.log(` ${TEAL}${BOLD}⬢${RESET} ${WHITE}${BOLD}QUALIA${RESET}${who}`);
|
|
501
|
+
console.log(` ${RULE_DIM}`);
|
|
502
|
+
console.log(` ${WHITE}You build in milestones:${RESET} ${TEAL}Plan ${DIM}→ ${TEAL}Build ${DIM}→ ${TEAL}Verify ${DIM}→ ${TEAL}Ship${RESET}`);
|
|
503
|
+
console.log(` ${DIM}You never have to guess what's next — the statusline shows where you are,${RESET}`);
|
|
504
|
+
console.log(` ${DIM}and ${RESET}${TEAL}/qualia${RESET}${DIM} always tells you the next command.${RESET}`);
|
|
505
|
+
console.log(` ${RULE_DIM}`);
|
|
506
|
+
console.log(` ${TEAL}⟶${RESET} ${WHITE}First move:${RESET} ${TEAL}${BOLD}/qualia${RESET}`);
|
|
507
|
+
console.log("");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ─── Phase Complete (per-phase celebration + next move) ──
|
|
511
|
+
// Smaller sibling of milestone-complete, shown by /qualia-build (or /qualia-ship)
|
|
512
|
+
// when a phase closes. Optional metrics passed as key=value (tasks, mdone,
|
|
513
|
+
// mtotal, streak, next, nextname); any omitted metric is simply not rendered.
|
|
514
|
+
function cmdPhaseComplete(num, name, kv) {
|
|
515
|
+
kv = kv || {};
|
|
516
|
+
console.log("");
|
|
517
|
+
const title = name ? ` ${DIM}·${RESET} ${TEAL}${name}${RESET}` : "";
|
|
518
|
+
console.log(` ${GREEN}${BOLD}◆${RESET} ${WHITE}${BOLD}PHASE ${num} COMPLETE${RESET}${title}`);
|
|
519
|
+
console.log(` ${RULE_DIM}`);
|
|
520
|
+
const checks = [];
|
|
521
|
+
if (kv.tasks) checks.push(`${GREEN}✓${RESET} ${WHITE}${kv.tasks} tasks${RESET}`);
|
|
522
|
+
checks.push(`${GREEN}✓${RESET} ${WHITE}verified${RESET}`);
|
|
523
|
+
checks.push(`${GREEN}✓${RESET} ${WHITE}shipped${RESET}`);
|
|
524
|
+
console.log(` ${checks.join(" ")}`);
|
|
525
|
+
if (kv.mdone && kv.mtotal) {
|
|
526
|
+
const d = parseInt(kv.mdone, 10), t = parseInt(kv.mtotal, 10);
|
|
527
|
+
const remaining = t - d;
|
|
528
|
+
const tail = remaining <= 0 ? `${GREEN}milestone ready to close${RESET}`
|
|
529
|
+
: remaining === 1 ? `${DIM}one more to close the milestone${RESET}`
|
|
530
|
+
: `${DIM}${remaining} phases left in this milestone${RESET}`;
|
|
531
|
+
console.log(` ${pad(DIM + "Milestone" + RESET, 11)}${barTicks(d, t)} ${DIM}${d}/${t} phases · ${RESET}${tail}`);
|
|
532
|
+
}
|
|
533
|
+
if (kv.streak && parseInt(kv.streak, 10) > 1) {
|
|
534
|
+
console.log(` ${YELLOW}🔥 ${kv.streak} phases shipped today${RESET}`);
|
|
535
|
+
}
|
|
536
|
+
console.log(` ${RULE_DIM}`);
|
|
537
|
+
if (kv.next) {
|
|
538
|
+
const nn = kv.nextname ? ` ${DIM}(${kv.nextname})${RESET}` : "";
|
|
539
|
+
console.log(` ${TEAL}⟶${RESET} ${WHITE}Next:${RESET} ${TEAL}${BOLD}${kv.next}${RESET}${nn}`);
|
|
540
|
+
}
|
|
541
|
+
console.log("");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ─── Clock Out (end-of-day report card) ──────────────────
|
|
545
|
+
// Rendered by /qualia-report at shift close. Metrics passed as key=value
|
|
546
|
+
// (phases, tasks, commits, mdone, mtotal, streak, date, erp=ok|queued).
|
|
547
|
+
function cmdClockout(name, kv) {
|
|
548
|
+
kv = kv || {};
|
|
549
|
+
console.log("");
|
|
550
|
+
const who = name ? ` ${DIM}·${RESET} ${WHITE}${name}${RESET}` : "";
|
|
551
|
+
const date = kv.date ? ` ${DIM}·${RESET} ${DIM}${kv.date}${RESET}` : "";
|
|
552
|
+
console.log(` ${TEAL}${BOLD}▤${RESET} ${WHITE}${BOLD}CLOCK OUT${RESET}${who}${date}`);
|
|
553
|
+
console.log(` ${RULE_DIM}`);
|
|
554
|
+
const shipped = [];
|
|
555
|
+
if (kv.phases) shipped.push(`${WHITE}${kv.phases} phases${RESET}`);
|
|
556
|
+
if (kv.tasks) shipped.push(`${WHITE}${kv.tasks} tasks${RESET}`);
|
|
557
|
+
if (kv.commits) shipped.push(`${WHITE}${kv.commits} commits${RESET}`);
|
|
558
|
+
if (shipped.length) {
|
|
559
|
+
console.log(` ${pad(DIM + "Shipped" + RESET, 12)}${shipped.join(` ${DIM}·${RESET} `)}`);
|
|
560
|
+
}
|
|
561
|
+
if (kv.mdone && kv.mtotal) {
|
|
562
|
+
const d = parseInt(kv.mdone, 10), t = parseInt(kv.mtotal, 10);
|
|
563
|
+
const pct = t > 0 ? Math.round((d / t) * 100) : 0;
|
|
564
|
+
console.log(` ${pad(DIM + "Milestone" + RESET, 12)}${barTicks(d, t)} ${DIM}${pct}%${RESET}`);
|
|
565
|
+
}
|
|
566
|
+
if (kv.streak && parseInt(kv.streak, 10) >= 1) {
|
|
567
|
+
console.log(` ${pad(DIM + "Streak" + RESET, 12)}${YELLOW}${kv.streak} days${RESET}`);
|
|
568
|
+
}
|
|
569
|
+
console.log(` ${RULE_DIM}`);
|
|
570
|
+
const tail = kv.erp === "ok" ? `${GREEN}Report uploaded to ERP ✓${RESET}`
|
|
571
|
+
: kv.erp === "queued" ? `${YELLOW}Report queued — uploads next session${RESET}`
|
|
572
|
+
: `${DIM}Session report saved${RESET}`;
|
|
573
|
+
console.log(` ${tail} ${DIM}See you tomorrow.${RESET}`);
|
|
574
|
+
console.log("");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Parse trailing `key=value` CLI args into an object (used by the lifecycle
|
|
578
|
+
// cards so optional metrics are positional-order-independent).
|
|
579
|
+
function parseKV(args) {
|
|
580
|
+
const o = {};
|
|
581
|
+
for (const a of args || []) {
|
|
582
|
+
const i = a.indexOf("=");
|
|
583
|
+
if (i > 0) o[a.slice(0, i)] = a.slice(i + 1);
|
|
584
|
+
}
|
|
585
|
+
return o;
|
|
586
|
+
}
|
|
587
|
+
|
|
413
588
|
// ─── Plan Summary (story-file dashboard) ─────────────────
|
|
414
589
|
// Renders a polished overview of a plan file: phase goal, tasks grouped by wave,
|
|
415
590
|
// persona chips, dependency lines, AC count, validation count. Called by
|
|
@@ -733,6 +908,12 @@ module.exports = {
|
|
|
733
908
|
divider,
|
|
734
909
|
section,
|
|
735
910
|
sectionClose,
|
|
911
|
+
// Lifecycle cards (called in-process by install.js / reused by skills)
|
|
912
|
+
onboard: cmdOnboard,
|
|
913
|
+
phaseComplete: cmdPhaseComplete,
|
|
914
|
+
clockout: cmdClockout,
|
|
915
|
+
spine: cmdSpine,
|
|
916
|
+
barTicks,
|
|
736
917
|
// Existing helpers (kept exposed for reuse)
|
|
737
918
|
pad,
|
|
738
919
|
visibleLength,
|
|
@@ -776,9 +957,13 @@ switch (cmd) {
|
|
|
776
957
|
case "plan-summary": cmdPlanSummary(rest[0]); break;
|
|
777
958
|
case "journey-tree": cmdJourneyTree(rest[0]); break;
|
|
778
959
|
case "milestone-complete": cmdMilestoneComplete(rest[0], rest[1], rest.slice(2).join(" ")); break;
|
|
960
|
+
case "spine": cmdSpine(rest[0]); break;
|
|
961
|
+
case "onboard": cmdOnboard(rest.join(" ")); break;
|
|
962
|
+
case "phase-complete": cmdPhaseComplete(rest[0], rest[1] || "", parseKV(rest.slice(2))); break;
|
|
963
|
+
case "clockout": cmdClockout(rest[0] || "", parseKV(rest.slice(1))); break;
|
|
779
964
|
default:
|
|
780
965
|
console.error(
|
|
781
|
-
`Usage: qualia-ui.js <banner|context|divider|ok|fail|warn|info|spawn|wave|task|done|next|end|update|plan-summary|journey-tree|milestone-complete> [args]`
|
|
966
|
+
`Usage: qualia-ui.js <banner|context|divider|ok|fail|warn|info|spawn|wave|task|done|next|end|update|plan-summary|journey-tree|milestone-complete|spine|onboard|phase-complete|clockout> [args]`
|
|
782
967
|
);
|
|
783
968
|
process.exit(1);
|
|
784
969
|
}
|
package/bin/runtime-manifest.js
CHANGED
|
@@ -43,6 +43,7 @@ const RUNTIME_BIN_SCRIPTS = [
|
|
|
43
43
|
{ file: "learning-candidates.js", label: "learning-candidates.js (scan recent commits + daily-log for patterns worth promoting)" },
|
|
44
44
|
{ file: "status-snapshot.js", label: "status-snapshot.js (portable operator snapshot — install + project + work + ERP + memory)" },
|
|
45
45
|
{ file: "security-scan.js", label: "security-scan.js (static security scanner for agent config — secrets, permissions, hook hygiene)" },
|
|
46
|
+
{ file: "qualia-doctor.js", label: "qualia-doctor.js (memory-loop freshness gates for /qualia-doctor)" },
|
|
46
47
|
];
|
|
47
48
|
|
|
48
49
|
function binFiles() {
|
package/bin/statusline.js
CHANGED
|
@@ -205,7 +205,13 @@ try {
|
|
|
205
205
|
parts.push(mStr);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
if (total > 0)
|
|
208
|
+
if (total > 0) {
|
|
209
|
+
// Always-visible forward-motion bar: phase of total, scaled to 5 ticks.
|
|
210
|
+
const w = 5;
|
|
211
|
+
const filled = Math.max(0, Math.min(w, Math.round((phase / total) * w)));
|
|
212
|
+
const ticks = "▰".repeat(filled) + "▱".repeat(w - filled);
|
|
213
|
+
parts.push(`P${phase}/${total} ${ticks}`);
|
|
214
|
+
}
|
|
209
215
|
if (tasksTotal > 0) parts.push(`T${tasksDone}/${tasksTotal}`);
|
|
210
216
|
if (status) parts.push(status);
|
|
211
217
|
|
package/guide.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Follow the road. Type the commands. The framework handles the rest.
|
|
4
4
|
> `--auto` chains the whole road end-to-end with only two human checkpoints per project.
|
|
5
5
|
|
|
6
|
-
Surface: **
|
|
6
|
+
Surface: **28 active skills** (`skills/` is the source of truth — run `qualia-framework doctor` for the live list). Use `/qualia-fix` for broken behavior, `/qualia-feature` for new single-feature work, `/qualia-scope` in PROJECT MODE for kickoff capture, `/qualia-polish --loop` for the autonomous visual loop, and `/qualia-polish --vibe` for fast layout-preserving aesthetic pivots.
|
|
7
7
|
|
|
8
8
|
For the identity statement see [`SOUL.md`](./SOUL.md). For every skill flag see [`FLAGS.md`](./FLAGS.md). When something breaks see [`TROUBLESHOOTING.md`](./TROUBLESHOOTING.md). For release history see [`CHANGELOG.md`](./CHANGELOG.md).
|
|
9
9
|
|
|
@@ -73,6 +73,11 @@ Append `--auto` to `/qualia-new` and the framework chains every step:
|
|
|
73
73
|
| Road view | `/qualia-road` | View and navigate journey/milestone/phase status |
|
|
74
74
|
| Lost — need next command | `/qualia` | Mechanical state-driven router |
|
|
75
75
|
| Confused — need to understand the situation | `/qualia-idk` | Three-scan diagnostic + paste-ready command sequence |
|
|
76
|
+
| Post-launch change | `/qualia-update` | Ship one update to a LAUNCHED project (lean plan → build → verify → ship, no milestone machinery) |
|
|
77
|
+
| Security scan | `/qualia-secure` | Scan agent config (CLAUDE.md / settings / hooks / MCP) for injection, leaked secrets, unscoped perms |
|
|
78
|
+
| AI-feature gate | `/qualia-eval` | Evaluate an AI feature (chat / RAG / voice / agent) against a layered eval suite and gate on the result |
|
|
79
|
+
| Recall prior knowledge | `/qualia-recall` | OWNER-only — recall curated lessons from the knowledge layer + qualia-memory vault |
|
|
80
|
+
| Health check | `/qualia-doctor` | Framework health — install, project state, contracts, hooks, memory, ERP queue + safe repair suggestions |
|
|
76
81
|
|
|
77
82
|
## Full Journey Hierarchy
|
|
78
83
|
|
package/hooks/session-start.js
CHANGED
|
@@ -225,6 +225,7 @@ try {
|
|
|
225
225
|
fallbackText();
|
|
226
226
|
} else if (fs.existsSync(STATE_FILE)) {
|
|
227
227
|
runUi("banner", "router");
|
|
228
|
+
runUi("spine"); // horizontal "you are here" journey map (self-skips if <2 milestones)
|
|
228
229
|
renderWorkPacketContext();
|
|
229
230
|
const next = getNextCommand();
|
|
230
231
|
if (next) {
|
package/package.json
CHANGED
package/rules/infrastructure.md
CHANGED
|
@@ -46,8 +46,10 @@ Standard services across all Qualia projects. Use these unless the project expli
|
|
|
46
46
|
- `gh` — GitHub CLI (PRs, issues, repos)
|
|
47
47
|
|
|
48
48
|
## GitHub Organizations
|
|
49
|
-
|
|
50
|
-
-
|
|
49
|
+
> Canonical slugs (verified via `gh repo list`). This list is the single source of truth — other docs reference it, they do not restate it.
|
|
50
|
+
- **`Qualiasolutions`** — the home of the four core Qualia systems (`qualia-framework`, `qualia-memory`, `qualia-erp`, `qualiafinal`). Default target for internal/system repos.
|
|
51
|
+
- **`QualiaSolutionsCY`** — org for client/delivery projects (e.g. USD-Academy, innrvo). Default target for new client work.
|
|
52
|
+
- **`SakaniQualia`** — org for Sakani-related projects (real estate platform).
|
|
51
53
|
- All repos are private by default
|
|
52
54
|
- Main integration: feature branches integrate to `main` at **`/qualia-ship`** (ship is the single merge point — it fast-forwards the branch into `main`, deploys from `main`, and deletes the branch). Pushes to `main` are **allowed and recorded** by `branch-guard` (per-employee tally → ERP) — accountability, not a hard block. `/qualia-report` sweeps for branches with unshipped commits + stale PRs at clock-out so nothing lingers. Keep GitHub branch protection on `main` OFF (or with the team allowed to push) for this model; if you re-enable required reviews, switch ship to an auto-merged PR instead.
|
|
53
55
|
|
|
@@ -92,6 +92,29 @@ node ${QUALIA_BIN}/knowledge.js list
|
|
|
92
92
|
|
|
93
93
|
Healthy memory has at least `index.md`, `agents.md`, and a writable `daily-log/` directory. Missing curated memory is not fatal, but missing installed memory files means reinstall.
|
|
94
94
|
|
|
95
|
+
### 5a. Memory-loop freshness gates
|
|
96
|
+
|
|
97
|
+
The memory loop (capture → promote/flush → ingest ERP → export team wiki) can silently freeze — the audit found capture stale for 23 days with zero monitoring. Run the freshness gates:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
node ${QUALIA_BIN}/qualia-doctor.js
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
It prints PASS / WARN / FAIL with the offending detail for five gates:
|
|
104
|
+
|
|
105
|
+
1. **daily-log freshness** — newest `knowledge/daily-log/*.md` ≤ 2 days old (capture is alive).
|
|
106
|
+
2. **flush last-run** — `.qualia-flush.log` last event ≤ 8 days ago (the nightly flush is scheduled and running).
|
|
107
|
+
3. **team-export freshness** — `${QUALIA_MEMORY:-~/qualia-memory}/wiki/_export` rebuilt ≤ 2 days ago (export is scheduled and running).
|
|
108
|
+
4. **export concept coverage** — `_export` concept count == the vault source concept set (`wiki/sessions/concepts/` + `wiki/concepts/`); a mismatch means the export is stale or partial.
|
|
109
|
+
5. **no deprecated in export** — zero `tags: deprecated` files in `_export` (deprecated rows must not leak into the published team snapshot).
|
|
110
|
+
|
|
111
|
+
Any **FAIL** means a leg of the loop stopped. Repair routing:
|
|
112
|
+
- daily-log FAIL → capture (Stop hook) not firing → reinstall to re-wire the `Stop` hook.
|
|
113
|
+
- flush / export FAIL → the nightly `qualia-loop.timer` is not running → `systemctl --user status qualia-loop.timer`, or reinstall (`npx qualia-framework@latest install`) to re-install the timer.
|
|
114
|
+
- coverage / deprecated FAIL → run the vault export manually (`python3 ~/qualia-memory/scripts/export-team-wiki.py`) and investigate the offending file.
|
|
115
|
+
|
|
116
|
+
These gates are advisory in `qualia-framework doctor` (they render under `Memory loop:`) and exit-coded when run with `--exit-code`.
|
|
117
|
+
|
|
95
118
|
## 6. ERP Queue Health
|
|
96
119
|
|
|
97
120
|
Run:
|
|
@@ -180,6 +203,7 @@ State ledger: ...
|
|
|
180
203
|
Contracts: ...
|
|
181
204
|
Planning hygiene: ...
|
|
182
205
|
Memory: ...
|
|
206
|
+
Memory loop: ...
|
|
183
207
|
Design/UI: ...
|
|
184
208
|
Employee experience: ...
|
|
185
209
|
Env: ...
|
|
@@ -188,7 +212,7 @@ ERP: ...
|
|
|
188
212
|
Next: ...
|
|
189
213
|
```
|
|
190
214
|
|
|
191
|
-
`Env` summarizes section 7's env-var check (PASS / DEGRADED / BLOCKED (owner key needed) / N/A). `CLI auth` summarizes the vercel/supabase/gh login checks (PASS if all three are authenticated, else DEGRADED with the first failing CLI named).
|
|
215
|
+
`Env` summarizes section 7's env-var check (PASS / DEGRADED / BLOCKED (owner key needed) / N/A). `CLI auth` summarizes the vercel/supabase/gh login checks (PASS if all three are authenticated, else DEGRADED with the first failing CLI named). `Memory loop` summarizes section 5a's five freshness gates (PASS if all PASS, WARN if any WARN, FAIL if any FAIL, naming the first failing gate).
|
|
192
216
|
|
|
193
217
|
## Rules
|
|
194
218
|
|
|
@@ -299,7 +299,7 @@ polish({scope}): {brief summary}
|
|
|
299
299
|
- {key change 2}
|
|
300
300
|
- rubric scores: typography {N}, color {N}, graphics {N}, ..., aggregate {N}/45
|
|
301
301
|
|
|
302
|
-
Co-Authored-By: Claude Opus 4.
|
|
302
|
+
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
303
303
|
EOF
|
|
304
304
|
)"
|
|
305
305
|
```
|
|
@@ -155,6 +155,17 @@ fi
|
|
|
155
155
|
node ${QUALIA_BIN}/qualia-ui.js divider
|
|
156
156
|
node ${QUALIA_BIN}/qualia-ui.js ok "Report $CLIENT_REPORT_ID complete."
|
|
157
157
|
node ${QUALIA_BIN}/qualia-ui.js info "Shift report submitted. You can clock out now."
|
|
158
|
+
|
|
159
|
+
# End-of-day report card. Name from config; commits = $COUNT; milestone
|
|
160
|
+
# progress from state. erp=ok when the upload confirmed, erp=queued when it was
|
|
161
|
+
# enqueued for retry. Every metric self-omits if its variable is empty.
|
|
162
|
+
EMP_NAME=$(node -pe 'try{JSON.parse(require("fs").readFileSync(process.env.HOME+"/.claude/.qualia-config.json","utf8")).installed_by||""}catch{""}' 2>/dev/null)
|
|
163
|
+
MDONE=$(node ${QUALIA_BIN}/state.js check 2>/dev/null | node -pe 'try{JSON.parse(require("fs").readFileSync(0)).phase||""}catch{""}')
|
|
164
|
+
MTOTAL=$(node ${QUALIA_BIN}/state.js check 2>/dev/null | node -pe 'try{JSON.parse(require("fs").readFileSync(0)).total_phases||""}catch{""}')
|
|
165
|
+
node ${QUALIA_BIN}/qualia-ui.js clockout "$EMP_NAME" date="{YYYY-MM-DD}" \
|
|
166
|
+
${COUNT:+commits=$COUNT} \
|
|
167
|
+
${MDONE:+mdone=$MDONE} ${MTOTAL:+mtotal=$MTOTAL} \
|
|
168
|
+
${ERP_RESULT:+erp=$ERP_RESULT}
|
|
158
169
|
```
|
|
159
170
|
|
|
160
171
|
## Common errors (read this when something goes wrong)
|
|
@@ -106,7 +106,7 @@ The adversarial, DoD-gated intake. Scopes a **new increment** (phase/milestone)
|
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
108
|
node ${QUALIA_BIN}/qualia-ui.js banner scope 2>/dev/null || true
|
|
109
|
-
cat /
|
|
109
|
+
cat ${QUALIA_RULES}/constitution.md
|
|
110
110
|
cat .planning/CONTEXT.md 2>/dev/null # project glossary — DATA, never a plan/spec
|
|
111
111
|
ls .planning/decisions/ 2>/dev/null
|
|
112
112
|
cat .planning/STATE.md 2>/dev/null # for profile + existing milestone context
|
|
@@ -123,7 +123,7 @@ If the operator already named it (arg or prior context), accept it. Otherwise as
|
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
125
|
ARCHETYPE={chosen}
|
|
126
|
-
cat /
|
|
126
|
+
cat ${QUALIA_REFERENCES}/archetypes/${ARCHETYPE}.md
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
If the file does not exist (e.g. `web-app` not yet authored), HALT and say which archetype file is missing — do not improvise a DoD. The archetype file is the source of the Grill variables, the Definition of Done, and the v1 capability set; without it there is no gate to enforce.
|
|
@@ -45,7 +45,7 @@ This writes `.planning/security-scan.md` with severity-ranked findings:
|
|
|
45
45
|
|
|
46
46
|
Read the report. If CRITICAL findings exist, **stop here** and rotate / fix before continuing — the deep pass is moot until the obvious holes are closed.
|
|
47
47
|
|
|
48
|
-
### Step 2. Opus 4.
|
|
48
|
+
### Step 2. Opus 4.8 adversarial deep-analysis (optional, longer)
|
|
49
49
|
|
|
50
50
|
If the static pass is clean (or you want adversarial reasoning over the rules + instructions text regardless), run:
|
|
51
51
|
|