qualia-framework 6.1.0 → 6.2.9
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 +39 -26
- package/agents/roadmapper.md +1 -1
- package/bin/cli.js +339 -200
- package/bin/codex-goal.js +92 -0
- package/bin/erp-retry.js +11 -3
- package/bin/install.js +483 -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 +38 -1
- package/hooks/pre-push.js +56 -157
- 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/rules/codex-goal.md +46 -0
- package/skills/qualia-build/SKILL.md +4 -0
- package/skills/qualia-feature/SKILL.md +4 -0
- 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-plan/SKILL.md +4 -0
- 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-deploy-gate.js
CHANGED
|
@@ -8,14 +8,51 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require("fs");
|
|
10
10
|
const path = require("path");
|
|
11
|
+
const os = require("os");
|
|
11
12
|
const { spawnSync } = require("child_process");
|
|
12
13
|
|
|
13
14
|
const _traceStart = Date.now();
|
|
14
15
|
|
|
16
|
+
function qualiaHome() {
|
|
17
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
18
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
19
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
20
|
+
return path.join(os.homedir(), ".claude");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const QUALIA_HOME = qualiaHome();
|
|
24
|
+
|
|
25
|
+
// Self-filter on the proposed bash command — only act when the user is
|
|
26
|
+
// actually trying to deploy. Claude Code's `if: "Bash(vercel --prod*)"` does
|
|
27
|
+
// substring matching (not glob), so commands that contain the literal text
|
|
28
|
+
// "vercel --prod" inside a for-loop or grep argument trip the gate. Codex
|
|
29
|
+
// ignores the `if` field entirely. Both runtimes therefore need this hook to
|
|
30
|
+
// inspect tool_input.command itself before doing any work.
|
|
31
|
+
//
|
|
32
|
+
// Direct invocation (no stdin — `node pre-deploy-gate.js`) is treated as
|
|
33
|
+
// "run the gate" so the test suite and manual `qualia-framework verify` flows
|
|
34
|
+
// keep working. Only when stdin contains a parseable tool_input.command that
|
|
35
|
+
// does NOT match a deploy pattern do we exit early.
|
|
36
|
+
(function selfFilter() {
|
|
37
|
+
if (process.stdin.isTTY) return; // direct invocation — run full gate
|
|
38
|
+
let command = null; // null = no stdin payload, "" = empty command, "str" = parsed
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
41
|
+
if (raw) {
|
|
42
|
+
const payload = JSON.parse(raw);
|
|
43
|
+
command = (payload && payload.tool_input && payload.tool_input.command) || "";
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
if (command === null) return; // malformed or empty stdin — run full gate
|
|
47
|
+
if (!/^\s*(npx\s+)?vercel\s+(--prod|deploy\s+--prod)\b/.test(command)) {
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
|
|
15
52
|
function _trace(hookName, result, extra) {
|
|
16
53
|
try {
|
|
17
54
|
const os = require("os");
|
|
18
|
-
const traceDir = path.join(
|
|
55
|
+
const traceDir = path.join(QUALIA_HOME, ".qualia-traces");
|
|
19
56
|
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
20
57
|
const entry = {
|
|
21
58
|
hook: hookName,
|
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,39 @@ 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;
|
|
39
|
+
const QUALIA_HOME = qualiaHome();
|
|
40
|
+
const TRACKING = path.join(".planning", "tracking.json");
|
|
41
|
+
const SHELL = process.platform === "win32";
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
// Self-filter — only stamp on actual `git push`. Claude's `if` matcher does
|
|
44
|
+
// substring matching (so unrelated commands with "git push" as a literal
|
|
45
|
+
// argument trip the hook) and Codex ignores the `if` field entirely. Direct
|
|
46
|
+
// invocation (no stdin) falls through to the legacy behavior so tests and
|
|
47
|
+
// manual runs still stamp tracking.json.
|
|
48
|
+
(function selfFilter() {
|
|
49
|
+
if (process.stdin.isTTY) return;
|
|
50
|
+
let command = null;
|
|
53
51
|
try {
|
|
54
52
|
const raw = fs.readFileSync(0, "utf8");
|
|
55
|
-
if (raw
|
|
53
|
+
if (raw) {
|
|
56
54
|
const payload = JSON.parse(raw);
|
|
57
|
-
|
|
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
|
-
}
|
|
55
|
+
command = (payload && payload.tool_input && payload.tool_input.command) || "";
|
|
75
56
|
}
|
|
76
|
-
}
|
|
57
|
+
} catch {}
|
|
58
|
+
if (command === null) return;
|
|
59
|
+
if (!/^\s*git\s+push\b/.test(command)) process.exit(0);
|
|
60
|
+
})();
|
|
77
61
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const branch = ((r.stdout || "").trim());
|
|
81
|
-
return branch === "main" || branch === "master";
|
|
62
|
+
function git(args) {
|
|
63
|
+
return spawnSync("git", args, { encoding: "utf8", timeout: 5000, shell: SHELL });
|
|
82
64
|
}
|
|
83
65
|
|
|
84
66
|
function atomicWrite(file, content) {
|
|
@@ -87,91 +69,31 @@ function atomicWrite(file, content) {
|
|
|
87
69
|
fs.renameSync(tmp, file);
|
|
88
70
|
}
|
|
89
71
|
|
|
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() {
|
|
72
|
+
function stampLocal() {
|
|
96
73
|
if (!fs.existsSync(TRACKING)) return { skipped: "no-tracking-file" };
|
|
97
|
-
if (!inGitRepo()) return { skipped: "not-a-git-repo" };
|
|
98
74
|
|
|
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;
|
|
75
|
+
let parsed;
|
|
106
76
|
try {
|
|
107
|
-
|
|
108
|
-
parsed = JSON.parse(raw);
|
|
77
|
+
parsed = JSON.parse(fs.readFileSync(TRACKING, "utf8"));
|
|
109
78
|
} catch (err) {
|
|
110
79
|
return { skipped: "tracking-unreadable", error: err.message };
|
|
111
80
|
}
|
|
112
81
|
|
|
113
|
-
const
|
|
82
|
+
const head = git(["log", "--oneline", "-1", "--format=%h"]);
|
|
83
|
+
const lastCommit = (head.stdout || "").trim();
|
|
84
|
+
const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
85
|
+
|
|
114
86
|
if (lastCommit) parsed.last_commit = lastCommit;
|
|
115
87
|
parsed.last_updated = now;
|
|
116
88
|
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
89
|
|
|
120
90
|
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;
|
|
91
|
+
return { stamped: true, sha: lastCommit, ts: now };
|
|
170
92
|
}
|
|
171
93
|
|
|
172
94
|
function _trace(result, extra) {
|
|
173
95
|
try {
|
|
174
|
-
const traceDir = path.join(
|
|
96
|
+
const traceDir = path.join(QUALIA_HOME, ".qualia-traces");
|
|
175
97
|
if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
|
|
176
98
|
const entry = {
|
|
177
99
|
hook: "pre-push",
|
|
@@ -186,34 +108,11 @@ function _trace(result, extra) {
|
|
|
186
108
|
}
|
|
187
109
|
|
|
188
110
|
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
|
-
}
|
|
111
|
+
const result = stampLocal();
|
|
213
112
|
_trace("allow", result);
|
|
214
113
|
} catch (err) {
|
|
215
|
-
//
|
|
216
|
-
console.error(`⚠ tracking.json
|
|
114
|
+
// Never block a push on a stamp failure — tracking.json is local telemetry.
|
|
115
|
+
console.error(`⚠ tracking.json stamp failed: ${err.message}. Push proceeding.`);
|
|
217
116
|
_trace("warn", { error: err.message });
|
|
218
117
|
}
|
|
219
118
|
|
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.9",
|
|
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": [
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Codex /goal integration
|
|
2
|
+
|
|
3
|
+
When this skill spawns a unit of work on **Codex** (not Claude Code), set the thread goal at the start so Codex's native token-budget + status tracking takes over.
|
|
4
|
+
|
|
5
|
+
## Runtime detection
|
|
6
|
+
|
|
7
|
+
You are on Codex when `~/.codex/` exists and `~/.claude/` is absent or stale. The simplest probe:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
test -f ~/.codex/AGENTS.md && echo codex || echo claude
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If the answer is `claude`, **skip this entire rule** — Claude Code has no equivalent surface and emitting `/goal` text would just be noise.
|
|
14
|
+
|
|
15
|
+
## How to set the goal
|
|
16
|
+
|
|
17
|
+
1. Run the helper to produce the objective string + suggested token budget:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node ~/.codex/bin/codex-goal.js {scope}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`{scope}` is one of: `phase` · `task` · `feature` · `quick`. Use the scope of the current skill.
|
|
24
|
+
|
|
25
|
+
2. The output is two lines:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
/goal {objective text from STATE.md + ROADMAP.md}
|
|
29
|
+
# token_budget suggestion: {N}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. **If the `update_goal` tool is available** to you (Codex exposes it as a model-callable tool), call it directly with:
|
|
33
|
+
- `objective` = the text after `/goal ` on line 1
|
|
34
|
+
- `token_budget` = the integer suggestion on line 2
|
|
35
|
+
|
|
36
|
+
4. **If `update_goal` is not available**, surface the `/goal` line to the user in your next message and let them paste it. Do not silently skip — the goal-set takes 1 second and is the only way Codex's budget telemetry knows what to track.
|
|
37
|
+
|
|
38
|
+
## When NOT to set a goal
|
|
39
|
+
|
|
40
|
+
- The user is on Claude Code (no `/goal` surface).
|
|
41
|
+
- A goal is already active for this thread (Codex rejects `update_goal` when one exists — call `thread/goal/get` first if you're using the tool API directly).
|
|
42
|
+
- The work is open-ended exploration with no clear objective (e.g. `/qualia-idk`, `/qualia-discuss`). Goals are for executing a defined scope.
|
|
43
|
+
|
|
44
|
+
## Why
|
|
45
|
+
|
|
46
|
+
Codex's `thread_goals` table tracks `objective`, `token_budget`, `tokens_used`, and a `status` enum (`active | paused | blocked | usage_limited | budget_limited | complete`). Setting the goal lets the user see burn-vs-budget in the TUI without the framework reinventing it. The token-budget number also makes the model self-aware of how much context it has left for the current unit of work.
|
|
@@ -24,6 +24,10 @@ Execute phase plan. Each task = fresh subagent. Independent tasks run parallel.
|
|
|
24
24
|
|
|
25
25
|
## Process
|
|
26
26
|
|
|
27
|
+
### 0. Codex goal (Codex runtime only)
|
|
28
|
+
|
|
29
|
+
Per `rules/codex-goal.md` — set the thread goal at phase start with scope `phase`.
|
|
30
|
+
|
|
27
31
|
### 1. Load Plan
|
|
28
32
|
|
|
29
33
|
```bash
|
|
@@ -39,6 +39,10 @@ One command for everything between a typo and a phase. Auto-detects scope from t
|
|
|
39
39
|
|
|
40
40
|
## Process
|
|
41
41
|
|
|
42
|
+
### 0. Codex goal (Codex runtime only)
|
|
43
|
+
|
|
44
|
+
Per `rules/codex-goal.md` — set the thread goal with scope matching the auto-detected bucket (`quick` for inline, `feature` for spawn). Do this AFTER Step 2 (auto-detect scope) so the budget matches the actual work shape.
|
|
45
|
+
|
|
42
46
|
### 1. Capture description
|
|
43
47
|
|
|
44
48
|
If invoked without args, ask: **"What do you want to build?"**
|
|
@@ -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"
|
|
@@ -27,6 +27,10 @@ Spawn planner to break phase into tasks, validate with checker (max 2 revision c
|
|
|
27
27
|
|
|
28
28
|
## Process
|
|
29
29
|
|
|
30
|
+
### 0. Codex goal (Codex runtime only)
|
|
31
|
+
|
|
32
|
+
Per `rules/codex-goal.md` — set the thread goal at plan start with scope `phase`. The objective covers both planning and the subsequent build, so a single goal-set at this stage is enough.
|
|
33
|
+
|
|
30
34
|
### 1. Determine Phase & Load Context
|
|
31
35
|
|
|
32
36
|
```bash
|
|
@@ -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
|
|