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 +28 -0
- package/agents/research-synthesizer.md +7 -0
- package/bin/cli.js +142 -2
- package/bin/install.js +68 -1
- package/bin/knowledge-flush.js +164 -0
- package/bin/knowledge.js +317 -0
- package/docs/journey-demo.html +1008 -0
- package/docs/reviews/v4.1.0-audit.html +1488 -0
- package/docs/reviews/v4.1.0-audit.md +263 -0
- package/hooks/git-guardrails.js +167 -0
- package/hooks/stop-session-log.js +180 -0
- package/package.json +1 -1
- package/skills/qualia-debug/SKILL.md +1 -1
- package/skills/qualia-design/SKILL.md +15 -0
- package/skills/qualia-flush/SKILL.md +200 -0
- package/skills/qualia-learn/SKILL.md +47 -37
- package/skills/qualia-new/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +3 -2
- package/skills/qualia-postmortem/SKILL.md +238 -0
- package/skills/qualia-review/SKILL.md +3 -2
- package/skills/qualia-verify/SKILL.md +60 -0
- package/templates/knowledge/agents.md +71 -0
- package/templates/knowledge/index.md +47 -0
- package/tests/bin.test.sh +316 -9
- package/tests/hooks.test.sh +122 -0
- package/tests/runner.js +7 -2
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);
|