qualia-framework 4.1.1 → 4.4.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.
Files changed (43) hide show
  1. package/README.md +15 -11
  2. package/agents/builder.md +28 -0
  3. package/agents/research-synthesizer.md +7 -0
  4. package/bin/agent-runs.js +233 -0
  5. package/bin/cli.js +355 -16
  6. package/bin/install.js +87 -6
  7. package/bin/knowledge-flush.js +164 -0
  8. package/bin/knowledge.js +317 -0
  9. package/bin/plan-contract.js +220 -0
  10. package/bin/state.js +15 -9
  11. package/docs/agent-runs.md +273 -0
  12. package/docs/journey-demo.html +1008 -0
  13. package/docs/plan-contract.md +321 -0
  14. package/docs/reviews/v4.1.0-audit.html +1488 -0
  15. package/docs/reviews/v4.1.0-audit.md +263 -0
  16. package/hooks/auto-update.js +3 -7
  17. package/hooks/git-guardrails.js +167 -0
  18. package/hooks/pre-compact.js +22 -11
  19. package/hooks/pre-deploy-gate.js +16 -2
  20. package/hooks/pre-push.js +22 -2
  21. package/hooks/stop-session-log.js +180 -0
  22. package/package.json +8 -2
  23. package/skills/qualia-build/SKILL.md +5 -5
  24. package/skills/qualia-debug/SKILL.md +1 -1
  25. package/skills/qualia-design/SKILL.md +15 -0
  26. package/skills/qualia-flush/SKILL.md +200 -0
  27. package/skills/qualia-learn/SKILL.md +47 -37
  28. package/skills/qualia-new/SKILL.md +1 -1
  29. package/skills/qualia-plan/SKILL.md +3 -2
  30. package/skills/qualia-postmortem/SKILL.md +238 -0
  31. package/skills/qualia-quick/SKILL.md +1 -1
  32. package/skills/qualia-report/SKILL.md +1 -1
  33. package/skills/qualia-review/SKILL.md +3 -2
  34. package/skills/qualia-ship/SKILL.md +12 -10
  35. package/skills/qualia-verify/SKILL.md +60 -0
  36. package/templates/help.html +13 -7
  37. package/templates/knowledge/agents.md +71 -0
  38. package/templates/knowledge/index.md +47 -0
  39. package/tests/bin.test.sh +322 -12
  40. package/tests/hooks.test.sh +131 -20
  41. package/tests/lib.test.sh +217 -0
  42. package/tests/runner.js +103 -77
  43. package/tests/state.test.sh +4 -3
package/README.md CHANGED
@@ -115,13 +115,13 @@ Project
115
115
 
116
116
  **Why it matters:** non-technical team members can follow the ladder from any entry point. `/qualia` and `/qualia-milestone` render JOURNEY.md as a visual ladder with current position highlighted.
117
117
 
118
- ## What's Inside (v4.0.0)
118
+ ## What's Inside (v4.3.0)
119
119
 
120
- - **26 skills** — from setup to handoff, plus debug, design, review, optimize, diagnostic (`qualia-idk`), session management, skill authoring, per-phase depth (discuss, research, map), and full-journey additions (`--auto` chaining, milestone closure)
120
+ - **28 skills** — from setup to handoff, plus debug, design, review, optimize, diagnostic (`qualia-idk`), memory flush, postmortem, session management, skill authoring, per-phase depth (discuss, research, map), and full-journey additions (`--auto` chaining, milestone closure)
121
121
  - **8 agents** (each runs in fresh context): planner, builder, verifier, qa-browser, researcher, research-synthesizer, roadmapper, plan-checker
122
- - **7 hooks** (pure Node.js, cross-platform): session-start, branch-guard, pre-push tracking sync, migration-guard, pre-deploy-gate, pre-compact state save, auto-update
123
- - **5 rules**: security, frontend, design-reference, deployment, infrastructure
124
- - **19 template files**: project.md, **journey.md** (new in v4), plan.md (story-file format), state.md, DESIGN.md, tracking.json (now with `milestone_name` + `milestones[]`), requirements.md (multi-milestone), roadmap.md (current milestone only), phase-context.md, 4 project-type templates (website, ai-agent, voice-agent, mobile-app), 5 research-project templates (STACK, FEATURES, ARCHITECTURE, PITFALLS, SUMMARY), help.html
122
+ - **9 hooks** (pure Node.js, cross-platform): session-start, auto-update, git-guardrails, branch-guard, pre-push tracking sync, migration-guard, pre-deploy-gate, pre-compact state save, stop-session-log
123
+ - **6 rules**: security, frontend, design-reference, deployment, infrastructure, grounding
124
+ - **21 template files**: project.md, **journey.md** (new in v4), plan.md (story-file format), state.md, DESIGN.md, tracking.json (now with `milestone_name` + `milestones[]`), requirements.md (multi-milestone), roadmap.md (current milestone only), phase-context.md, 4 project-type templates (website, ai-agent, voice-agent, mobile-app), 5 research-project templates (STACK, FEATURES, ARCHITECTURE, PITFALLS, SUMMARY), knowledge templates, help.html
125
125
  - **1 reference** — questioning.md methodology for deep project initialization
126
126
 
127
127
  ## Supported Platforms
@@ -156,13 +156,17 @@ Splitting planner, builder, and verifier into separate agents with separate cont
156
156
 
157
157
  ### Production-Grade Hooks
158
158
 
159
- All 7 hooks are real ops engineering, not theoretical:
159
+ All 9 hooks are real ops engineering, not theoretical:
160
160
 
161
161
  - **Pre-deploy gate** — TypeScript, lint, tests, build, and `service_role` leak scan before `vercel --prod`
162
+ - **Session start** — Shows project state, next command, update notices, and health warnings at session start
163
+ - **Auto-update** — Daily update check with cached failures so offline/npm issues do not slow every command
164
+ - **Git guardrails** — Blocks destructive git operations like force-push to main/master, `git clean -fd`, and `rm -rf .git`
162
165
  - **Branch guard** — Role-aware: owner can push to main, employees can't (parses refspec so `feature/x:main` bypass is blocked)
163
166
  - **Migration guard** — Catches `DROP TABLE` without `IF EXISTS`, `DELETE`/`UPDATE` without `WHERE`, `CREATE TABLE` without RLS, `GRANT ... TO PUBLIC`, `ALTER TABLE ... DROP COLUMN`
164
167
  - **Pre-push** — Stamps tracking.json via a bot commit so the ERP always sees fresh data
165
168
  - **Pre-compact** — Saves state before context compression
169
+ - **Stop-session log** — Writes lightweight daily session checkpoints into the knowledge layer
166
170
 
167
171
  ### Enforced State Machine
168
172
 
@@ -183,12 +187,12 @@ npx qualia-framework@latest install
183
187
  |
184
188
  v
185
189
  ~/.claude/
186
- ├── skills/ 26 slash commands
190
+ ├── skills/ 28 slash commands
187
191
  ├── agents/ 8 agent definitions (planner, builder, verifier, qa-browser, roadmapper, research-synthesizer, researcher, plan-checker)
188
- ├── hooks/ 7 Node.js hooks — cross-platform (no bash dependency)
189
- ├── bin/ state.js (state machine) + qualia-ui.js (cosmetics, banners, journey-tree) + statusline.js
192
+ ├── hooks/ 9 Node.js hooks — cross-platform (no bash dependency)
193
+ ├── bin/ state.js + qualia-ui.js + statusline.js + knowledge.js + knowledge-flush.js
190
194
  ├── knowledge/ learned-patterns.md, common-fixes.md, client-prefs.md
191
- ├── rules/ security, frontend, design-reference, deployment, infrastructure
195
+ ├── rules/ security, frontend, design-reference, deployment, infrastructure, grounding
192
196
  ├── qualia-templates/ project.md, journey.md, plan.md (story-file), state.md, DESIGN.md, tracking.json, requirements.md, roadmap.md, + projects/*.md + research-project/*.md + help.html
193
197
  ├── qualia-references/ questioning.md (deep project initialization methodology)
194
198
  ├── CLAUDE.md global instructions (role-configured per team member)
@@ -201,6 +205,6 @@ Stack: Next.js 16+, React 19, TypeScript, Supabase, Vercel. Voice: Retell AI, El
201
205
 
202
206
  ## Changelog
203
207
 
204
- See [CHANGELOG.md](./CHANGELOG.md) for the full version history. v4.0.0 release notes are the most recent section.
208
+ See [CHANGELOG.md](./CHANGELOG.md) for the full version history. v4.3.0 release notes are the most recent section.
205
209
 
206
210
  Built by [Qualia Solutions](https://qualiasolutions.net) — Nicosia, Cyprus.
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.
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ // Agent runs telemetry — JSONL writer + reader. See docs/agent-runs.md.
3
+ //
4
+ // Pure library. Atomic writes via fs.appendFileSync (single write() syscall
5
+ // to an O_APPEND file descriptor; safe at our record sizes — see the spec).
6
+ //
7
+ // Zero npm dependencies.
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const crypto = require("crypto");
12
+
13
+ const SCHEMA_VERSION = 1;
14
+
15
+ const VALID_AGENT_TYPES = new Set([
16
+ "planner", "plan-checker", "builder", "verifier", "qa-browser",
17
+ "researcher", "research-synthesizer", "roadmapper", "team-orchestrator",
18
+ "custom",
19
+ ]);
20
+
21
+ const VALID_STATUS = new Set([
22
+ "success", "partial", "blocked", "failure", "timeout", "interrupted",
23
+ ]);
24
+
25
+ // One UUID per process — fallback when Claude Code doesn't expose a session id.
26
+ let _processSessionId = null;
27
+ function processSessionId() {
28
+ if (!_processSessionId) {
29
+ const buf = crypto.randomBytes(16);
30
+ // RFC 4122 v4
31
+ buf[6] = (buf[6] & 0x0f) | 0x40;
32
+ buf[8] = (buf[8] & 0x3f) | 0x80;
33
+ const h = buf.toString("hex");
34
+ _processSessionId = `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`;
35
+ }
36
+ return _processSessionId;
37
+ }
38
+
39
+ // ULID-ish: timestamp prefix + random suffix. Sortable by time.
40
+ function newRunId() {
41
+ const ts = Date.now().toString(36).toUpperCase().padStart(10, "0");
42
+ const rand = crypto.randomBytes(10).toString("hex").toUpperCase();
43
+ return `${ts}${rand}`.slice(0, 26);
44
+ }
45
+
46
+ function planningDir(cwd) {
47
+ return path.join(cwd || process.cwd(), ".planning");
48
+ }
49
+
50
+ function jsonlPath(cwd) {
51
+ return path.join(planningDir(cwd), "agent-runs.jsonl");
52
+ }
53
+
54
+ function logDir(cwd) {
55
+ return path.join(planningDir(cwd), "agent-runs");
56
+ }
57
+
58
+ function telemetryEnabled() {
59
+ return (process.env.QUALIA_TELEMETRY || "").toLowerCase() !== "off";
60
+ }
61
+
62
+ function ensureDir(p) {
63
+ if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
64
+ }
65
+
66
+ function truncTail(s, max) {
67
+ if (typeof s !== "string") return undefined;
68
+ if (s.length <= max) return s;
69
+ return s.slice(s.length - max);
70
+ }
71
+
72
+ // ─── Writer ────────────────────────────────────────────────────────────
73
+
74
+ // start({ agent_type, model, ... }) → opaque token used by finish()
75
+ function start(opts) {
76
+ const now = new Date().toISOString();
77
+ const token = {
78
+ started_at: now,
79
+ started_ms: Date.now(),
80
+ record: {
81
+ schema_version: SCHEMA_VERSION,
82
+ run_id: opts.run_id || newRunId(),
83
+ parent_run_id: opts.parent_run_id || undefined,
84
+ skill_invocation_id: opts.skill_invocation_id || processSessionId(),
85
+ session_id: opts.session_id || processSessionId(),
86
+ agent_type: opts.agent_type,
87
+ agent_name: opts.agent_name || undefined,
88
+ model: opts.model,
89
+ effort: opts.effort || undefined,
90
+ project: opts.project || undefined,
91
+ phase: opts.phase != null ? opts.phase : undefined,
92
+ milestone: opts.milestone != null ? opts.milestone : undefined,
93
+ task_id: opts.task_id || undefined,
94
+ wave: opts.wave != null ? opts.wave : undefined,
95
+ retry_of: opts.retry_of || undefined,
96
+ started_at: now,
97
+ },
98
+ };
99
+ return token;
100
+ }
101
+
102
+ // finish(token, { status, ... }) → writes the JSONL line + optional log file
103
+ function finish(token, result) {
104
+ if (!token || !token.record) throw new Error("finish: invalid token");
105
+ if (!telemetryEnabled()) return { written: false, reason: "telemetry-off" };
106
+
107
+ const cwd = result.cwd || process.cwd();
108
+ if (!fs.existsSync(planningDir(cwd))) {
109
+ return { written: false, reason: "no-planning-dir" };
110
+ }
111
+
112
+ const finishedMs = Date.now();
113
+ const record = {
114
+ ...token.record,
115
+ status: result.status,
116
+ started_at: token.record.started_at,
117
+ finished_at: new Date(finishedMs).toISOString(),
118
+ duration_ms: finishedMs - token.started_ms,
119
+ input_tokens: result.input_tokens,
120
+ output_tokens: result.output_tokens,
121
+ cache_read_tokens: result.cache_read_tokens,
122
+ cache_creation_tokens: result.cache_creation_tokens,
123
+ tool_calls_count: result.tool_calls_count,
124
+ files_changed: Array.isArray(result.files_changed) ? [...new Set(result.files_changed)] : undefined,
125
+ commit_sha: result.commit_sha || undefined,
126
+ verifier_score: result.verifier_score,
127
+ verification_result: result.verification_result,
128
+ failure_reason: result.failure_reason,
129
+ failure_detail: truncTail(result.failure_detail, 500),
130
+ };
131
+
132
+ if (!VALID_AGENT_TYPES.has(record.agent_type)) {
133
+ record.failure_reason = record.failure_reason || "unknown";
134
+ // don't reject — we want the trace even if the caller misnamed itself
135
+ }
136
+ if (!VALID_STATUS.has(record.status)) {
137
+ record.status = "failure";
138
+ record.failure_reason = record.failure_reason || "unknown";
139
+ }
140
+
141
+ // Side log for non-success runs.
142
+ if (record.status !== "success" && typeof result.full_stderr === "string" && result.full_stderr.length) {
143
+ try {
144
+ ensureDir(logDir(cwd));
145
+ const logFile = path.join(logDir(cwd), `${record.run_id}.log`);
146
+ fs.writeFileSync(logFile, result.full_stderr);
147
+ record.log_file = path.relative(cwd, logFile).split(path.sep).join("/");
148
+ } catch {
149
+ // Side-log is best-effort — never block the JSONL write.
150
+ }
151
+ }
152
+
153
+ // Drop undefined keys for a compact line.
154
+ const clean = {};
155
+ for (const [k, v] of Object.entries(record)) if (v !== undefined) clean[k] = v;
156
+
157
+ const line = JSON.stringify(clean) + "\n";
158
+ ensureDir(planningDir(cwd));
159
+ fs.appendFileSync(jsonlPath(cwd), line);
160
+ return { written: true, run_id: record.run_id, log_file: record.log_file };
161
+ }
162
+
163
+ // ─── Reader ────────────────────────────────────────────────────────────
164
+
165
+ function read(cwd, opts) {
166
+ const file = jsonlPath(cwd);
167
+ if (!fs.existsSync(file)) return [];
168
+ const lines = fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean);
169
+ const records = [];
170
+ for (const line of lines) {
171
+ try { records.push(JSON.parse(line)); }
172
+ catch { /* skip corrupt line; we never want a single bad record to mask the rest */ }
173
+ }
174
+ let out = records;
175
+ if (opts && opts.failed) {
176
+ out = out.filter((r) => r.status !== "success");
177
+ }
178
+ if (opts && opts.task_id) {
179
+ out = out.filter((r) => r.task_id === opts.task_id);
180
+ }
181
+ if (opts && opts.phase != null) {
182
+ out = out.filter((r) => r.phase === opts.phase);
183
+ }
184
+ if (opts && opts.limit) {
185
+ out = out.slice(-opts.limit);
186
+ }
187
+ return out;
188
+ }
189
+
190
+ function prune(cwd, beforeIso) {
191
+ const file = jsonlPath(cwd);
192
+ if (!fs.existsSync(file)) return { removed: 0, logs_removed: 0 };
193
+ const cutoff = Date.parse(beforeIso);
194
+ if (!Number.isFinite(cutoff)) throw new Error(`prune: invalid date "${beforeIso}"`);
195
+
196
+ const lines = fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean);
197
+ const kept = [];
198
+ const removedRunIds = [];
199
+ for (const line of lines) {
200
+ let rec;
201
+ try { rec = JSON.parse(line); }
202
+ catch { kept.push(line); continue; } // preserve unparseable; never destroy data we don't understand
203
+ const ts = Date.parse(rec.finished_at || rec.started_at || "");
204
+ if (Number.isFinite(ts) && ts < cutoff) {
205
+ removedRunIds.push(rec.run_id);
206
+ } else {
207
+ kept.push(line);
208
+ }
209
+ }
210
+ fs.writeFileSync(file, kept.join("\n") + (kept.length ? "\n" : ""));
211
+
212
+ let logsRemoved = 0;
213
+ if (fs.existsSync(logDir(cwd))) {
214
+ for (const id of removedRunIds) {
215
+ const lf = path.join(logDir(cwd), `${id}.log`);
216
+ try { fs.unlinkSync(lf); logsRemoved++; } catch {}
217
+ }
218
+ }
219
+ return { removed: removedRunIds.length, logs_removed: logsRemoved };
220
+ }
221
+
222
+ module.exports = {
223
+ SCHEMA_VERSION,
224
+ start,
225
+ finish,
226
+ read,
227
+ prune,
228
+ // exposed for tests / introspection
229
+ newRunId,
230
+ processSessionId,
231
+ jsonlPath,
232
+ logDir,
233
+ };