qualia-framework 7.2.0 → 7.2.2

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.
@@ -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
+ }
@@ -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/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: **25 active skills**. 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.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework",
3
- "version": "7.2.0",
3
+ "version": "7.2.2",
4
4
  "description": "Claude Code and Codex workflow framework by Qualia Solutions. Plan, build, verify, ship.",
5
5
  "bin": {
6
6
  "qualia-framework": "./bin/cli.js"
@@ -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
- - **QualiasolutionsCY**primary org for all Qualia Solutions projects
50
- - **SakaniQualia**org for Sakani-related projects (real estate platform)
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
 
@@ -39,6 +39,12 @@ node ${QUALIA_BIN}/last-report.js 2>/dev/null
39
39
  ```
40
40
  Exit 0 → it prints a one-line digest of the newest session report (`Last session ({date}, {age}d ago): {summary} → next: {next}`). Exit 1 → no reports yet (nothing to surface). When a project is loaded and a digest exists, print that line **at the very TOP of your output**, before the banner — so the first thing the operator (or a teammate picking the project up) sees is exactly where the last session ended.
41
41
 
42
+ Surface a pending framework update — read the same sticky notice the session-start banner uses (written by `auto-update.js` when npm ships a newer version), so an operator who only ever types `/qualia` still learns an update is out:
43
+ ```bash
44
+ node -e "try{const fs=require('fs'),os=require('os'),p=require('path');const h=process.env.QUALIA_HOME||p.join(os.homedir(),'.claude');const n=JSON.parse(fs.readFileSync(p.join(h,'.qualia-update-available.json'),'utf8'));const cur=(JSON.parse(fs.readFileSync(p.join(h,'.qualia-config.json'),'utf8')).version)||n.current;if(n&&n.latest&&n.latest!==cur)console.log('UPDATE_AVAILABLE '+cur+' -> '+n.latest)}catch{}"
45
+ ```
46
+ If it prints `UPDATE_AVAILABLE {cur} -> {latest}`, show one line at the **very top** of your reply (above the last-session digest and banner): `▲ Framework update available: v{cur} → v{latest}. Run: npx qualia-framework@latest install`. If it prints nothing, say nothing. Never run a live `npm view` here — `/qualia` must stay instant; this only reads the cached notice.
47
+
42
48
  Read conversation context — what has the user been doing, what errors occurred.
43
49
 
44
50
  ### 2. Classify and Route
@@ -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.7 (1M context) <noreply@anthropic.com>
302
+ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
303
303
  EOF
304
304
  )"
305
305
  ```
@@ -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 /home/qualia/.claude/rules/constitution.md
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 /home/qualia/.claude/references/archetypes/${ARCHETYPE}.md
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.7 adversarial deep-analysis (optional, longer)
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
 
@@ -2,6 +2,14 @@
2
2
  name: zoho-workflow
3
3
  description: "Zoho Invoice + Mail ops via ERP-first routing. Invoices from templates, cover emails, contacts, inbox, payment reminders. Triggers: 'invoice this client', 'send an email', 'check inbox', 'create a Zoho contact', 'payment reminder'."
4
4
  tags: [zoho, invoice, email, billing, crm]
5
+ # Money-moving + client-comms skill — least-privilege scope. ERP-first routing
6
+ # (mcp__qualia-erp__*) is preferred; raw Zoho Books/Mail are the fallback surface.
7
+ allowed-tools:
8
+ - Bash
9
+ - Read
10
+ - mcp__qualia-erp__*
11
+ - mcp__claude_ai_Zoho_Books__*
12
+ - mcp__claude_ai_Mail__*
5
13
  ---
6
14
 
7
15
  # Zoho Workflow
@@ -0,0 +1,23 @@
1
+ {
2
+ "_comment": "R6 golden verifier fixture — a SEEDED STUB the verifier MUST score FAIL. This proves the deterministic verifier (bin/verify-panel.js) cannot default-pass. The fixture contains (a) a surviving CRITICAL finding voted real by majority, and (b) an unvoted HIGH (INSUFFICIENT-EVIDENCE-style: unverified != disproven). Both survive the skeptic round, so the verdict MUST be FAIL (exit 1). If verify-panel ever regresses to a default-3/PASS, r6-golden.test.sh turns red.",
3
+ "phase": 99,
4
+ "lenses": ["security", "correctness"],
5
+ "findings": [
6
+ {
7
+ "lens": "security",
8
+ "file": "lib/supabase/client.ts",
9
+ "line": 12,
10
+ "severity": "CRITICAL",
11
+ "title": "service_role key imported into a client component (auth bypass)",
12
+ "votes": { "real": 3, "notReal": 0 }
13
+ },
14
+ {
15
+ "lens": "correctness",
16
+ "file": "app/api/checkout/route.ts",
17
+ "line": 47,
18
+ "severity": "HIGH",
19
+ "title": "INSUFFICIENT EVIDENCE — payment idempotency unverified, no test backs the happy path",
20
+ "votes": { "real": 0, "notReal": 0 }
21
+ }
22
+ ]
23
+ }
package/tests/lib.test.sh CHANGED
@@ -532,7 +532,7 @@ TMP=$(mktmp)
532
532
  mkdir -p "$TMP/home/.claude/bin" "$TMP/home/.claude/hooks" "$TMP/home/.claude/knowledge/daily-log" "$TMP/home/.claude/qualia-design" "$TMP/home/.claude/agents" "$TMP/home/.claude/qualia-templates" "$TMP/project"
533
533
  echo '{"installed_by":"Test","role":"OWNER","version":"6.3.0","erp":{"enabled":false}}' > "$TMP/home/.claude/.qualia-config.json"
534
534
  touch "$TMP/home/.claude/CLAUDE.md" "$TMP/home/.claude/settings.json"
535
- for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs dep-verify.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
535
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs dep-verify.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js qualia-doctor.js auto-report.js; do
536
536
  touch "$TMP/home/.claude/bin/$f"
537
537
  done
538
538
  for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js secret-guard.js; do
@@ -647,7 +647,7 @@ TMP=$(mktmp)
647
647
  mkdir -p "$TMP/.claude/bin" "$TMP/.claude/hooks" "$TMP/.claude/knowledge/daily-log" "$TMP/.claude/qualia-design" "$TMP/.claude/agents" "$TMP/.claude/qualia-templates" "$TMP/project/.planning"
648
648
  echo '{"installed_by":"Test","role":"OWNER","erp":{"enabled":false}}' > "$TMP/.claude/.qualia-config.json"
649
649
  touch "$TMP/.claude/CLAUDE.md" "$TMP/.claude/settings.json"
650
- for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs dep-verify.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
650
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs dep-verify.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js qualia-doctor.js auto-report.js; do
651
651
  touch "$TMP/.claude/bin/$f"
652
652
  done
653
653
  for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js secret-guard.js; do
@@ -0,0 +1,136 @@
1
+ #!/bin/bash
2
+ # memory-loop.test.sh — the one-loop contract: knowledge-flush vault dual-write
3
+ # (ITEM 13) + qualia-doctor freshness gates (ITEM 14b).
4
+ #
5
+ # Pure-function tests, no agent CLI, no live vault. Each builds a temp QUALIA_HOME
6
+ # and QUALIA_MEMORY so the loop logic is exercised hermetically.
7
+ #
8
+ # Run: bash tests/memory-loop.test.sh
9
+
10
+ PASS=0
11
+ FAIL=0
12
+ DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ BIN_DIR="$(cd "$DIR/../bin" && pwd)"
14
+ NODE="${NODE:-node}"
15
+ FLUSH="$BIN_DIR/knowledge-flush.js"
16
+ DOCTOR="$BIN_DIR/qualia-doctor.js"
17
+
18
+ assert_contains() {
19
+ local name="$1" hay="$2" needle="$3"
20
+ if echo "$hay" | grep -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
21
+ else echo " ✗ $name (missing '$needle' in: $hay)"; FAIL=$((FAIL+1)); fi
22
+ }
23
+ assert_ok() {
24
+ local name="$1" rc="$2"
25
+ if [ "$rc" -eq 0 ]; then echo " ✓ $name"; PASS=$((PASS+1));
26
+ else echo " ✗ $name (rc=$rc)"; FAIL=$((FAIL+1)); fi
27
+ }
28
+
29
+ echo "memory-loop.test.sh — dual-write + freshness gates"
30
+ echo ""
31
+
32
+ # --- syntax ---
33
+ $NODE -c "$FLUSH" 2>/dev/null && { echo " ✓ knowledge-flush.js syntax"; PASS=$((PASS+1)); } || { echo " ✗ knowledge-flush.js syntax"; FAIL=$((FAIL+1)); }
34
+ $NODE -c "$DOCTOR" 2>/dev/null && { echo " ✓ qualia-doctor.js syntax"; PASS=$((PASS+1)); } || { echo " ✗ qualia-doctor.js syntax"; FAIL=$((FAIL+1)); }
35
+
36
+ # ── ITEM 13: dualWriteVault mirrors curated concepts into the vault ──────────
37
+ TMP=$(mktemp -d); mkdir -p "$TMP/vault"
38
+ OUT=$(QUALIA_HOME="$TMP/home" QUALIA_MEMORY="$TMP/vault" $NODE -e '
39
+ const fs=require("fs"),path=require("path");
40
+ const kh=process.env.QUALIA_HOME;
41
+ fs.mkdirSync(path.join(kh,"knowledge","concepts"),{recursive:true});
42
+ fs.writeFileSync(path.join(kh,"knowledge","learned-patterns.md"),"# Patterns\n- always RLS\n");
43
+ fs.writeFileSync(path.join(kh,"knowledge","concepts","voice-call-state.md"),"# Voice\nstate\n");
44
+ const m=require("'"$FLUSH"'");
45
+ const r1=m.dualWriteVault();
46
+ const files1=fs.readdirSync(m.VAULT_CONCEPTS_DIR).sort();
47
+ const r2=m.dualWriteVault();
48
+ const files2=fs.readdirSync(m.VAULT_CONCEPTS_DIR).sort();
49
+ console.log("detail="+r1.event_detail);
50
+ console.log("written="+r1.written);
51
+ console.log("idempotent="+(files1.length===files2.length && files1.join(",")===files2.join(",")));
52
+ console.log("files="+files2.join(","));
53
+ const sample=fs.readFileSync(path.join(m.VAULT_CONCEPTS_DIR,files2[0]),"utf8");
54
+ console.log("hasFrontmatter="+sample.includes("source: qualia-framework/knowledge-flush"));
55
+ ' 2>&1)
56
+ assert_contains "dual-write mirrors concepts (vault-mirrored)" "$OUT" "detail=vault-mirrored"
57
+ assert_contains "dual-write wrote 2 files" "$OUT" "written=2"
58
+ assert_contains "dual-write is idempotent" "$OUT" "idempotent=true"
59
+ assert_contains "dual-write emits a concept-prefixed subdir file" "$OUT" "concept-voice-call-state.md"
60
+ assert_contains "dual-write emits curated top-level file" "$OUT" "learned-patterns.md"
61
+ assert_contains "dual-write stamps provenance front-matter" "$OUT" "hasFrontmatter=true"
62
+ rm -rf "$TMP"
63
+
64
+ # --- vault absent → graceful skip, no crash ---
65
+ TMP=$(mktemp -d)
66
+ OUT=$(QUALIA_HOME="$TMP/home" QUALIA_MEMORY="$TMP/does-not-exist" $NODE -e '
67
+ const fs=require("fs"),path=require("path");
68
+ const kh=process.env.QUALIA_HOME;
69
+ fs.mkdirSync(path.join(kh,"knowledge"),{recursive:true});
70
+ fs.writeFileSync(path.join(kh,"knowledge","learned-patterns.md"),"# P\n- x\n");
71
+ const m=require("'"$FLUSH"'");
72
+ const r=m.dualWriteVault();
73
+ console.log("detail="+r.event_detail+" written="+r.written);
74
+ ' 2>&1)
75
+ assert_ok "vault-absent does not crash" $?
76
+ assert_contains "vault-absent is skipped gracefully" "$OUT" "detail=vault-absent"
77
+ rm -rf "$TMP"
78
+
79
+ # ── ITEM 14b: freshness gates ───────────────────────────────────────────────
80
+ # Healthy loop: fresh daily-log, recent flush log, fresh export with matching
81
+ # coverage and no deprecated rows → all PASS.
82
+ TMP=$(mktemp -d)
83
+ KH="$TMP/home"; VAULT="$TMP/vault"
84
+ mkdir -p "$KH/knowledge/daily-log"
85
+ TODAY=$(date +%Y-%m-%d)
86
+ echo "# log" > "$KH/knowledge/daily-log/$TODAY.md"
87
+ printf '{"timestamp":"%s","event":"ok"}\n' "$(date -Iseconds)" > "$KH/.qualia-flush.log"
88
+ mkdir -p "$VAULT/wiki/sessions/concepts" "$VAULT/wiki/_export/concepts"
89
+ echo "# c1" > "$VAULT/wiki/sessions/concepts/c1.md"
90
+ echo "# c1 exported" > "$VAULT/wiki/_export/concepts/c1.md"
91
+ OUT=$(QUALIA_HOME="$KH" QUALIA_MEMORY="$VAULT" $NODE "$DOCTOR" --json 2>&1)
92
+ assert_contains "healthy daily-log → PASS" "$OUT" '"gate": "daily-log freshness"'
93
+ HEALTHY_FAILS=$(echo "$OUT" | grep -c '"status": "FAIL"')
94
+ if [ "$HEALTHY_FAILS" -eq 0 ]; then echo " ✓ healthy loop has zero FAIL gates"; PASS=$((PASS+1)); else echo " ✗ healthy loop has $HEALTHY_FAILS FAIL gate(s): $OUT"; FAIL=$((FAIL+1)); fi
95
+ rm -rf "$TMP"
96
+
97
+ # Broken loop: no daily-log, no flush log, no export → FAILs + --exit-code 1.
98
+ TMP=$(mktemp -d)
99
+ mkdir -p "$TMP/home/knowledge" "$TMP/vault"
100
+ OUT=$(QUALIA_HOME="$TMP/home" QUALIA_MEMORY="$TMP/vault" $NODE "$DOCTOR" --json 2>&1)
101
+ assert_contains "broken loop flags stale daily-log" "$OUT" 'no daily-log entries'
102
+ assert_contains "broken loop flags missing flush log" "$OUT" 'flush never ran'
103
+ assert_contains "broken loop flags missing export" "$OUT" 'export never ran'
104
+ QUALIA_HOME="$TMP/home" QUALIA_MEMORY="$TMP/vault" $NODE "$DOCTOR" --exit-code >/dev/null 2>&1
105
+ RC=$?
106
+ if [ "$RC" -eq 1 ]; then echo " ✓ broken loop --exit-code returns 1"; PASS=$((PASS+1)); else echo " ✗ broken loop --exit-code returned $RC"; FAIL=$((FAIL+1)); fi
107
+ rm -rf "$TMP"
108
+
109
+ # Coverage gate: source has 2 concepts, export has 1 → FAIL (stale/partial).
110
+ TMP=$(mktemp -d)
111
+ KH="$TMP/home"; VAULT="$TMP/vault"
112
+ mkdir -p "$KH/knowledge/daily-log"
113
+ echo "# log" > "$KH/knowledge/daily-log/$(date +%Y-%m-%d).md"
114
+ printf '{"timestamp":"%s","event":"ok"}\n' "$(date -Iseconds)" > "$KH/.qualia-flush.log"
115
+ mkdir -p "$VAULT/wiki/sessions/concepts" "$VAULT/wiki/_export/concepts"
116
+ echo "# a" > "$VAULT/wiki/sessions/concepts/a.md"
117
+ echo "# b" > "$VAULT/wiki/sessions/concepts/b.md"
118
+ echo "# a" > "$VAULT/wiki/_export/concepts/a.md"
119
+ OUT=$(QUALIA_HOME="$KH" QUALIA_MEMORY="$VAULT" $NODE "$DOCTOR" --json 2>&1)
120
+ assert_contains "partial export → coverage FAIL" "$OUT" 'export is stale or partial'
121
+ rm -rf "$TMP"
122
+
123
+ # Deprecated gate: an export file tagged deprecated → FAIL.
124
+ TMP=$(mktemp -d)
125
+ KH="$TMP/home"; VAULT="$TMP/vault"
126
+ mkdir -p "$KH/knowledge/daily-log" "$VAULT/wiki/_export"
127
+ echo "# log" > "$KH/knowledge/daily-log/$(date +%Y-%m-%d).md"
128
+ printf '{"timestamp":"%s","event":"ok"}\n' "$(date -Iseconds)" > "$KH/.qualia-flush.log"
129
+ printf -- '---\ntags: [deprecated, old]\n---\n# stale\n' > "$VAULT/wiki/_export/old.md"
130
+ OUT=$(QUALIA_HOME="$KH" QUALIA_MEMORY="$VAULT" $NODE "$DOCTOR" --json 2>&1)
131
+ assert_contains "deprecated row in export → FAIL" "$OUT" 'deprecated file(s)'
132
+ rm -rf "$TMP"
133
+
134
+ echo ""
135
+ echo "=== Results: $PASS passed, $FAIL failed ==="
136
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1