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.
Files changed (59) hide show
  1. package/README.md +39 -26
  2. package/agents/roadmapper.md +1 -1
  3. package/bin/cli.js +339 -200
  4. package/bin/codex-goal.js +92 -0
  5. package/bin/erp-retry.js +11 -3
  6. package/bin/install.js +483 -55
  7. package/bin/knowledge-flush.js +25 -13
  8. package/bin/knowledge.js +11 -1
  9. package/bin/project-snapshot.js +293 -0
  10. package/bin/qualia-ui.js +13 -2
  11. package/bin/report-payload.js +137 -0
  12. package/bin/state.js +8 -1
  13. package/bin/statusline.js +14 -2
  14. package/docs/changelog-v6.html +864 -0
  15. package/docs/ecosystem-operating-model.md +121 -0
  16. package/docs/erp-contract.md +74 -21
  17. package/docs/onboarding.html +1 -1
  18. package/docs/release.md +44 -0
  19. package/docs/reviews/v6.2.1-revival-audit.md +53 -0
  20. package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
  21. package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
  22. package/guide.md +16 -4
  23. package/hooks/auto-update.js +14 -7
  24. package/hooks/branch-guard.js +10 -2
  25. package/hooks/env-empty-guard.js +10 -1
  26. package/hooks/git-guardrails.js +10 -1
  27. package/hooks/migration-guard.js +4 -1
  28. package/hooks/pre-deploy-gate.js +38 -1
  29. package/hooks/pre-push.js +56 -157
  30. package/hooks/session-start.js +22 -14
  31. package/hooks/stop-session-log.js +11 -3
  32. package/hooks/supabase-destructive-guard.js +11 -1
  33. package/hooks/vercel-account-guard.js +12 -3
  34. package/package.json +3 -2
  35. package/rules/codex-goal.md +46 -0
  36. package/skills/qualia-build/SKILL.md +4 -0
  37. package/skills/qualia-feature/SKILL.md +4 -0
  38. package/skills/qualia-map/SKILL.md +1 -1
  39. package/skills/qualia-milestone/SKILL.md +1 -1
  40. package/skills/qualia-optimize/SKILL.md +1 -1
  41. package/skills/qualia-plan/SKILL.md +4 -0
  42. package/skills/qualia-polish/SKILL.md +2 -2
  43. package/skills/qualia-report/SKILL.md +6 -43
  44. package/skills/qualia-road/SKILL.md +1 -1
  45. package/skills/qualia-verify/SKILL.md +1 -1
  46. package/templates/help.html +1 -1
  47. package/templates/knowledge/agents.md +3 -3
  48. package/templates/knowledge/index.md +1 -1
  49. package/templates/tracking.json +3 -0
  50. package/templates/work-packet.md +46 -0
  51. package/tests/bin.test.sh +411 -13
  52. package/tests/hooks.test.sh +1 -8
  53. package/tests/install-smoke.test.sh +137 -0
  54. package/tests/published-install-smoke.test.sh +126 -0
  55. package/tests/refs.test.sh +42 -0
  56. package/tests/run-all.sh +1 -0
  57. package/tests/runner.js +19 -33
  58. package/tests/state.test.sh +4 -1
  59. package/hooks/pre-compact.js +0 -127
@@ -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(os.homedir(), ".claude", ".qualia-traces");
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 + commit it before push.
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
- // Cross-platform (Windows/macOS/Linux). No external dependencies.
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
- // History rationale: a previous version (≤v3.4.1) wrote the stamp to
10
- // tracking.json and then `git add`-ed it, but the actual `git push` ran on
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 git(args, opts = {}) {
28
- return spawnSync("git", args, {
29
- encoding: "utf8",
30
- timeout: 5000,
31
- shell: SHELL,
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
- // Self-gate against branch-guard: PreToolUse hooks run in parallel, so this
37
- // hook MUST mirror branch-guard's block logic before creating a side-effect
38
- // commit. Otherwise: branch-guard exits 2 (push blocked), we already wrote a
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
- // Mirror branch-guard's stdin parse for refspec targets.
52
- let pushCommand = "";
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 && raw.trim()) {
53
+ if (raw) {
56
54
  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
- }
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
- // Current-branch fallback.
79
- const r = git(["branch", "--show-current"], { stdio: ["ignore", "pipe", "ignore"] });
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 inGitRepo() {
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
- // Read current commit + stamp
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
- raw = fs.readFileSync(TRACKING, "utf8");
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 before = JSON.stringify({ last_commit: parsed.last_commit, last_updated: parsed.last_updated });
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(HOME, ".claude", ".qualia-traces");
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
- // Defer to branch-guard: if the push will be blocked, don't create the
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
- // Exception during commit stamping warn, don't block (same v5.0 rationale).
216
- console.error(`⚠ tracking.json sync threw: ${err.message}. Push proceeding. Self-service fix: git checkout HEAD -- .planning/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
 
@@ -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
- const UI = path.join(HOME, ".claude", "bin", "qualia-ui.js");
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(HOME, ".claude", ".qualia-update-available.json");
28
- const HEALTH_FILE = path.join(HOME, ".claude", ".qualia-install-health.json");
29
- const ERP_RETRY = path.join(HOME, ".claude", "bin", "erp-retry.js");
30
- const ERP_QUEUE = path.join(HOME, ".claude", ".erp-retry-queue.json");
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(HOME, ".claude", "rules", "grounding.md"),
38
- path.join(HOME, ".claude", "rules", "security.md"),
39
- path.join(HOME, ".claude", "rules", "deployment.md"),
40
- path.join(HOME, ".claude", "qualia-design", "frontend.md"),
41
- path.join(HOME, ".claude", "bin", "state.js"),
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(HOME, ".claude", "bin", "state.js");
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(HOME, ".claude", ".qualia-config.json"), "utf8"));
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(os.homedir(), ".claude", ".qualia-traces");
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(os.homedir(), ".claude", ".qualia-traces");
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
- const KNOWLEDGE_DIR = path.join(os.homedir(), ".claude", "knowledge");
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(os.homedir(), ".claude", ".qualia-last-session-log");
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(os.homedir(), ".claude", ".qualia-traces");
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(os.homedir(), ".claude", ".qualia-traces");
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(os.homedir(), ".claude", ".qualia-traces");
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(os.homedir(), ".claude", ".vercel-allowed-teams");
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 ~/.claude/.vercel-allowed-teams.`;
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.1.0",
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" → skip silently, defaults apply.
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. **ERP sync aware.** tracking.json milestones[] gets a summary entry on close the ERP reads this to render the tree.
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 (skip silently if missing):
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, skip silently if not configured
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, skip silently and note in the report. Don't fail the polish over missing tooling.
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 &lt; 90 OR axe critical/serious violations: **fix programmatically** (these are deterministic — no vision needed). Re-run to confirm.
156
156