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/bin/knowledge.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ~/.claude/bin/knowledge.js — unified loader for the memory layer.
|
|
3
|
+
//
|
|
4
|
+
// Replaces ad-hoc `cat ~/.claude/knowledge/X.md` calls scattered across
|
|
5
|
+
// skills. One entry point, deterministic output, every command exits 0 even
|
|
6
|
+
// when the requested file is missing (prints "(no entries)" to stdout) so
|
|
7
|
+
// skills can pipe the output into prompts without breaking on a fresh install.
|
|
8
|
+
//
|
|
9
|
+
// Why this exists (v4.1.0 audit finding #3):
|
|
10
|
+
// Skills hardcode `cat ~/.claude/knowledge/common-fixes.md`. New knowledge
|
|
11
|
+
// files are dead weight — the agent never sees them because the skill never
|
|
12
|
+
// references them by name. This loader gives skills ONE call (`knowledge.js`
|
|
13
|
+
// with no args prints index.md, the entry point) and lets the agent navigate
|
|
14
|
+
// from there. New files reachable from the index get used automatically.
|
|
15
|
+
//
|
|
16
|
+
// Subcommands:
|
|
17
|
+
// knowledge.js → prints index.md (default)
|
|
18
|
+
// knowledge.js load <file> → prints knowledge/<file>
|
|
19
|
+
// knowledge.js list → lists all knowledge files
|
|
20
|
+
// knowledge.js search <query> → grep across all files
|
|
21
|
+
// knowledge.js append --type <pattern|fix|client> --title <T> --body <B>
|
|
22
|
+
// appends a formatted entry
|
|
23
|
+
// knowledge.js path [<file>] → prints absolute path
|
|
24
|
+
//
|
|
25
|
+
// Cross-platform (Windows/macOS/Linux). No shell dependencies.
|
|
26
|
+
|
|
27
|
+
const fs = require("fs");
|
|
28
|
+
const path = require("path");
|
|
29
|
+
const os = require("os");
|
|
30
|
+
|
|
31
|
+
const KNOWLEDGE_DIR = path.join(os.homedir(), ".claude", "knowledge");
|
|
32
|
+
const INDEX_FILE = path.join(KNOWLEDGE_DIR, "index.md");
|
|
33
|
+
|
|
34
|
+
// Type → filename mapping for `append` and convenience aliases used by the
|
|
35
|
+
// existing `/qualia-learn` taxonomy. Keep this list short — every additional
|
|
36
|
+
// type needs a corresponding section in index.md.
|
|
37
|
+
const TYPE_TO_FILE = {
|
|
38
|
+
pattern: "learned-patterns.md",
|
|
39
|
+
patterns: "learned-patterns.md",
|
|
40
|
+
fix: "common-fixes.md",
|
|
41
|
+
fixes: "common-fixes.md",
|
|
42
|
+
client: "client-prefs.md",
|
|
43
|
+
"client-pref": "client-prefs.md",
|
|
44
|
+
"client-prefs": "client-prefs.md",
|
|
45
|
+
"client-preference": "client-prefs.md",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function ensureDir() {
|
|
49
|
+
if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readSafe(p) {
|
|
53
|
+
try {
|
|
54
|
+
return fs.readFileSync(p, "utf8");
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Look up a knowledge file by friendly name. Accepts: "index", "index.md",
|
|
61
|
+
// "patterns" (alias), "fixes" (alias), a bare filename, a subdirectory-
|
|
62
|
+
// qualified path like "concepts/stripe-checkout", or a name that exists
|
|
63
|
+
// inside a known subdirectory. Returns the resolved absolute path (may
|
|
64
|
+
// not exist on disk).
|
|
65
|
+
//
|
|
66
|
+
// Resolution order:
|
|
67
|
+
// 1. "index" / "index.md" → top-level index.md
|
|
68
|
+
// 2. Known type alias (pattern|fix|client) → mapped top-level filename
|
|
69
|
+
// 3. Path with "/" → treat as relative to knowledge dir (concepts/foo)
|
|
70
|
+
// 4. Bare name → look in top-level first; if missing, search known
|
|
71
|
+
// subdirectories (concepts/, daily-log/) for an exact match. This
|
|
72
|
+
// means /qualia-flush can write to concepts/voice-agent-call-state.md
|
|
73
|
+
// and skills can later run `knowledge.js load voice-agent-call-state`
|
|
74
|
+
// without knowing it lives in a subdirectory.
|
|
75
|
+
function resolveFile(name) {
|
|
76
|
+
if (!name || name === "index" || name === "index.md") return INDEX_FILE;
|
|
77
|
+
const lower = name.toLowerCase();
|
|
78
|
+
if (TYPE_TO_FILE[lower]) {
|
|
79
|
+
return path.join(KNOWLEDGE_DIR, TYPE_TO_FILE[lower]);
|
|
80
|
+
}
|
|
81
|
+
const withExt = name.endsWith(".md") ? name : `${name}.md`;
|
|
82
|
+
// Subdirectory-qualified path: concepts/foo, daily-log/2026-04-26
|
|
83
|
+
if (withExt.includes("/") || withExt.includes(path.sep)) {
|
|
84
|
+
return path.join(KNOWLEDGE_DIR, withExt);
|
|
85
|
+
}
|
|
86
|
+
// Top-level wins if it exists.
|
|
87
|
+
const topLevel = path.join(KNOWLEDGE_DIR, withExt);
|
|
88
|
+
if (fs.existsSync(topLevel)) return topLevel;
|
|
89
|
+
// Otherwise search known subdirectories. Stop at the first match — if
|
|
90
|
+
// multiple subdirs have the same filename, the user should qualify.
|
|
91
|
+
const KNOWN_SUBDIRS = ["concepts", "connections", "daily-log"];
|
|
92
|
+
for (const sub of KNOWN_SUBDIRS) {
|
|
93
|
+
const candidate = path.join(KNOWLEDGE_DIR, sub, withExt);
|
|
94
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
95
|
+
}
|
|
96
|
+
// Fall back to top-level (will trigger the "no entries" stub on read).
|
|
97
|
+
return topLevel;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function cmdLoad(arg) {
|
|
101
|
+
ensureDir();
|
|
102
|
+
const target = resolveFile(arg);
|
|
103
|
+
if (!fs.existsSync(target)) {
|
|
104
|
+
// Missing file → print a stub message, never fail. Skills can pipe this
|
|
105
|
+
// safely. The instruction line tells the agent what to do next.
|
|
106
|
+
const rel = path.relative(KNOWLEDGE_DIR, target) || "index.md";
|
|
107
|
+
console.log(`(no entries in ${rel} — use /qualia-learn to add one)`);
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
process.stdout.write(readSafe(target));
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function cmdList() {
|
|
115
|
+
ensureDir();
|
|
116
|
+
if (!fs.existsSync(KNOWLEDGE_DIR)) {
|
|
117
|
+
console.log("(knowledge layer not initialized — run: npx qualia-framework@latest install)");
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
const entries = [];
|
|
121
|
+
for (const f of fs.readdirSync(KNOWLEDGE_DIR)) {
|
|
122
|
+
const p = path.join(KNOWLEDGE_DIR, f);
|
|
123
|
+
let stat;
|
|
124
|
+
try { stat = fs.statSync(p); } catch { continue; }
|
|
125
|
+
if (stat.isDirectory()) {
|
|
126
|
+
// For daily-log/, count entries.
|
|
127
|
+
let count = 0;
|
|
128
|
+
try { count = fs.readdirSync(p).filter((x) => x.endsWith(".md")).length; } catch {}
|
|
129
|
+
entries.push({ name: `${f}/`, size: count, mtime: stat.mtimeMs, kind: "dir" });
|
|
130
|
+
} else if (f.endsWith(".md")) {
|
|
131
|
+
entries.push({ name: f, size: stat.size, mtime: stat.mtimeMs, kind: "file" });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
135
|
+
for (const e of entries) {
|
|
136
|
+
const sizeStr = e.kind === "dir" ? `${e.size} entries` : `${e.size}B`;
|
|
137
|
+
const date = new Date(e.mtime).toISOString().split("T")[0];
|
|
138
|
+
console.log(`${e.name.padEnd(28)} ${sizeStr.padEnd(14)} ${date}`);
|
|
139
|
+
}
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function cmdSearch(query) {
|
|
144
|
+
ensureDir();
|
|
145
|
+
if (!query) {
|
|
146
|
+
console.error("Usage: knowledge.js search <query>");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
if (!fs.existsSync(KNOWLEDGE_DIR)) {
|
|
150
|
+
console.log("(knowledge layer not initialized)");
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
const needle = query.toLowerCase();
|
|
154
|
+
const matches = [];
|
|
155
|
+
function walk(dir) {
|
|
156
|
+
for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
157
|
+
const p = path.join(dir, f.name);
|
|
158
|
+
if (f.isDirectory()) { walk(p); continue; }
|
|
159
|
+
if (!f.name.endsWith(".md")) continue;
|
|
160
|
+
const lines = readSafe(p).split("\n");
|
|
161
|
+
lines.forEach((line, idx) => {
|
|
162
|
+
if (line.toLowerCase().includes(needle)) {
|
|
163
|
+
const rel = path.relative(KNOWLEDGE_DIR, p);
|
|
164
|
+
matches.push(`${rel}:${idx + 1}: ${line.trim()}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
walk(KNOWLEDGE_DIR);
|
|
170
|
+
if (matches.length === 0) {
|
|
171
|
+
console.log(`(no matches for "${query}")`);
|
|
172
|
+
} else {
|
|
173
|
+
for (const m of matches) console.log(m);
|
|
174
|
+
}
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Parse minimal --flag value pairs from argv. Stops at the first positional
|
|
179
|
+
// argument, returns { flags: { type: "pattern", title: "X" }, rest: [...] }.
|
|
180
|
+
function parseFlags(argv) {
|
|
181
|
+
const flags = {};
|
|
182
|
+
const rest = [];
|
|
183
|
+
for (let i = 0; i < argv.length; i++) {
|
|
184
|
+
const a = argv[i];
|
|
185
|
+
if (a.startsWith("--")) {
|
|
186
|
+
const key = a.slice(2);
|
|
187
|
+
const next = argv[i + 1];
|
|
188
|
+
if (next && !next.startsWith("--")) {
|
|
189
|
+
flags[key] = next;
|
|
190
|
+
i++;
|
|
191
|
+
} else {
|
|
192
|
+
flags[key] = true;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
rest.push(a);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { flags, rest };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function cmdAppend(rawArgs) {
|
|
202
|
+
ensureDir();
|
|
203
|
+
const { flags } = parseFlags(rawArgs);
|
|
204
|
+
const type = String(flags.type || "").toLowerCase();
|
|
205
|
+
const title = String(flags.title || "").trim();
|
|
206
|
+
const body = String(flags.body || "").trim();
|
|
207
|
+
const project = String(flags.project || "general").trim();
|
|
208
|
+
const context = String(flags.context || "").trim();
|
|
209
|
+
|
|
210
|
+
if (!type || !TYPE_TO_FILE[type]) {
|
|
211
|
+
console.error(`append: --type must be one of: ${Object.keys(TYPE_TO_FILE).filter((k) => !k.includes("-")).join(", ")}`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
if (!title) {
|
|
215
|
+
console.error("append: --title is required");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
if (!body) {
|
|
219
|
+
console.error("append: --body is required");
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const dest = path.join(KNOWLEDGE_DIR, TYPE_TO_FILE[type]);
|
|
224
|
+
|
|
225
|
+
// 8-char hex id, ISO date.
|
|
226
|
+
const id = Math.floor(Math.random() * 0xffffffff).toString(16).padStart(8, "0");
|
|
227
|
+
const date = new Date().toISOString().split("T")[0];
|
|
228
|
+
const entry = [
|
|
229
|
+
"",
|
|
230
|
+
"---",
|
|
231
|
+
"",
|
|
232
|
+
`### ${title}`,
|
|
233
|
+
`**ID:** ${id}`,
|
|
234
|
+
`**Date:** ${date}`,
|
|
235
|
+
`**Project:** ${project}`,
|
|
236
|
+
context ? `**Context:** ${context}` : null,
|
|
237
|
+
"",
|
|
238
|
+
body,
|
|
239
|
+
"",
|
|
240
|
+
].filter((l) => l !== null).join("\n");
|
|
241
|
+
|
|
242
|
+
if (!fs.existsSync(dest)) {
|
|
243
|
+
const header = `# ${title.split(":")[0] || type} entries\n\nAuto-maintained by /qualia-learn (via bin/knowledge.js).\n`;
|
|
244
|
+
fs.writeFileSync(dest, header + entry + "\n");
|
|
245
|
+
} else {
|
|
246
|
+
fs.appendFileSync(dest, entry + "\n");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(`appended ${id} to ${path.basename(dest)}`);
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function cmdPath(arg) {
|
|
254
|
+
console.log(resolveFile(arg));
|
|
255
|
+
process.exit(0);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function cmdHelp() {
|
|
259
|
+
process.stdout.write(`knowledge.js — Qualia Framework memory-layer loader
|
|
260
|
+
|
|
261
|
+
Usage:
|
|
262
|
+
knowledge.js # print index.md (entry point)
|
|
263
|
+
knowledge.js load <file> # print a specific knowledge file
|
|
264
|
+
# accepts: index, patterns, fixes,
|
|
265
|
+
# client, supabase-patterns, etc.
|
|
266
|
+
knowledge.js list # list all files with size + mtime
|
|
267
|
+
knowledge.js search <query> # grep across all files
|
|
268
|
+
knowledge.js append --type <type> --title <T> --body <B> [--project <P>] [--context <C>]
|
|
269
|
+
# type: pattern | fix | client
|
|
270
|
+
knowledge.js path [<file>] # print absolute path (no read)
|
|
271
|
+
knowledge.js help # this message
|
|
272
|
+
|
|
273
|
+
Skills should ALWAYS go through this loader. New knowledge files reachable
|
|
274
|
+
from index.md become usable to every agent automatically. Hardcoded
|
|
275
|
+
\`cat ~/.claude/knowledge/X.md\` calls in skills are an anti-pattern (audit
|
|
276
|
+
finding #3 from the v4.1.0 review) — they make new files invisible.
|
|
277
|
+
`);
|
|
278
|
+
process.exit(0);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const cmd = process.argv[2];
|
|
282
|
+
const rest = process.argv.slice(3);
|
|
283
|
+
|
|
284
|
+
switch (cmd) {
|
|
285
|
+
case undefined:
|
|
286
|
+
case null:
|
|
287
|
+
cmdLoad(null);
|
|
288
|
+
break;
|
|
289
|
+
case "load":
|
|
290
|
+
cmdLoad(rest[0]);
|
|
291
|
+
break;
|
|
292
|
+
case "list":
|
|
293
|
+
case "ls":
|
|
294
|
+
cmdList();
|
|
295
|
+
break;
|
|
296
|
+
case "search":
|
|
297
|
+
case "grep":
|
|
298
|
+
cmdSearch(rest[0]);
|
|
299
|
+
break;
|
|
300
|
+
case "append":
|
|
301
|
+
case "add":
|
|
302
|
+
cmdAppend(rest);
|
|
303
|
+
break;
|
|
304
|
+
case "path":
|
|
305
|
+
case "which":
|
|
306
|
+
cmdPath(rest[0]);
|
|
307
|
+
break;
|
|
308
|
+
case "help":
|
|
309
|
+
case "-h":
|
|
310
|
+
case "--help":
|
|
311
|
+
cmdHelp();
|
|
312
|
+
break;
|
|
313
|
+
default:
|
|
314
|
+
// Unknown command → fall through to load with that arg, so
|
|
315
|
+
// `knowledge.js patterns` works as shorthand for `knowledge.js load patterns`.
|
|
316
|
+
cmdLoad(cmd);
|
|
317
|
+
}
|