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.
- package/README.md +15 -11
- package/agents/builder.md +28 -0
- package/agents/research-synthesizer.md +7 -0
- package/bin/agent-runs.js +233 -0
- package/bin/cli.js +355 -16
- package/bin/install.js +87 -6
- package/bin/knowledge-flush.js +164 -0
- package/bin/knowledge.js +317 -0
- package/bin/plan-contract.js +220 -0
- package/bin/state.js +15 -9
- package/docs/agent-runs.md +273 -0
- package/docs/journey-demo.html +1008 -0
- package/docs/plan-contract.md +321 -0
- package/docs/reviews/v4.1.0-audit.html +1488 -0
- package/docs/reviews/v4.1.0-audit.md +263 -0
- package/hooks/auto-update.js +3 -7
- package/hooks/git-guardrails.js +167 -0
- package/hooks/pre-compact.js +22 -11
- package/hooks/pre-deploy-gate.js +16 -2
- package/hooks/pre-push.js +22 -2
- package/hooks/stop-session-log.js +180 -0
- package/package.json +8 -2
- package/skills/qualia-build/SKILL.md +5 -5
- 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-quick/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +1 -1
- package/skills/qualia-review/SKILL.md +3 -2
- package/skills/qualia-ship/SKILL.md +12 -10
- package/skills/qualia-verify/SKILL.md +60 -0
- package/templates/help.html +13 -7
- package/templates/knowledge/agents.md +71 -0
- package/templates/knowledge/index.md +47 -0
- package/tests/bin.test.sh +322 -12
- package/tests/hooks.test.sh +131 -20
- package/tests/lib.test.sh +217 -0
- package/tests/runner.js +103 -77
- 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.
|
|
118
|
+
## What's Inside (v4.3.0)
|
|
119
119
|
|
|
120
|
-
- **
|
|
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
|
-
- **
|
|
123
|
-
- **
|
|
124
|
-
- **
|
|
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
|
|
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/
|
|
190
|
+
├── skills/ 28 slash commands
|
|
187
191
|
├── agents/ 8 agent definitions (planner, builder, verifier, qa-browser, roadmapper, research-synthesizer, researcher, plan-checker)
|
|
188
|
-
├── hooks/
|
|
189
|
-
├── bin/ state.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.
|
|
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
|
+
};
|