qualia-framework 6.1.0 → 6.2.7
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 +35 -26
- package/agents/roadmapper.md +1 -1
- package/bin/cli.js +339 -200
- package/bin/erp-retry.js +11 -3
- package/bin/install.js +383 -55
- package/bin/knowledge-flush.js +25 -13
- package/bin/knowledge.js +11 -1
- package/bin/project-snapshot.js +293 -0
- package/bin/qualia-ui.js +13 -2
- package/bin/report-payload.js +137 -0
- package/bin/state.js +8 -1
- package/bin/statusline.js +14 -2
- package/docs/changelog-v6.html +864 -0
- package/docs/ecosystem-operating-model.md +121 -0
- package/docs/erp-contract.md +74 -21
- package/docs/onboarding.html +1 -1
- package/docs/release.md +44 -0
- package/docs/reviews/v6.2.1-revival-audit.md +53 -0
- package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
- package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
- package/guide.md +16 -4
- package/hooks/auto-update.js +14 -7
- package/hooks/branch-guard.js +10 -2
- package/hooks/env-empty-guard.js +10 -1
- package/hooks/git-guardrails.js +10 -1
- package/hooks/migration-guard.js +4 -1
- package/hooks/pre-deploy-gate.js +11 -1
- package/hooks/pre-push.js +42 -162
- package/hooks/session-start.js +22 -14
- package/hooks/stop-session-log.js +11 -3
- package/hooks/supabase-destructive-guard.js +11 -1
- package/hooks/vercel-account-guard.js +12 -3
- package/package.json +3 -2
- package/skills/qualia-map/SKILL.md +1 -1
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-polish/SKILL.md +2 -2
- package/skills/qualia-report/SKILL.md +6 -43
- package/skills/qualia-road/SKILL.md +1 -1
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/help.html +1 -1
- package/templates/knowledge/agents.md +3 -3
- package/templates/knowledge/index.md +1 -1
- package/templates/tracking.json +3 -0
- package/templates/work-packet.md +46 -0
- package/tests/bin.test.sh +411 -13
- package/tests/hooks.test.sh +1 -8
- package/tests/install-smoke.test.sh +137 -0
- package/tests/published-install-smoke.test.sh +126 -0
- package/tests/refs.test.sh +42 -0
- package/tests/run-all.sh +1 -0
- package/tests/runner.js +19 -33
- package/tests/state.test.sh +4 -1
- package/hooks/pre-compact.js +0 -127
package/hooks/pre-push.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// ~/.claude/hooks/pre-push.js — stamp tracking.json
|
|
3
|
-
// PreToolUse hook on `git push*` commands. The stamp is included in the push
|
|
4
|
-
// via a small bot commit (no-verify, bot author) so the ERP — which reads
|
|
5
|
-
// tracking.json straight from git — sees fresh data on every push.
|
|
2
|
+
// ~/.claude/hooks/pre-push.js — stamp .planning/tracking.json on push.
|
|
6
3
|
//
|
|
7
|
-
//
|
|
4
|
+
// Stamps last_commit + last_pushed_at locally on every `git push`. Does NOT
|
|
5
|
+
// commit, stage, or push tracking.json. The file is consumed by local readers
|
|
6
|
+
// only:
|
|
7
|
+
// - bin/statusline.js (renders last-push time)
|
|
8
|
+
// - hooks/stop-session-log.js (renders session-stop banner)
|
|
9
|
+
// - /qualia-report skill (reads stamps for the report payload)
|
|
10
|
+
// - bin/state.js (state machine)
|
|
11
|
+
//
|
|
12
|
+
// History rationale (v6.2 — 2026-05-20): pre-v6.2 versions of this hook wrote
|
|
13
|
+
// the stamp, `git add`-ed it, then created a bot commit (`chore(track): ERP
|
|
14
|
+
// sync …`) so a downstream consumer — the ERP at portal.qualiasolutions.net —
|
|
15
|
+
// could read tracking.json straight from GitHub. That consumer was documented
|
|
16
|
+
// (docs/erp-contract.md) but never actually implemented; the ERP repo had no
|
|
17
|
+
// route, cron, or worker that reads tracking.json. The bot commits were
|
|
18
|
+
// polluting every Qualia project's history (and on auto-deploy-enabled Vercel
|
|
19
|
+
// projects, triggering useless redeploys) for no benefit. v6.2 strips the
|
|
20
|
+
// commit entirely — the stamps still happen, just locally.
|
|
8
21
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the snapshot already prepared by Claude Code's tool dispatcher — so the
|
|
12
|
-
// stamp never made it onto the wire. This rewrite creates a real commit so
|
|
13
|
-
// the next `git push` it spawned by Claude Code includes it.
|
|
22
|
+
// PreToolUse hook on `git push*` commands. Never blocks the push.
|
|
23
|
+
// Cross-platform (Windows/macOS/Linux). No external dependencies.
|
|
14
24
|
|
|
15
25
|
const fs = require("fs");
|
|
16
26
|
const path = require("path");
|
|
@@ -18,67 +28,20 @@ const os = require("os");
|
|
|
18
28
|
const { spawnSync } = require("child_process");
|
|
19
29
|
|
|
20
30
|
const _traceStart = Date.now();
|
|
21
|
-
const HOME = os.homedir();
|
|
22
|
-
const TRACKING = path.join(".planning", "tracking.json");
|
|
23
|
-
const CONFIG = path.join(HOME, ".claude", ".qualia-config.json");
|
|
24
|
-
const BOT_AUTHOR = "Qualia Framework <bot@qualia.solutions>";
|
|
25
|
-
const SHELL = process.platform === "win32";
|
|
26
31
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
...opts,
|
|
33
|
-
});
|
|
32
|
+
function qualiaHome() {
|
|
33
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
34
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
35
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
36
|
+
return path.join(os.homedir(), ".claude");
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// bot commit that's now orphaned in the local branch. Next push (to a feature
|
|
40
|
-
// branch) silently ships the orphan. v5.9.2 fix.
|
|
41
|
-
function wouldBeBlocked() {
|
|
42
|
-
let role = "";
|
|
43
|
-
try {
|
|
44
|
-
const cfg = JSON.parse(fs.readFileSync(CONFIG, "utf8"));
|
|
45
|
-
role = cfg.role || "";
|
|
46
|
-
} catch {
|
|
47
|
-
return false; // branch-guard will fail-closed on its own
|
|
48
|
-
}
|
|
49
|
-
if (role === "OWNER") return false;
|
|
50
|
-
|
|
51
|
-
// Mirror branch-guard's stdin parse for refspec targets.
|
|
52
|
-
let pushCommand = "";
|
|
53
|
-
try {
|
|
54
|
-
const raw = fs.readFileSync(0, "utf8");
|
|
55
|
-
if (raw && raw.trim()) {
|
|
56
|
-
const payload = JSON.parse(raw);
|
|
57
|
-
pushCommand = (payload && payload.tool_input && payload.tool_input.command) || "";
|
|
58
|
-
}
|
|
59
|
-
} catch { /* no stdin */ }
|
|
60
|
-
|
|
61
|
-
if (pushCommand) {
|
|
62
|
-
const tokens = pushCommand.split(/\s+/).filter(Boolean);
|
|
63
|
-
const pushIdx = tokens.indexOf("push");
|
|
64
|
-
if (pushIdx !== -1) {
|
|
65
|
-
for (let i = pushIdx + 1; i < tokens.length; i++) {
|
|
66
|
-
let tok = tokens[i];
|
|
67
|
-
if (tok.startsWith("-")) continue;
|
|
68
|
-
if (tok.startsWith("+")) tok = tok.slice(1);
|
|
69
|
-
tok = tok.replace(/^['"]|['"]$/g, "");
|
|
70
|
-
if (tok.includes(":")) {
|
|
71
|
-
const dst = tok.split(":").pop().replace(/^refs\/heads\//, "");
|
|
72
|
-
if (dst === "main" || dst === "master") return true;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
39
|
+
const QUALIA_HOME = qualiaHome();
|
|
40
|
+
const TRACKING = path.join(".planning", "tracking.json");
|
|
41
|
+
const SHELL = process.platform === "win32";
|
|
77
42
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const branch = ((r.stdout || "").trim());
|
|
81
|
-
return branch === "main" || branch === "master";
|
|
43
|
+
function git(args) {
|
|
44
|
+
return spawnSync("git", args, { encoding: "utf8", timeout: 5000, shell: SHELL });
|
|
82
45
|
}
|
|
83
46
|
|
|
84
47
|
function atomicWrite(file, content) {
|
|
@@ -87,91 +50,31 @@ function atomicWrite(file, content) {
|
|
|
87
50
|
fs.renameSync(tmp, file);
|
|
88
51
|
}
|
|
89
52
|
|
|
90
|
-
function
|
|
91
|
-
const r = git(["rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
92
|
-
return r.status === 0 && (r.stdout || "").trim() === "true";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function commitStamp() {
|
|
53
|
+
function stampLocal() {
|
|
96
54
|
if (!fs.existsSync(TRACKING)) return { skipped: "no-tracking-file" };
|
|
97
|
-
if (!inGitRepo()) return { skipped: "not-a-git-repo" };
|
|
98
55
|
|
|
99
|
-
|
|
100
|
-
const head = git(["log", "--oneline", "-1", "--format=%h"]);
|
|
101
|
-
const lastCommit = ((head.stdout || "").trim());
|
|
102
|
-
const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
103
|
-
|
|
104
|
-
// Mutate tracking.json (atomic). Tolerate CRLF on Windows-edited files.
|
|
105
|
-
let raw, parsed;
|
|
56
|
+
let parsed;
|
|
106
57
|
try {
|
|
107
|
-
|
|
108
|
-
parsed = JSON.parse(raw);
|
|
58
|
+
parsed = JSON.parse(fs.readFileSync(TRACKING, "utf8"));
|
|
109
59
|
} catch (err) {
|
|
110
60
|
return { skipped: "tracking-unreadable", error: err.message };
|
|
111
61
|
}
|
|
112
62
|
|
|
113
|
-
const
|
|
63
|
+
const head = git(["log", "--oneline", "-1", "--format=%h"]);
|
|
64
|
+
const lastCommit = (head.stdout || "").trim();
|
|
65
|
+
const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
66
|
+
|
|
114
67
|
if (lastCommit) parsed.last_commit = lastCommit;
|
|
115
68
|
parsed.last_updated = now;
|
|
116
69
|
parsed.last_pushed_at = now;
|
|
117
|
-
const after = JSON.stringify({ last_commit: parsed.last_commit, last_updated: parsed.last_updated });
|
|
118
|
-
if (before === after) return { skipped: "no-change" };
|
|
119
70
|
|
|
120
71
|
atomicWrite(TRACKING, JSON.stringify(parsed, null, 2) + "\n");
|
|
121
|
-
|
|
122
|
-
// Commit so the stamp is part of the push that's about to happen.
|
|
123
|
-
// --no-verify: skip user pre-commit hooks (this is a bot commit).
|
|
124
|
-
// --no-gpg-sign: don't pop a signing prompt for a chore commit.
|
|
125
|
-
// --author: attribute to bot, not user.
|
|
126
|
-
// -c core.autocrlf=false: without this, Windows installs with autocrlf=true
|
|
127
|
-
// normalize the just-written LF-terminated JSON to CRLF in the index, so the
|
|
128
|
-
// diff against HEAD is empty, the commit fails, and we roll back the stamp.
|
|
129
|
-
// Forcing autocrlf=false for this one add/commit pair preserves the JSON as
|
|
130
|
-
// written and keeps the stamp consistent across platforms.
|
|
131
|
-
const add = git(["-c", "core.autocrlf=false", "add", TRACKING]);
|
|
132
|
-
if (add.status !== 0) return { skipped: "git-add-failed", error: add.stderr };
|
|
133
|
-
|
|
134
|
-
const commit = git([
|
|
135
|
-
"-c", "core.autocrlf=false",
|
|
136
|
-
"commit",
|
|
137
|
-
"--no-verify",
|
|
138
|
-
"--no-gpg-sign",
|
|
139
|
-
"--author", BOT_AUTHOR,
|
|
140
|
-
"-m", `chore(track): ERP sync ${now}`,
|
|
141
|
-
]);
|
|
142
|
-
if (commit.status !== 0) {
|
|
143
|
-
// Commit failed (e.g., empty diff because git's auto-CRLF normalized the
|
|
144
|
-
// only change to nothing, or branch is in a detached/conflicted state).
|
|
145
|
-
// Unstage tracking.json and restore the working tree copy so the user's
|
|
146
|
-
// next manual commit isn't polluted by our aborted stamp.
|
|
147
|
-
try { git(["reset", "HEAD", "--", TRACKING]); } catch {}
|
|
148
|
-
try { if (raw != null) atomicWrite(TRACKING, raw); } catch {}
|
|
149
|
-
return { skipped: "git-commit-failed", error: (commit.stderr || commit.stdout || "").trim() };
|
|
150
|
-
}
|
|
151
|
-
return { committed: true, sha: lastCommit, ts: now };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function shouldBlock(result) {
|
|
155
|
-
// v5.0: tracking.json sync failures are now WARN not BLOCK. Rationale:
|
|
156
|
-
// tracking.json is ERP metadata, not load-bearing for the push itself. A
|
|
157
|
-
// corrupt tracking.json should NEVER prevent a production hotfix. The
|
|
158
|
-
// self-service fix is `git checkout HEAD -- .planning/tracking.json` and
|
|
159
|
-
// gets surfaced in the warning message below.
|
|
160
|
-
// To restore old fail-closed behavior set QUALIA_BLOCK_ON_TRACKING_FAIL=1.
|
|
161
|
-
if (process.env.QUALIA_BLOCK_ON_TRACKING_FAIL === "1") {
|
|
162
|
-
const hardSkips = new Set([
|
|
163
|
-
"tracking-unreadable",
|
|
164
|
-
"git-add-failed",
|
|
165
|
-
"git-commit-failed",
|
|
166
|
-
]);
|
|
167
|
-
return result && hardSkips.has(result.skipped);
|
|
168
|
-
}
|
|
169
|
-
return false;
|
|
72
|
+
return { stamped: true, sha: lastCommit, ts: now };
|
|
170
73
|
}
|
|
171
74
|
|
|
172
75
|
function _trace(result, extra) {
|
|
173
76
|
try {
|
|
174
|
-
const traceDir = path.join(
|
|
77
|
+
const traceDir = path.join(QUALIA_HOME, ".qualia-traces");
|
|
175
78
|
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
176
79
|
const entry = {
|
|
177
80
|
hook: "pre-push",
|
|
@@ -186,34 +89,11 @@ function _trace(result, extra) {
|
|
|
186
89
|
}
|
|
187
90
|
|
|
188
91
|
try {
|
|
189
|
-
|
|
190
|
-
// bot commit. branch-guard runs in parallel and will exit 2 with the
|
|
191
|
-
// user-facing block message.
|
|
192
|
-
if (wouldBeBlocked()) {
|
|
193
|
-
_trace("skip", { reason: "would-be-blocked-by-branch-guard" });
|
|
194
|
-
process.exit(0);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const result = commitStamp();
|
|
198
|
-
if (shouldBlock(result)) {
|
|
199
|
-
const detail = result.error ? ` ${String(result.error).slice(0, 500)}` : "";
|
|
200
|
-
const msg = `BLOCKED: tracking.json sync failed (${result.skipped}). Fix before pushing.${detail}\nSelf-service fix: git checkout HEAD -- .planning/tracking.json`;
|
|
201
|
-
console.error(msg);
|
|
202
|
-
console.log(msg);
|
|
203
|
-
_trace("block", result);
|
|
204
|
-
process.exit(2);
|
|
205
|
-
}
|
|
206
|
-
// v5.0: warn-not-block on tracking.json failures. The push proceeds; the user
|
|
207
|
-
// sees a clear message and a self-service fix path. Production hotfixes are
|
|
208
|
-
// never blocked by ERP-metadata corruption.
|
|
209
|
-
if (result && (result.skipped === "tracking-unreadable" || result.skipped === "git-add-failed" || result.skipped === "git-commit-failed")) {
|
|
210
|
-
const detail = result.error ? ` (${String(result.error).slice(0, 200)})` : "";
|
|
211
|
-
console.error(`⚠ tracking.json sync failed: ${result.skipped}${detail}. Push proceeding. Self-service fix: git checkout HEAD -- .planning/tracking.json`);
|
|
212
|
-
}
|
|
92
|
+
const result = stampLocal();
|
|
213
93
|
_trace("allow", result);
|
|
214
94
|
} catch (err) {
|
|
215
|
-
//
|
|
216
|
-
console.error(`⚠ tracking.json
|
|
95
|
+
// Never block a push on a stamp failure — tracking.json is local telemetry.
|
|
96
|
+
console.error(`⚠ tracking.json stamp failed: ${err.message}. Push proceeding.`);
|
|
217
97
|
_trace("warn", { error: err.message });
|
|
218
98
|
}
|
|
219
99
|
|
package/hooks/session-start.js
CHANGED
|
@@ -21,24 +21,32 @@ const DIM = "\x1b[38;2;80;90;100m";
|
|
|
21
21
|
const RESET = "\x1b[0m";
|
|
22
22
|
|
|
23
23
|
const HOME = os.homedir();
|
|
24
|
-
|
|
24
|
+
function qualiaHome() {
|
|
25
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
26
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
27
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
28
|
+
return path.join(HOME, ".claude");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const QUALIA_HOME = qualiaHome();
|
|
32
|
+
const UI = path.join(QUALIA_HOME, "bin", "qualia-ui.js");
|
|
25
33
|
const STATE_FILE = path.join(".planning", "STATE.md");
|
|
26
34
|
const CONTINUE_HERE = ".continue-here.md";
|
|
27
|
-
const NOTIF_FILE = path.join(
|
|
28
|
-
const HEALTH_FILE = path.join(
|
|
29
|
-
const ERP_RETRY = path.join(
|
|
30
|
-
const ERP_QUEUE = path.join(
|
|
35
|
+
const NOTIF_FILE = path.join(QUALIA_HOME, ".qualia-update-available.json");
|
|
36
|
+
const HEALTH_FILE = path.join(QUALIA_HOME, ".qualia-install-health.json");
|
|
37
|
+
const ERP_RETRY = path.join(QUALIA_HOME, "bin", "erp-retry.js");
|
|
38
|
+
const ERP_QUEUE = path.join(QUALIA_HOME, ".erp-retry-queue.json");
|
|
31
39
|
|
|
32
40
|
// Critical files referenced by skills via @-import. If any are missing, skills
|
|
33
41
|
// silently get empty context and produce ungrounded output. We spot-check these
|
|
34
42
|
// on session-start and write a cached result (1-per-day) to HEALTH_FILE so we
|
|
35
43
|
// don't stat on every session.
|
|
36
44
|
const CRITICAL_FILES = [
|
|
37
|
-
path.join(
|
|
38
|
-
path.join(
|
|
39
|
-
path.join(
|
|
40
|
-
path.join(
|
|
41
|
-
path.join(
|
|
45
|
+
path.join(QUALIA_HOME, "rules", "grounding.md"),
|
|
46
|
+
path.join(QUALIA_HOME, "rules", "security.md"),
|
|
47
|
+
path.join(QUALIA_HOME, "rules", "deployment.md"),
|
|
48
|
+
path.join(QUALIA_HOME, "qualia-design", "frontend.md"),
|
|
49
|
+
path.join(QUALIA_HOME, "bin", "state.js"),
|
|
42
50
|
];
|
|
43
51
|
|
|
44
52
|
function checkInstallHealth() {
|
|
@@ -74,7 +82,7 @@ function runUi(...args) {
|
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
function getNextCommand() {
|
|
77
|
-
const stateJs = path.join(
|
|
85
|
+
const stateJs = path.join(QUALIA_HOME, "bin", "state.js");
|
|
78
86
|
if (!fs.existsSync(stateJs)) return "";
|
|
79
87
|
try {
|
|
80
88
|
const r = spawnSync(process.execPath, [stateJs, "check"], {
|
|
@@ -91,7 +99,7 @@ function getNextCommand() {
|
|
|
91
99
|
|
|
92
100
|
function readConfig() {
|
|
93
101
|
try {
|
|
94
|
-
return JSON.parse(fs.readFileSync(path.join(
|
|
102
|
+
return JSON.parse(fs.readFileSync(path.join(QUALIA_HOME, ".qualia-config.json"), "utf8"));
|
|
95
103
|
} catch {
|
|
96
104
|
return {};
|
|
97
105
|
}
|
|
@@ -198,7 +206,7 @@ try {
|
|
|
198
206
|
// Hook must never exit non-zero. Log to trace so silent crashes are visible
|
|
199
207
|
// in analytics, but do not print to stderr (would clutter the banner).
|
|
200
208
|
try {
|
|
201
|
-
const traceDir = path.join(
|
|
209
|
+
const traceDir = path.join(QUALIA_HOME, ".qualia-traces");
|
|
202
210
|
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
203
211
|
const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
|
|
204
212
|
fs.appendFileSync(
|
|
@@ -215,7 +223,7 @@ try {
|
|
|
215
223
|
|
|
216
224
|
function _trace(hookName, result, extra) {
|
|
217
225
|
try {
|
|
218
|
-
const traceDir = path.join(
|
|
226
|
+
const traceDir = path.join(QUALIA_HOME, ".qualia-traces");
|
|
219
227
|
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
220
228
|
const entry = {
|
|
221
229
|
hook: hookName,
|
|
@@ -31,14 +31,22 @@ const { spawnSync } = require("child_process");
|
|
|
31
31
|
|
|
32
32
|
const _traceStart = Date.now();
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
function qualiaHome() {
|
|
35
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
36
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
37
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
38
|
+
return path.join(os.homedir(), ".claude");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const QUALIA_HOME = qualiaHome();
|
|
42
|
+
const KNOWLEDGE_DIR = path.join(QUALIA_HOME, "knowledge");
|
|
35
43
|
const DAILY_DIR = path.join(KNOWLEDGE_DIR, "daily-log");
|
|
36
|
-
const LAST_WRITE_FILE = path.join(
|
|
44
|
+
const LAST_WRITE_FILE = path.join(QUALIA_HOME, ".qualia-last-session-log");
|
|
37
45
|
const MIN_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
38
46
|
|
|
39
47
|
function _trace(result, extra) {
|
|
40
48
|
try {
|
|
41
|
-
const traceDir = path.join(
|
|
49
|
+
const traceDir = path.join(QUALIA_HOME, ".qualia-traces");
|
|
42
50
|
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
43
51
|
fs.appendFileSync(
|
|
44
52
|
path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`),
|
|
@@ -11,9 +11,19 @@ const path = require("path");
|
|
|
11
11
|
const os = require("os");
|
|
12
12
|
|
|
13
13
|
const _traceStart = Date.now();
|
|
14
|
+
|
|
15
|
+
function qualiaHome() {
|
|
16
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
17
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
18
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
19
|
+
return path.join(os.homedir(), ".claude");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const QUALIA_HOME = qualiaHome();
|
|
23
|
+
|
|
14
24
|
function _trace(result, extra) {
|
|
15
25
|
try {
|
|
16
|
-
const traceDir = path.join(
|
|
26
|
+
const traceDir = path.join(QUALIA_HOME, ".qualia-traces");
|
|
17
27
|
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
18
28
|
const entry = {
|
|
19
29
|
hook: "supabase-destructive-guard", result,
|
|
@@ -17,9 +17,18 @@ const { spawnSync } = require("child_process");
|
|
|
17
17
|
|
|
18
18
|
const _traceStart = Date.now();
|
|
19
19
|
|
|
20
|
+
function qualiaHome() {
|
|
21
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
22
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
23
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
24
|
+
return path.join(os.homedir(), ".claude");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const QUALIA_HOME = qualiaHome();
|
|
28
|
+
|
|
20
29
|
function _trace(result, extra) {
|
|
21
30
|
try {
|
|
22
|
-
const traceDir = path.join(
|
|
31
|
+
const traceDir = path.join(QUALIA_HOME, ".qualia-traces");
|
|
23
32
|
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
24
33
|
const entry = {
|
|
25
34
|
hook: "vercel-account-guard",
|
|
@@ -54,7 +63,7 @@ if (!/vercel\s+(--prod|deploy)/.test(command)) {
|
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
// Read allowed teams (one slug per line).
|
|
57
|
-
const teamsFile = path.join(
|
|
66
|
+
const teamsFile = path.join(QUALIA_HOME, ".vercel-allowed-teams");
|
|
58
67
|
let allowed;
|
|
59
68
|
try {
|
|
60
69
|
allowed = fs.readFileSync(teamsFile, "utf8").split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
@@ -84,7 +93,7 @@ if (allowed.includes(actual)) {
|
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
// Block: wrong account.
|
|
87
|
-
const msg = `BLOCKED: Wrong Vercel account/team. Active: '${actual}'. Allowed: ${allowed.join(", ")}. Run \`vercel switch ${allowed[0]}\` first, or add this team to
|
|
96
|
+
const msg = `BLOCKED: Wrong Vercel account/team. Active: '${actual}'. Allowed: ${allowed.join(", ")}. Run \`vercel switch ${allowed[0]}\` first, or add this team to ${path.join(QUALIA_HOME, ".vercel-allowed-teams")}.`;
|
|
88
97
|
console.error(msg);
|
|
89
98
|
console.log(msg);
|
|
90
99
|
_trace("block", { actual, allowed_count: allowed.length, reason: "wrong-account" });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qualia-framework",
|
|
3
|
-
"version": "6.
|
|
4
|
-
"description": "Claude Code workflow framework by Qualia Solutions. Plan, build, verify, ship.",
|
|
3
|
+
"version": "6.2.7",
|
|
4
|
+
"description": "Claude Code and Codex workflow framework by Qualia Solutions. Plan, build, verify, ship.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"qualia-framework": "./bin/cli.js"
|
|
7
7
|
},
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"test:slop-detect": "bash tests/slop-detect.test.sh",
|
|
33
33
|
"test:statusline": "bash tests/statusline.test.sh",
|
|
34
34
|
"test:refs": "bash tests/refs.test.sh",
|
|
35
|
+
"test:published-install": "bash tests/published-install-smoke.test.sh",
|
|
35
36
|
"test:shell": "bash tests/run-all.sh"
|
|
36
37
|
},
|
|
37
38
|
"files": [
|
|
@@ -163,7 +163,7 @@ Read `.planning/codebase/onboarding.md`. Per detected dimension:
|
|
|
163
163
|
- Domain docs at non-default path → `.planning/agents/domain.md`
|
|
164
164
|
- CLAUDE.md/AGENTS.md found → APPEND `## Qualia substrate` (never overwrite)
|
|
165
165
|
|
|
166
|
-
"none" →
|
|
166
|
+
"none" → report that no preference was provided; defaults apply.
|
|
167
167
|
|
|
168
168
|
### 6. Commit
|
|
169
169
|
|
|
@@ -276,6 +276,6 @@ node ~/.claude/bin/qualia-ui.js end "M{N} CLOSED · M{N+1} OPEN" "/qualia-plan 1
|
|
|
276
276
|
2. **JOURNEY.md is the source of truth for next milestone.** Don't ask the user to name it unless JOURNEY.md is missing (legacy project).
|
|
277
277
|
3. **Archive, don't delete.** Old phase work stays accessible via `.planning/archive/`.
|
|
278
278
|
4. **New milestone = fresh phase numbering.** First phase of the new milestone is Phase 1, not Phase {N+1}.
|
|
279
|
-
5. **
|
|
279
|
+
5. **Report aware.** `tracking.json.milestones[]` gets a summary entry on close so local status and `/qualia-report` have milestone context.
|
|
280
280
|
6. **Handoff is the final milestone (full projects).** If the current milestone IS Handoff, there is no "next" — route to `/qualia-report` and the project is done.
|
|
281
281
|
7. **Demo closes via the demo-extension branch (Step 1b).** A demo's single milestone closing triggers the "client signed?" question. The framework never silently auto-extends a demo without asking — the conversion is a real business decision.
|
|
@@ -41,7 +41,7 @@ Modes: `full`, `perf`, `ui`, `backend`, `alignment`, `deepen`, `fix`.
|
|
|
41
41
|
|
|
42
42
|
### Step 2: Load Planning Context (MANDATORY)
|
|
43
43
|
|
|
44
|
-
Read all (
|
|
44
|
+
Read all (use the shown NO_* marker when missing):
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
cat .planning/PROJECT.md 2>/dev/null || echo "NO_PROJECT"
|
|
@@ -97,7 +97,7 @@ node bin/slop-detect.mjs {target_paths}
|
|
|
97
97
|
npx tsc --noEmit 2>&1 | head -20
|
|
98
98
|
|
|
99
99
|
# Token enforcement (if Stylelint configured)
|
|
100
|
-
npx stylelint "src/**/*.css" 2>&1 | head -20 # optional,
|
|
100
|
+
npx stylelint "src/**/*.css" 2>&1 | head -20 # optional, report if not configured
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
ANY critical-severity slop hit = mandatory fix before proceeding. Don't stage anything until critical findings are zero.
|
|
@@ -150,7 +150,7 @@ npx lhci autorun \
|
|
|
150
150
|
npx @axe-core/cli http://localhost:3000{route} --exit 2>&1 | tail -30
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
-
If Lighthouse/axe not installed,
|
|
153
|
+
If Lighthouse/axe are not installed, continue but note the missing tooling in the report. Don't fail the polish over optional local tooling, and don't hide the skipped gate.
|
|
154
154
|
|
|
155
155
|
If a11y < 90 OR axe critical/serious violations: **fix programmatically** (these are deterministic — no vision needed). Re-run to confirm.
|
|
156
156
|
|
|
@@ -126,7 +126,7 @@ fi
|
|
|
126
126
|
### Step 6 — Upload to ERP
|
|
127
127
|
|
|
128
128
|
The full payload-builder + 3-attempt-retry logic lives unchanged from v4 — see the **ERP Upload** section below for the canonical implementation. Behavior summary:
|
|
129
|
-
- ERP disabled in config → skip
|
|
129
|
+
- ERP disabled in config → skip upload with an info line, note local commit
|
|
130
130
|
- API key missing → warn with self-service fix instructions, skip upload
|
|
131
131
|
- 401/422 → permanent failure, no retry, tell employee to ask Fawzi
|
|
132
132
|
- Transient (timeout/5xx) → 3 attempts with 1s/3s/9s backoff
|
|
@@ -149,7 +149,7 @@ node ~/.claude/bin/qualia-ui.js info "Shift report submitted. You can clock out
|
|
|
149
149
|
| Symptom | Likely cause | Self-service fix |
|
|
150
150
|
|---|---|---|
|
|
151
151
|
| "Could not allocate report ID" | tracking.json missing/corrupt | `cat .planning/tracking.json` to inspect, or restore from `git checkout HEAD -- .planning/tracking.json` |
|
|
152
|
-
| "ERP API key missing" | `~/.claude/.erp-api-key` empty | `qualia-framework set-erp-key
|
|
152
|
+
| "ERP API key missing" | `~/.claude/.erp-api-key` empty | `printf '%s' "$QUALIA_ERP_KEY" \| qualia-framework set-erp-key` (ask Fawzi for the key) |
|
|
153
153
|
| "ERP auth failed (401)" | Key revoked or wrong | Ask Fawzi for a fresh key |
|
|
154
154
|
| "ERP upload failed after 3 attempts" | ERP down or network issue | Local commit is safe. Re-run `/qualia-report` later. |
|
|
155
155
|
| "git push failed" | Auth or network or upstream issue | `git push` manually, see the error, fix, re-run |
|
|
@@ -181,53 +181,16 @@ IDEMPOTENCY_KEY=$(node -e "
|
|
|
181
181
|
|
|
182
182
|
# Guard: API key required for upload (otherwise curl posts an empty bearer)
|
|
183
183
|
if [ "$ERP_ENABLED" = "true" ] && [ -z "$API_KEY" ] && [ "$DRY_RUN" != "true" ]; then
|
|
184
|
-
node ~/.claude/bin/qualia-ui.js warn "ERP API key missing (~/.claude/.erp-api-key). Run: qualia-framework set-erp-key
|
|
184
|
+
node ~/.claude/bin/qualia-ui.js warn "ERP API key missing (~/.claude/.erp-api-key). Run: printf '%s' \"\$QUALIA_ERP_KEY\" | qualia-framework set-erp-key"
|
|
185
185
|
ERP_ENABLED="false"
|
|
186
186
|
fi
|
|
187
187
|
|
|
188
|
-
# Build payload (env-var-passed user values to dodge shell escaping)
|
|
188
|
+
# Build payload (env-var-passed user values to dodge shell escaping).
|
|
189
|
+
# `report-payload.js` is the canonical, tested Framework -> ERP payload builder.
|
|
189
190
|
PAYLOAD=$(
|
|
190
191
|
SUBMITTED_BY="$SUBMITTED_BY" SUBMITTED_AT="$SUBMITTED_AT" \
|
|
191
192
|
CLIENT_REPORT_ID="$CLIENT_REPORT_ID" REPORT_FILE="$REPORT_FILE" \
|
|
192
|
-
node -
|
|
193
|
-
const fs=require('fs'),path=require('path'),os=require('os');
|
|
194
|
-
const {spawnSync}=require('child_process');
|
|
195
|
-
const git=(a)=>{const r=spawnSync('git',a,{encoding:'utf8',timeout:3000});return r.status===0?r.stdout.trim():'';};
|
|
196
|
-
const repoSlug=(r)=>(r||'').replace(/^git@github\\.com:/,'github.com/').replace(/^https?:\\/\\//,'').replace(/\\.git$/,'').split('/').filter(Boolean).pop();
|
|
197
|
-
let config={};try{config=JSON.parse(fs.readFileSync(path.join(os.homedir(),'.claude/.qualia-config.json'),'utf8'));}catch{}
|
|
198
|
-
const t=JSON.parse(fs.readFileSync('.planning/tracking.json','utf8'));
|
|
199
|
-
const notes=fs.readFileSync(process.env.REPORT_FILE,'utf8').substring(0,60000);
|
|
200
|
-
const commits=[];try{const r=spawnSync('git',['log','--oneline','--since=8 hours ago','--format=%h'],{encoding:'utf8',timeout:3000});if(r.stdout)commits.push(...r.stdout.trim().split('\n').filter(Boolean));}catch{}
|
|
201
|
-
const gitRemote=t.git_remote||git(['config','--get','remote.origin.url']);
|
|
202
|
-
const projectKey=t.project_id||repoSlug(gitRemote)||require('path').basename(process.cwd());
|
|
203
|
-
// Session duration: minutes from session_started_at to submitted_at. The ERP's
|
|
204
|
-
// example payload (docs/erp-contract.md:93) includes this; without it the ERP
|
|
205
|
-
// can't compute shift-length analytics without parsing notes.
|
|
206
|
-
let sessionDurationMinutes=0;
|
|
207
|
-
if(t.session_started_at){
|
|
208
|
-
const startMs=Date.parse(t.session_started_at);
|
|
209
|
-
const endMs=Date.parse(process.env.SUBMITTED_AT)||Date.now();
|
|
210
|
-
if(!Number.isNaN(startMs)&&endMs>startMs){
|
|
211
|
-
sessionDurationMinutes=Math.round((endMs-startMs)/60000);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
console.log(JSON.stringify({
|
|
215
|
-
project:t.project||require('path').basename(process.cwd()),
|
|
216
|
-
project_id:projectKey,team_id:t.team_id||'qualia-solutions',git_remote:gitRemote,
|
|
217
|
-
client:t.client||'',client_report_id:process.env.CLIENT_REPORT_ID,
|
|
218
|
-
framework_version:config.version||'',milestone:t.milestone||1,
|
|
219
|
-
milestone_name:t.milestone_name||'',milestones:Array.isArray(t.milestones)?t.milestones:[],
|
|
220
|
-
phase:t.phase,phase_name:t.phase_name,total_phases:t.total_phases,status:t.status,
|
|
221
|
-
tasks_done:t.tasks_done||0,tasks_total:t.tasks_total||0,verification:t.verification||'pending',
|
|
222
|
-
gap_cycles:(t.gap_cycles||{})[String(t.phase)]||0,build_count:t.build_count||0,
|
|
223
|
-
deploy_count:t.deploy_count||0,deployed_url:t.deployed_url||'',
|
|
224
|
-
...(t.session_started_at?{session_started_at:t.session_started_at}:{}),
|
|
225
|
-
...(t.last_pushed_at?{last_pushed_at:t.last_pushed_at}:{}),
|
|
226
|
-
session_duration_minutes:sessionDurationMinutes,
|
|
227
|
-
lifetime:t.lifetime||{},commits:commits,notes:notes,
|
|
228
|
-
submitted_by:process.env.SUBMITTED_BY||'unknown',submitted_at:process.env.SUBMITTED_AT
|
|
229
|
-
}));
|
|
230
|
-
"
|
|
193
|
+
node ~/.claude/bin/report-payload.js
|
|
231
194
|
)
|
|
232
195
|
|
|
233
196
|
# --dry-run: print and stop
|
|
@@ -117,7 +117,7 @@ No accumulated garbage. No context rot.
|
|
|
117
117
|
- **Intent verification** — Confirm before modifying 3+ files (OWNER role: just do it)
|
|
118
118
|
|
|
119
119
|
## Tracking
|
|
120
|
-
`.planning/tracking.json` is updated on every push.
|
|
120
|
+
`.planning/tracking.json` is updated locally on every push. It feeds statusline, stop-session-log, and `/qualia-report`; the ERP receives explicit report uploads and does not read this file from git.
|
|
121
121
|
Never edit tracking.json manually — hooks update it from STATE.md.
|
|
122
122
|
|
|
123
123
|
## Compaction — ALWAYS preserve
|
|
@@ -106,7 +106,7 @@ Findings merge into main report. Union PASS/FAIL: either pass found CRITICAL/HIG
|
|
|
106
106
|
|
|
107
107
|
### 2d. INSUFFICIENT EVIDENCE downgrade (mandatory)
|
|
108
108
|
|
|
109
|
-
The verifier marks criteria it could not check (budget exhaustion, missing context) as `INSUFFICIENT EVIDENCE`. The orchestrator
|
|
109
|
+
The verifier marks criteria it could not check (budget exhaustion, missing context) as `INSUFFICIENT EVIDENCE`. The orchestrator must treat those as FAIL before declaring PASS. Grep the verification file and downgrade immediately:
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
112
|
IE_COUNT=$(grep -c "INSUFFICIENT EVIDENCE" .planning/phase-{N}-verification.md 2>/dev/null || echo 0)
|
package/templates/help.html
CHANGED
|
@@ -437,7 +437,7 @@
|
|
|
437
437
|
<div class="cmd"><span class="cmd-name">qualia-framework install</span><span class="cmd-desc">Install or reinstall the framework.</span></div>
|
|
438
438
|
<div class="cmd"><span class="cmd-name">qualia-framework update</span><span class="cmd-desc">Update to the latest version.</span></div>
|
|
439
439
|
<div class="cmd"><span class="cmd-name">qualia-framework version</span><span class="cmd-desc">Show installed version + check for updates.</span></div>
|
|
440
|
-
<div class="cmd"><span class="cmd-name">qualia-framework uninstall</span><span class="cmd-desc">Clean removal from
|
|
440
|
+
<div class="cmd"><span class="cmd-name">qualia-framework uninstall</span><span class="cmd-desc">Clean removal from installed Claude/Codex homes while preserving user-owned settings.</span></div>
|
|
441
441
|
<div class="cmd"><span class="cmd-name">qualia-framework migrate</span><span class="cmd-desc">Upgrade legacy settings.json to the current hook layout.</span></div>
|
|
442
442
|
<div class="cmd"><span class="cmd-name">qualia-framework analytics</span><span class="cmd-desc">Hook telemetry, verification pass rates, gap cycles.</span></div>
|
|
443
443
|
<div class="cmd"><span class="cmd-name">qualia-framework set-erp-key</span><span class="cmd-desc">Save and enable the ERP API key.</span></div>
|
|
@@ -6,11 +6,11 @@ once at session start.
|
|
|
6
6
|
|
|
7
7
|
## What's here
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The installed `knowledge/` directory is the project-spanning memory tier. It holds three
|
|
10
10
|
kinds of files, each with a clear purpose. Treat the structure as a contract.
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
|
|
13
|
+
knowledge/
|
|
14
14
|
├── agents.md ← this file (system overview)
|
|
15
15
|
├── index.md ← entry point — start here when answering questions
|
|
16
16
|
├── daily-log/
|
|
@@ -43,7 +43,7 @@ kinds of files, each with a clear purpose. Treat the structure as a contract.
|
|
|
43
43
|
**Do not pretend something is in memory if it is not.** Better to say
|
|
44
44
|
"INSUFFICIENT EVIDENCE: searched index.md and learned-patterns.md, no entry
|
|
45
45
|
matches" than to hallucinate a recalled pattern. The grounding protocol
|
|
46
|
-
(
|
|
46
|
+
(`rules/grounding.md`) applies here too.
|
|
47
47
|
|
|
48
48
|
## Tiers
|
|
49
49
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Knowledge Index
|
|
2
2
|
|
|
3
|
-
Entry point for
|
|
3
|
+
Entry point for the installed `knowledge/` directory. When answering a question, **read this
|
|
4
4
|
file first**, then jump to the specific file(s) that match.
|
|
5
5
|
|
|
6
6
|
> Auto-maintained by `/qualia-learn`. Do not hand-edit unless the file is out
|