qualia-framework 4.1.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agents/builder.md CHANGED
@@ -42,6 +42,34 @@ Parse every field in your task block:
42
42
  For every file you're about to modify — read it first. No exceptions.
43
43
  For every `@file` reference in Context — read it now.
44
44
 
45
+ ### 2b. Load Relevant Knowledge
46
+
47
+ Before writing code, check the memory layer for prior decisions and known
48
+ fixes that apply to this task. Hardcoded `cat ~/.claude/knowledge/X.md` is
49
+ forbidden — always go through the loader so newly-added knowledge files
50
+ become reachable automatically:
51
+
52
+ ```bash
53
+ # Always — read the index to discover what's available
54
+ node ~/.claude/bin/knowledge.js
55
+
56
+ # If your task touches Supabase/auth/RLS:
57
+ node ~/.claude/bin/knowledge.js load supabase-patterns
58
+ node ~/.claude/bin/knowledge.js load patterns
59
+
60
+ # If you're fixing a bug or hitting a familiar error, check known fixes:
61
+ node ~/.claude/bin/knowledge.js load fixes
62
+ node ~/.claude/bin/knowledge.js search "{error keyword}"
63
+
64
+ # For client-specific work (project name appears in PROJECT.md):
65
+ node ~/.claude/bin/knowledge.js load client
66
+ ```
67
+
68
+ If a relevant entry exists, follow it (or note in your DONE message that
69
+ you deviated and why). If nothing matches, proceed normally — the loader
70
+ prints `(no entries in X — use /qualia-learn to add one)` for missing files,
71
+ which is fine and means there is nothing to apply yet.
72
+
45
73
  ### 3. Build It
46
74
  - Follow the Action exactly as specified
47
75
  - Keep every Acceptance Criterion in mind — you are building toward observable user behaviors, not just files
@@ -2,8 +2,15 @@
2
2
  name: qualia-research-synthesizer
3
3
  description: Merges 4 parallel research outputs (STACK, FEATURES, ARCHITECTURE, PITFALLS) into SUMMARY.md with roadmap implications. Spawned by qualia-new after researchers complete.
4
4
  tools: Read, Write
5
+ model: haiku
5
6
  ---
6
7
 
8
+ <!-- model: haiku — pure synthesis of already-gathered markdown. No new
9
+ reasoning beyond merging well-structured research files. Cole Medin's
10
+ "model-per-node" pattern: switch to haiku only where the work is
11
+ mechanical, not where it's high-stakes. -->
12
+
13
+
7
14
  # Research Synthesizer
8
15
 
9
16
  You merge 4 dimensional research files into one executive SUMMARY.md that informs roadmap creation. You don't do new research — you synthesize what's already gathered.
package/bin/cli.js CHANGED
@@ -139,6 +139,8 @@ const QUALIA_HOOK_FILES = [
139
139
  "migration-guard.js",
140
140
  "pre-deploy-gate.js",
141
141
  "pre-compact.js",
142
+ "git-guardrails.js",
143
+ "stop-session-log.js",
142
144
  ];
143
145
  const QUALIA_LEGACY_HOOK_FILES = [
144
146
  "block-env-edit.js", // removed in v3.2.0
@@ -157,7 +159,7 @@ const QUALIA_AGENT_FILES = [
157
159
  ];
158
160
 
159
161
  // 3 Qualia bin scripts.
160
- const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js"];
162
+ const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js", "knowledge.js", "knowledge-flush.js"];
161
163
 
162
164
  // 5 Qualia rules.
163
165
  const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md", "infrastructure.md"];
@@ -872,6 +874,133 @@ function cmdErpPing() {
872
874
  process.exit(1);
873
875
  }
874
876
 
877
+ // ─── Doctor: post-install health check ───────────────────
878
+ // Mirrors the spot-check that session-start.js runs once per 24h. Surfaces
879
+ // missing files, mis-wired hooks, stale settings.json, and version drift.
880
+ // Use whenever something feels off, before opening an issue, or after a
881
+ // version upgrade. Exits 0 if healthy, 1 if any issue is found.
882
+ // ─── Flush: convenience wrapper around knowledge-flush.js ───────
883
+ // Exposes the cron-runnable script as a top-level CLI command so users can
884
+ // run `qualia-framework flush` ad-hoc. All args after the command pass through.
885
+ function cmdFlush() {
886
+ const flushScript = path.join(CLAUDE_DIR, "bin", "knowledge-flush.js");
887
+ if (!fs.existsSync(flushScript)) {
888
+ console.log(` ${RED}✗${RESET} knowledge-flush.js not installed at ${flushScript}`);
889
+ console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
890
+ process.exit(1);
891
+ }
892
+ const args = process.argv.slice(3);
893
+ const r = spawnSync(process.execPath, [flushScript, ...args], {
894
+ stdio: "inherit",
895
+ shell: false,
896
+ });
897
+ process.exit(typeof r.status === "number" ? r.status : 1);
898
+ }
899
+
900
+ function cmdDoctor() {
901
+ banner();
902
+ console.log("");
903
+
904
+ const issues = [];
905
+ const checks = [];
906
+
907
+ function check(label, ok, hint) {
908
+ checks.push({ label, ok, hint });
909
+ if (!ok) issues.push({ label, hint });
910
+ }
911
+
912
+ // ── Critical files (the same set session-start.js validates) ──
913
+ const criticalFiles = [
914
+ path.join(CLAUDE_DIR, "rules", "grounding.md"),
915
+ path.join(CLAUDE_DIR, "rules", "security.md"),
916
+ path.join(CLAUDE_DIR, "rules", "frontend.md"),
917
+ path.join(CLAUDE_DIR, "rules", "deployment.md"),
918
+ path.join(CLAUDE_DIR, "bin", "state.js"),
919
+ path.join(CLAUDE_DIR, "bin", "qualia-ui.js"),
920
+ path.join(CLAUDE_DIR, "bin", "statusline.js"),
921
+ path.join(CLAUDE_DIR, "bin", "knowledge.js"),
922
+ path.join(CLAUDE_DIR, "bin", "knowledge-flush.js"),
923
+ path.join(CLAUDE_DIR, "CLAUDE.md"),
924
+ CONFIG_FILE,
925
+ ];
926
+ for (const f of criticalFiles) {
927
+ check(
928
+ `${path.relative(CLAUDE_DIR, f) || f}`,
929
+ fs.existsSync(f),
930
+ "run: npx qualia-framework@latest install",
931
+ );
932
+ }
933
+
934
+ // ── Hooks ─────────────────────────────────────────────
935
+ for (const h of QUALIA_HOOK_FILES) {
936
+ check(
937
+ `hooks/${h}`,
938
+ fs.existsSync(path.join(CLAUDE_DIR, "hooks", h)),
939
+ "reinstall: npx qualia-framework@latest install",
940
+ );
941
+ }
942
+
943
+ // ── Knowledge layer ────────────────────────────────────
944
+ const knowledgeFiles = [
945
+ path.join(CLAUDE_DIR, "knowledge", "agents.md"),
946
+ path.join(CLAUDE_DIR, "knowledge", "index.md"),
947
+ path.join(CLAUDE_DIR, "knowledge", "daily-log"),
948
+ ];
949
+ for (const f of knowledgeFiles) {
950
+ check(
951
+ `knowledge/${path.basename(f)}${fs.existsSync(f) && fs.statSync(f).isDirectory() ? "/" : ""}`,
952
+ fs.existsSync(f),
953
+ "reinstall to initialize the memory layer: npx qualia-framework@latest install",
954
+ );
955
+ }
956
+
957
+ // ── settings.json hook wiring ──────────────────────────
958
+ const settingsPath = path.join(CLAUDE_DIR, "settings.json");
959
+ if (fs.existsSync(settingsPath)) {
960
+ try {
961
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
962
+ const wantEvents = ["SessionStart", "PreToolUse", "PreCompact", "Stop"];
963
+ for (const ev of wantEvents) {
964
+ const blocks = (settings.hooks || {})[ev] || [];
965
+ const hasQualia = blocks.some((b) =>
966
+ (b.hooks || []).some((h) => typeof h.command === "string" && h.command.includes(".claude")),
967
+ );
968
+ check(`settings.json hooks.${ev}`, hasQualia, "reinstall to wire hooks");
969
+ }
970
+ } catch (e) {
971
+ check("settings.json parseable", false, e.message);
972
+ }
973
+ } else {
974
+ check("settings.json", false, "Claude Code never ran here? Open Claude once first");
975
+ }
976
+
977
+ // ── Version vs. installed ──────────────────────────────
978
+ const cfg = readConfig();
979
+ if (cfg.installed_at) {
980
+ check(`config installed_by=${cfg.installed_by || "?"} role=${cfg.role || "?"}`, true);
981
+ } else {
982
+ check("config has install metadata", false, "reinstall to record");
983
+ }
984
+
985
+ // ── Render ────────────────────────────────────────────
986
+ for (const c of checks) {
987
+ const mark = c.ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
988
+ console.log(` ${mark} ${c.label}`);
989
+ }
990
+ console.log("");
991
+ if (issues.length === 0) {
992
+ console.log(` ${GREEN}All checks passed. Framework is healthy.${RESET}`);
993
+ console.log("");
994
+ process.exit(0);
995
+ }
996
+ console.log(` ${RED}${issues.length} issue${issues.length === 1 ? "" : "s"} found:${RESET}`);
997
+ for (const i of issues) {
998
+ console.log(` ${DIM}•${RESET} ${i.label}${i.hint ? ` ${DIM}— ${i.hint}${RESET}` : ""}`);
999
+ }
1000
+ console.log("");
1001
+ process.exit(1);
1002
+ }
1003
+
875
1004
  function cmdHelp() {
876
1005
  banner();
877
1006
  console.log("");
@@ -884,7 +1013,9 @@ function cmdHelp() {
884
1013
  console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
885
1014
  console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
886
1015
  console.log(` qualia-framework ${TEAL}analytics${RESET} Show outcome scoring & gap cycle stats`);
887
- console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
1016
+ console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key
1017
+ qualia-framework ${TEAL}doctor${RESET} Health-check the install (files, hooks, settings)
1018
+ qualia-framework ${TEAL}flush${RESET} Promote daily-log → curated knowledge (memory layer)`);
888
1019
  console.log("");
889
1020
  console.log(` ${WHITE}After install:${RESET}`);
890
1021
  console.log(` ${TG}/qualia${RESET} What should I do next?`);
@@ -941,6 +1072,15 @@ switch (cmd) {
941
1072
  case "ping":
942
1073
  cmdErpPing();
943
1074
  break;
1075
+ case "doctor":
1076
+ case "health":
1077
+ case "health-check":
1078
+ cmdDoctor();
1079
+ break;
1080
+ case "flush":
1081
+ case "knowledge-flush":
1082
+ cmdFlush();
1083
+ break;
944
1084
  default:
945
1085
  cmdHelp();
946
1086
  }
package/bin/install.js CHANGED
@@ -294,8 +294,12 @@ async function main() {
294
294
  const tmplDir = path.join(FRAMEWORK_DIR, "templates");
295
295
  const tmplDest = path.join(CLAUDE_DIR, "qualia-templates");
296
296
  if (!fs.existsSync(tmplDest)) fs.mkdirSync(tmplDest, { recursive: true });
297
+ // `knowledge/` is a sibling templates directory but installs to a different
298
+ // destination (~/.claude/knowledge/, not ~/.claude/qualia-templates/), so we
299
+ // skip it here and handle it in the dedicated "Knowledge layer" section below.
297
300
  for (const entry of fs.readdirSync(tmplDir, { withFileTypes: true })) {
298
301
  if (entry.name.startsWith(".")) continue;
302
+ if (entry.name === "knowledge") continue;
299
303
  const srcPath = path.join(tmplDir, entry.name);
300
304
  const destPath = path.join(tmplDest, entry.name);
301
305
  try {
@@ -311,6 +315,38 @@ async function main() {
311
315
  }
312
316
  }
313
317
 
318
+ // ─── Knowledge layer (Karpathy-style raw → wiki memory tier) ──────
319
+ // Initializes ~/.claude/knowledge/ on first install. Never overwrites
320
+ // existing files — re-running the installer is safe for users who have
321
+ // already accumulated learnings.
322
+ printSection("Knowledge layer");
323
+ const knowledgeSrc = path.join(FRAMEWORK_DIR, "templates", "knowledge");
324
+ const knowledgeDest = path.join(CLAUDE_DIR, "knowledge");
325
+ if (!fs.existsSync(knowledgeDest)) fs.mkdirSync(knowledgeDest, { recursive: true });
326
+ const dailyLogDir = path.join(knowledgeDest, "daily-log");
327
+ if (!fs.existsSync(dailyLogDir)) {
328
+ fs.mkdirSync(dailyLogDir, { recursive: true });
329
+ ok("daily-log/ (created)");
330
+ } else {
331
+ log(`${DIM}daily-log/ (kept)${RESET}`);
332
+ }
333
+ if (fs.existsSync(knowledgeSrc)) {
334
+ for (const file of fs.readdirSync(knowledgeSrc)) {
335
+ const src = path.join(knowledgeSrc, file);
336
+ const dest = path.join(knowledgeDest, file);
337
+ try {
338
+ if (fs.existsSync(dest)) {
339
+ log(`${DIM}${file} (kept — user has customized)${RESET}`);
340
+ } else {
341
+ copy(src, dest);
342
+ ok(`${file} (initialized)`);
343
+ }
344
+ } catch (e) {
345
+ warn(`${file} — ${e.message}`);
346
+ }
347
+ }
348
+ }
349
+
314
350
  // ─── References (methodology docs loaded by skills at runtime) ────
315
351
  printSection("References");
316
352
  const refDir = path.join(FRAMEWORK_DIR, "references");
@@ -374,6 +410,18 @@ async function main() {
374
410
  path.join(binDest, "statusline.js")
375
411
  );
376
412
  ok("statusline.js (status bar renderer)");
413
+ copy(
414
+ path.join(FRAMEWORK_DIR, "bin", "knowledge.js"),
415
+ path.join(binDest, "knowledge.js")
416
+ );
417
+ fs.chmodSync(path.join(binDest, "knowledge.js"), 0o755);
418
+ ok("knowledge.js (memory-layer loader)");
419
+ copy(
420
+ path.join(FRAMEWORK_DIR, "bin", "knowledge-flush.js"),
421
+ path.join(binDest, "knowledge-flush.js")
422
+ );
423
+ fs.chmodSync(path.join(binDest, "knowledge-flush.js"), 0o755);
424
+ ok("knowledge-flush.js (cron-runnable flush)");
377
425
  } catch (e) {
378
426
  warn(`scripts — ${e.message}`);
379
427
  }
@@ -562,6 +610,15 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
562
610
  CLAUDE_CODE_DISABLE_AUTO_MEMORY: "0",
563
611
  MAX_MCP_OUTPUT_TOKENS: "25000",
564
612
  CLAUDE_CODE_NO_FLICKER: "1",
613
+ // v4.2.0 phase 3 — enable forked subagents (Anthropic, 2026-04).
614
+ // Forks inherit the full conversation history + share the prompt cache,
615
+ // so design fan-outs and discuss-context handoffs preserve nuance instead
616
+ // of compressing 50k tokens of taste discussion into a 2k subagent prompt.
617
+ // /qualia-design and the builder agent reach for /fork when discuss
618
+ // context exists in the current session; verifier and plan-checker still
619
+ // use blank-context spawns to avoid the "kid grading their own homework"
620
+ // failure mode.
621
+ CLAUDE_AGENT_FORK_ENABLED: "1",
565
622
  });
566
623
 
567
624
  // Status line
@@ -613,6 +670,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
613
670
  const QUALIA_HOOK_SET = new Set([
614
671
  "session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
615
672
  "pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
673
+ "git-guardrails.js", "stop-session-log.js",
616
674
  ]);
617
675
  const isQualiaHookCmd = (cmd) => {
618
676
  if (typeof cmd !== "string") return false;
@@ -635,6 +693,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
635
693
  matcher: "Bash",
636
694
  hooks: [
637
695
  { type: "command", command: nodeCmd("auto-update.js"), timeout: 5 },
696
+ { type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "⬢ Checking git safety..." },
638
697
  { type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
639
698
  { type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
640
699
  { type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
@@ -655,6 +714,14 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
655
714
  ],
656
715
  },
657
716
  ],
717
+ Stop: [
718
+ {
719
+ matcher: ".*",
720
+ hooks: [
721
+ { type: "command", command: nodeCmd("stop-session-log.js"), timeout: 5 },
722
+ ],
723
+ },
724
+ ],
658
725
  };
659
726
 
660
727
  // Merge user hooks: strip Qualia-owned commands, preserve everything else.
@@ -692,7 +759,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
692
759
 
693
760
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
694
761
 
695
- ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact");
762
+ ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact, git-guardrails, stop-session-log");
696
763
  ok("Status line + spinner configured");
697
764
  ok("Environment variables + permissions");
698
765
 
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/bin/knowledge-flush.js — non-interactive memory-layer flush.
3
+ //
4
+ // Wraps `/qualia-flush` so it can run from cron (or systemd timer, or any
5
+ // CI/scheduled job) without an interactive Claude Code session. Closes the
6
+ // memory loop end-to-end:
7
+ //
8
+ // Stop hook (auto, every turn) → ~/.claude/knowledge/daily-log/{date}.md
9
+ // THIS SCRIPT (weekly cron) → spawns `claude -p "/qualia-flush --days 7"`
10
+ // /qualia-flush → promotes raw → curated tier
11
+ // bin/knowledge.js (every spawn) → reads index.md → reaches the right file
12
+ //
13
+ // Usage:
14
+ // node ~/.claude/bin/knowledge-flush.js # 7-day flush
15
+ // node ~/.claude/bin/knowledge-flush.js --days 14 # custom window
16
+ // node ~/.claude/bin/knowledge-flush.js --dry-run # preview only
17
+ // node ~/.claude/bin/knowledge-flush.js --project X # scope to one project
18
+ //
19
+ // Recommended cron entry (weekly Sunday 3 AM local):
20
+ // 0 3 * * 0 node ~/.claude/bin/knowledge-flush.js >> ~/.claude/.qualia-flush.log 2>&1
21
+ //
22
+ // Behavior:
23
+ // • If `claude` CLI isn't on PATH, exits 0 with a logged warning. Cron
24
+ // spam is worse than a missed flush — a real failure surfaces in the
25
+ // log file the user is presumably watching.
26
+ // • If the daily-log dir is empty (nothing to flush), exits 0 silently.
27
+ // • If `claude -p` returns non-zero, exits 1 with the error captured in
28
+ // the log so cron can be configured to alert on it.
29
+ // • Writes one structured JSONL line per run to ~/.claude/.qualia-flush.log
30
+ // so the user can audit "when did the last 5 flushes run, what did they
31
+ // produce?" without parsing free text.
32
+ //
33
+ // Cross-platform (Windows/macOS/Linux). Honors the same args as the skill.
34
+
35
+ const fs = require("fs");
36
+ const path = require("path");
37
+ const os = require("os");
38
+ const { spawnSync } = require("child_process");
39
+
40
+ const HOME = os.homedir();
41
+ const KNOWLEDGE_DIR = path.join(HOME, ".claude", "knowledge");
42
+ const DAILY_DIR = path.join(KNOWLEDGE_DIR, "daily-log");
43
+ const LOG_FILE = path.join(HOME, ".claude", ".qualia-flush.log");
44
+
45
+ const _start = Date.now();
46
+
47
+ function logEvent(event) {
48
+ try {
49
+ const dir = path.dirname(LOG_FILE);
50
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
51
+ const line = JSON.stringify({
52
+ timestamp: new Date().toISOString(),
53
+ duration_ms: Date.now() - _start,
54
+ ...event,
55
+ });
56
+ fs.appendFileSync(LOG_FILE, line + "\n");
57
+ } catch {}
58
+ }
59
+
60
+ function which(cmd) {
61
+ // Cross-platform `which`. Returns the first PATH match or null.
62
+ // We don't shell out to `which` itself because it doesn't exist on Windows
63
+ // (it's `where` there, with different semantics).
64
+ const sep = process.platform === "win32" ? ";" : ":";
65
+ const exts = process.platform === "win32"
66
+ ? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";")
67
+ : [""];
68
+ const dirs = (process.env.PATH || "").split(sep);
69
+ for (const dir of dirs) {
70
+ if (!dir) continue;
71
+ for (const ext of exts) {
72
+ const candidate = path.join(dir, cmd + ext);
73
+ try {
74
+ if (fs.existsSync(candidate)) return candidate;
75
+ } catch {}
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ // Pass-through args (so `--days 14`, `--dry-run`, `--project X` all reach the
82
+ // skill). We don't parse them ourselves — the skill is the source of truth
83
+ // for argument semantics. We only use `--days` locally to short-circuit when
84
+ // the daily-log is genuinely empty.
85
+ const argv = process.argv.slice(2);
86
+ const flagIdx = argv.indexOf("--days");
87
+ const days = flagIdx >= 0 ? parseInt(argv[flagIdx + 1], 10) || 7 : 7;
88
+
89
+ function dailyLogHasRecentEntries(windowDays) {
90
+ if (!fs.existsSync(DAILY_DIR)) return false;
91
+ const cutoff = new Date();
92
+ cutoff.setDate(cutoff.getDate() - windowDays);
93
+ const cutoffStr = cutoff.toISOString().split("T")[0];
94
+ try {
95
+ const entries = fs.readdirSync(DAILY_DIR);
96
+ for (const f of entries) {
97
+ if (!f.endsWith(".md")) continue;
98
+ const base = f.replace(/\.md$/, "");
99
+ if (base >= cutoffStr) return true;
100
+ }
101
+ } catch {}
102
+ return false;
103
+ }
104
+
105
+ // ── Preflight ────────────────────────────────────────────
106
+ const claudeBin = which("claude");
107
+ if (!claudeBin) {
108
+ logEvent({ event: "skipped", reason: "claude-cli-not-on-path" });
109
+ // Exit 0 — a missing CLI on the host running cron is a config issue, not
110
+ // a flush failure. Don't spam alerts.
111
+ process.exit(0);
112
+ }
113
+
114
+ if (!dailyLogHasRecentEntries(days)) {
115
+ logEvent({ event: "skipped", reason: "no-recent-daily-log", window_days: days });
116
+ process.exit(0);
117
+ }
118
+
119
+ // ── Run ──────────────────────────────────────────────────
120
+ // `claude -p "<prompt>"` runs a single non-interactive turn. The skill body
121
+ // invocation matches what the user would type at the prompt.
122
+ const prompt = `/qualia-flush ${argv.join(" ")}`.trim();
123
+
124
+ const result = spawnSync(claudeBin, ["-p", prompt], {
125
+ encoding: "utf8",
126
+ timeout: 5 * 60 * 1000, // 5 min hard cap — flush should never take this long
127
+ shell: process.platform === "win32",
128
+ stdio: ["ignore", "pipe", "pipe"],
129
+ });
130
+
131
+ const stdout = (result.stdout || "").trim();
132
+ const stderr = (result.stderr || "").trim();
133
+ const status = typeof result.status === "number" ? result.status : -1;
134
+
135
+ if (status !== 0) {
136
+ logEvent({
137
+ event: "failed",
138
+ status,
139
+ prompt,
140
+ stderr_tail: stderr.slice(-1000),
141
+ });
142
+ // Surface to stderr so cron's MAILTO sends an alert.
143
+ console.error(`knowledge-flush: claude -p exited ${status}`);
144
+ if (stderr) console.error(stderr.slice(-2000));
145
+ process.exit(1);
146
+ }
147
+
148
+ // Success: parse stdout for the skill's summary line if present, else log
149
+ // the full output tail.
150
+ const summaryMatch = stdout.match(/⬢ Flushed daily-log .+/);
151
+ logEvent({
152
+ event: "ok",
153
+ prompt,
154
+ summary: summaryMatch ? summaryMatch[0] : stdout.split("\n").slice(-3).join(" | "),
155
+ });
156
+
157
+ // Echo the user-facing summary to stdout so cron logs / interactive runs
158
+ // both surface what happened.
159
+ if (summaryMatch) {
160
+ console.log(summaryMatch[0]);
161
+ } else {
162
+ console.log(stdout.split("\n").slice(-5).join("\n"));
163
+ }
164
+ process.exit(0);