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.
Files changed (54) hide show
  1. package/README.md +35 -26
  2. package/agents/roadmapper.md +1 -1
  3. package/bin/cli.js +339 -200
  4. package/bin/erp-retry.js +11 -3
  5. package/bin/install.js +383 -55
  6. package/bin/knowledge-flush.js +25 -13
  7. package/bin/knowledge.js +11 -1
  8. package/bin/project-snapshot.js +293 -0
  9. package/bin/qualia-ui.js +13 -2
  10. package/bin/report-payload.js +137 -0
  11. package/bin/state.js +8 -1
  12. package/bin/statusline.js +14 -2
  13. package/docs/changelog-v6.html +864 -0
  14. package/docs/ecosystem-operating-model.md +121 -0
  15. package/docs/erp-contract.md +74 -21
  16. package/docs/onboarding.html +1 -1
  17. package/docs/release.md +44 -0
  18. package/docs/reviews/v6.2.1-revival-audit.md +53 -0
  19. package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
  20. package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
  21. package/guide.md +16 -4
  22. package/hooks/auto-update.js +14 -7
  23. package/hooks/branch-guard.js +10 -2
  24. package/hooks/env-empty-guard.js +10 -1
  25. package/hooks/git-guardrails.js +10 -1
  26. package/hooks/migration-guard.js +4 -1
  27. package/hooks/pre-deploy-gate.js +11 -1
  28. package/hooks/pre-push.js +42 -162
  29. package/hooks/session-start.js +22 -14
  30. package/hooks/stop-session-log.js +11 -3
  31. package/hooks/supabase-destructive-guard.js +11 -1
  32. package/hooks/vercel-account-guard.js +12 -3
  33. package/package.json +3 -2
  34. package/skills/qualia-map/SKILL.md +1 -1
  35. package/skills/qualia-milestone/SKILL.md +1 -1
  36. package/skills/qualia-optimize/SKILL.md +1 -1
  37. package/skills/qualia-polish/SKILL.md +2 -2
  38. package/skills/qualia-report/SKILL.md +6 -43
  39. package/skills/qualia-road/SKILL.md +1 -1
  40. package/skills/qualia-verify/SKILL.md +1 -1
  41. package/templates/help.html +1 -1
  42. package/templates/knowledge/agents.md +3 -3
  43. package/templates/knowledge/index.md +1 -1
  44. package/templates/tracking.json +3 -0
  45. package/templates/work-packet.md +46 -0
  46. package/tests/bin.test.sh +411 -13
  47. package/tests/hooks.test.sh +1 -8
  48. package/tests/install-smoke.test.sh +137 -0
  49. package/tests/published-install-smoke.test.sh +126 -0
  50. package/tests/refs.test.sh +42 -0
  51. package/tests/run-all.sh +1 -0
  52. package/tests/runner.js +19 -33
  53. package/tests/state.test.sh +4 -1
  54. 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 + 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,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 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;
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
- // 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";
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 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() {
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
- // 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;
56
+ let parsed;
106
57
  try {
107
- raw = fs.readFileSync(TRACKING, "utf8");
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 before = JSON.stringify({ last_commit: parsed.last_commit, last_updated: parsed.last_updated });
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(HOME, ".claude", ".qualia-traces");
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
- // 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
- }
92
+ const result = stampLocal();
213
93
  _trace("allow", result);
214
94
  } 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`);
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
 
@@ -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.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" → 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"
@@ -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
 
@@ -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 silently, note local commit
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 <key>` (ask Fawzi for the 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 <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 -e "
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. The ERP reads it via git.
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 treats those as silent PASS unless explicitly downgraded — that's the #1 false-pass vector. Grep the verification file before declaring PASS:
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)
@@ -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 ~/.claude/ while preserving user-owned settings.</span></div>
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
- `~/.claude/knowledge/` is the project-spanning memory tier. It holds three
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
- ~/.claude/knowledge/
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
- (`~/.claude/rules/grounding.md`) applies here too.
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 `~/.claude/knowledge/`. When answering a question, **read this
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
@@ -5,6 +5,9 @@
5
5
  "assigned_to": "",
6
6
  "team_id": "",
7
7
  "project_id": "",
8
+ "erp_project_id": "",
9
+ "client_id": "",
10
+ "workspace_id": "",
8
11
  "git_remote": "",
9
12
  "milestone": 1,
10
13
  "milestone_name": "",