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/bin/knowledge.js CHANGED
@@ -28,7 +28,17 @@ const fs = require("fs");
28
28
  const path = require("path");
29
29
  const os = require("os");
30
30
 
31
- const KNOWLEDGE_DIR = path.join(os.homedir(), ".claude", "knowledge");
31
+ function qualiaHome() {
32
+ if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
33
+ const parent = path.basename(path.dirname(__dirname));
34
+ if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
35
+ const claude = path.join(os.homedir(), ".claude");
36
+ if (fs.existsSync(path.join(claude, "knowledge"))) return claude;
37
+ return claude;
38
+ }
39
+
40
+ const QUALIA_HOME = qualiaHome();
41
+ const KNOWLEDGE_DIR = path.join(QUALIA_HOME, "knowledge");
32
42
  const INDEX_FILE = path.join(KNOWLEDGE_DIR, "index.md");
33
43
 
34
44
  // Type → filename mapping for `append` and convenience aliases used by the
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+ const fs = require("fs");
3
+ const http = require("http");
4
+ const https = require("https");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const { spawnSync } = require("child_process");
8
+
9
+ function readJson(file, fallback = {}) {
10
+ try {
11
+ return JSON.parse(fs.readFileSync(file, "utf8"));
12
+ } catch {
13
+ return fallback;
14
+ }
15
+ }
16
+
17
+ function readText(file, fallback = "") {
18
+ try {
19
+ return fs.readFileSync(file, "utf8");
20
+ } catch {
21
+ return fallback;
22
+ }
23
+ }
24
+
25
+ function qualiaHome(home = os.homedir()) {
26
+ if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
27
+ const parent = path.basename(path.dirname(__dirname));
28
+ if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
29
+ const claude = path.join(home, ".claude");
30
+ if (fs.existsSync(path.join(claude, ".qualia-config.json"))) return claude;
31
+ return claude;
32
+ }
33
+
34
+ function git(args, cwd) {
35
+ const result = spawnSync("git", args, {
36
+ cwd,
37
+ encoding: "utf8",
38
+ timeout: 3000,
39
+ });
40
+ return result.status === 0 ? result.stdout.trim() : "";
41
+ }
42
+
43
+ function repoSlug(remote) {
44
+ return (remote || "")
45
+ .replace(/^git@github\.com:/, "github.com/")
46
+ .replace(/^https?:\/\//, "")
47
+ .replace(/\.git$/, "")
48
+ .split("/")
49
+ .filter(Boolean)
50
+ .pop();
51
+ }
52
+
53
+ function uuid(value) {
54
+ if (typeof value !== "string") return "";
55
+ const trimmed = value.trim();
56
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)
57
+ ? trimmed
58
+ : "";
59
+ }
60
+
61
+ function parseJourneyMilestones(content) {
62
+ if (!content) return [];
63
+ const milestones = [];
64
+ const re = /^##\s+Milestone\s+(\d+)\s*[·•-]\s*(.+?)\s*(?:\[[^\]]+\])?\s*$/gm;
65
+ let match;
66
+ while ((match = re.exec(content)) !== null) {
67
+ milestones.push({
68
+ num: Number(match[1]),
69
+ name: match[2].trim(),
70
+ });
71
+ }
72
+ return milestones;
73
+ }
74
+
75
+ function clampPercent(value) {
76
+ if (!Number.isFinite(value)) return 0;
77
+ return Math.max(0, Math.min(100, Math.round(value)));
78
+ }
79
+
80
+ function buildSnapshot(options = {}) {
81
+ const cwd = options.cwd || process.cwd();
82
+ const home = options.home || os.homedir();
83
+ const installHome = options.qualiaHome || qualiaHome(home);
84
+ const now = options.now || new Date().toISOString();
85
+ const planning = options.planningDir || path.join(cwd, ".planning");
86
+ const tracking = readJson(path.join(planning, "tracking.json"), {});
87
+ const config = readJson(path.join(installHome, ".qualia-config.json"), {});
88
+ const journey = parseJourneyMilestones(readText(path.join(planning, "JOURNEY.md"), ""));
89
+ const gitRemote = tracking.git_remote || git(["config", "--get", "remote.origin.url"], cwd);
90
+ const projectId = tracking.project_id || repoSlug(gitRemote) || path.basename(cwd);
91
+ const currentMilestone = Number(tracking.milestone || 1);
92
+ const currentPhase = Number(tracking.phase || 0);
93
+ const totalPhases = Number(tracking.total_phases || 0);
94
+ const lifetime = tracking.lifetime && typeof tracking.lifetime === "object" ? tracking.lifetime : {};
95
+ const closedMilestones = Array.isArray(tracking.milestones) ? tracking.milestones : [];
96
+ const closedMilestoneNums = new Set(
97
+ closedMilestones
98
+ .map((m) => Number(m && m.num))
99
+ .filter((n) => Number.isFinite(n))
100
+ );
101
+ const journeyTotal = journey.length || Math.max(currentMilestone, closedMilestones.length || 1);
102
+ const phaseProgress =
103
+ totalPhases > 0 && currentPhase > 0 ? (Math.max(0, currentPhase - 1) / totalPhases) : 0;
104
+ const projectProgress =
105
+ journeyTotal > 0
106
+ ? ((closedMilestoneNums.size + Math.min(1, phaseProgress)) / journeyTotal) * 100
107
+ : 0;
108
+
109
+ return {
110
+ snapshot_version: 1,
111
+ generated_at: now,
112
+ source: "qualia-framework",
113
+ framework_version: config.version || "",
114
+ identifiers: {
115
+ project_id: projectId,
116
+ team_id: tracking.team_id || "qualia-solutions",
117
+ git_remote: gitRemote || "",
118
+ ...(uuid(tracking.erp_project_id) ? { erp_project_id: uuid(tracking.erp_project_id) } : {}),
119
+ ...(uuid(tracking.client_id) ? { client_id: uuid(tracking.client_id) } : {}),
120
+ ...(uuid(tracking.workspace_id) ? { workspace_id: uuid(tracking.workspace_id) } : {}),
121
+ },
122
+ project: {
123
+ name: tracking.project || path.basename(cwd),
124
+ client: tracking.client || "",
125
+ status: tracking.status || "setup",
126
+ deployed_url: tracking.deployed_url || "",
127
+ progress_percent: clampPercent(projectProgress),
128
+ },
129
+ current: {
130
+ milestone: currentMilestone,
131
+ milestone_name: tracking.milestone_name || "",
132
+ phase: currentPhase,
133
+ phase_name: tracking.phase_name || "",
134
+ total_phases: totalPhases,
135
+ tasks_done: Number(tracking.tasks_done || 0),
136
+ tasks_total: Number(tracking.tasks_total || 0),
137
+ verification: tracking.verification || "pending",
138
+ gap_cycles: (tracking.gap_cycles || {})[String(currentPhase)] || 0,
139
+ },
140
+ journey: {
141
+ total_milestones: journeyTotal,
142
+ milestones: journey.map((milestone) => ({
143
+ ...milestone,
144
+ status:
145
+ closedMilestoneNums.has(milestone.num)
146
+ ? "closed"
147
+ : milestone.num === currentMilestone
148
+ ? "active"
149
+ : "pending",
150
+ })),
151
+ closed_milestones: closedMilestones,
152
+ },
153
+ lifetime: {
154
+ tasks_completed: Number(lifetime.tasks_completed || 0),
155
+ phases_completed: Number(lifetime.phases_completed || 0),
156
+ milestones_completed: Number(lifetime.milestones_completed || closedMilestoneNums.size || 0),
157
+ total_phases: Number(lifetime.total_phases || 0),
158
+ last_closed_milestone: Number(lifetime.last_closed_milestone || 0),
159
+ build_count: Number(tracking.build_count || 0),
160
+ deploy_count: Number(tracking.deploy_count || 0),
161
+ },
162
+ timestamps: {
163
+ session_started_at: tracking.session_started_at || "",
164
+ last_pushed_at: tracking.last_pushed_at || "",
165
+ last_updated: tracking.last_updated || "",
166
+ },
167
+ };
168
+ }
169
+
170
+ function writeSnapshot(snapshot, options = {}) {
171
+ const cwd = options.cwd || process.cwd();
172
+ const planning = options.planningDir || path.join(cwd, ".planning");
173
+ const outDir = options.outDir || path.join(planning, "snapshots");
174
+ fs.mkdirSync(outDir, { recursive: true });
175
+ const stamp = snapshot.generated_at.replace(/[:.]/g, "-");
176
+ const file = path.join(outDir, `project-snapshot-${stamp}.json`);
177
+ fs.writeFileSync(file, `${JSON.stringify(snapshot, null, 2)}\n`);
178
+ return file;
179
+ }
180
+
181
+ function erpConfig(options = {}) {
182
+ const home = options.home || os.homedir();
183
+ const installHome = options.qualiaHome || qualiaHome(home);
184
+ const config = readJson(path.join(installHome, ".qualia-config.json"), {});
185
+ const erp = config.erp || {};
186
+ const url = (erp.url || "https://portal.qualiasolutions.net").replace(/\/+$/, "");
187
+ const keyFile = path.join(installHome, erp.api_key_file || ".erp-api-key");
188
+ const key = readText(keyFile, "").trim();
189
+ return {
190
+ enabled: erp.enabled !== false,
191
+ url,
192
+ key,
193
+ };
194
+ }
195
+
196
+ function uploadSnapshot(snapshot, options = {}) {
197
+ const cfg = options.erp || erpConfig(options);
198
+ if (!cfg.enabled) {
199
+ return Promise.reject(new Error("ERP disabled in Qualia config"));
200
+ }
201
+ if (!cfg.key) {
202
+ return Promise.reject(new Error("ERP API key missing in Qualia install"));
203
+ }
204
+
205
+ const endpoint = new URL("/api/v1/project-snapshots", cfg.url);
206
+ const payload = JSON.stringify(snapshot);
207
+ const transport = endpoint.protocol === "http:" ? http : https;
208
+
209
+ return new Promise((resolve, reject) => {
210
+ const req = transport.request(
211
+ endpoint,
212
+ {
213
+ method: "POST",
214
+ headers: {
215
+ Authorization: `Bearer ${cfg.key}`,
216
+ "Content-Type": "application/json",
217
+ "Content-Length": Buffer.byteLength(payload),
218
+ "User-Agent": "qualia-framework-project-snapshot",
219
+ },
220
+ timeout: options.timeout || 15000,
221
+ },
222
+ (res) => {
223
+ let body = "";
224
+ res.setEncoding("utf8");
225
+ res.on("data", (chunk) => {
226
+ body += chunk;
227
+ });
228
+ res.on("end", () => {
229
+ let parsed = null;
230
+ try {
231
+ parsed = body ? JSON.parse(body) : null;
232
+ } catch {
233
+ parsed = body;
234
+ }
235
+ if (res.statusCode >= 200 && res.statusCode < 300) {
236
+ resolve({ status: res.statusCode, body: parsed });
237
+ } else {
238
+ const message =
239
+ typeof parsed === "object" && parsed && parsed.message
240
+ ? parsed.message
241
+ : body || `HTTP ${res.statusCode}`;
242
+ reject(new Error(`ERP snapshot upload failed (${res.statusCode}): ${message}`));
243
+ }
244
+ });
245
+ }
246
+ );
247
+ req.on("timeout", () => {
248
+ req.destroy(new Error("ERP snapshot upload timed out"));
249
+ });
250
+ req.on("error", reject);
251
+ req.write(payload);
252
+ req.end();
253
+ });
254
+ }
255
+
256
+ function parseArgs(argv) {
257
+ return {
258
+ upload: argv.includes("--upload"),
259
+ write: argv.includes("--write"),
260
+ pretty: argv.includes("--pretty"),
261
+ };
262
+ }
263
+
264
+ if (require.main === module) {
265
+ (async () => {
266
+ const args = parseArgs(process.argv.slice(2));
267
+ const snapshot = buildSnapshot();
268
+ if (args.write) {
269
+ const file = writeSnapshot(snapshot);
270
+ process.stdout.write(`${file}\n`);
271
+ }
272
+ if (args.upload) {
273
+ const response = await uploadSnapshot(snapshot);
274
+ process.stdout.write(`${JSON.stringify(response.body || { ok: true })}\n`);
275
+ }
276
+ if (!args.write && !args.upload) {
277
+ process.stdout.write(`${JSON.stringify(snapshot, null, args.pretty ? 2 : 0)}\n`);
278
+ }
279
+ })().catch((error) => {
280
+ console.error(`project-snapshot failed: ${error.message}`);
281
+ process.exit(1);
282
+ });
283
+ }
284
+
285
+ module.exports = {
286
+ buildSnapshot,
287
+ erpConfig,
288
+ parseJourneyMilestones,
289
+ repoSlug,
290
+ uploadSnapshot,
291
+ uuid,
292
+ writeSnapshot,
293
+ };
package/bin/qualia-ui.js CHANGED
@@ -42,6 +42,17 @@ const BOLD = "\x1b[1m";
42
42
  const RULE = "━".repeat(42);
43
43
  const RULE_DIM = `${DIM2}${RULE}${RESET}`;
44
44
 
45
+ function qualiaHome() {
46
+ if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
47
+ const parent = path.basename(path.dirname(__dirname));
48
+ if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
49
+ const claude = path.join(os.homedir(), ".claude");
50
+ if (fs.existsSync(path.join(claude, ".qualia-config.json"))) return claude;
51
+ return claude;
52
+ }
53
+
54
+ const QUALIA_HOME = qualiaHome();
55
+
45
56
  // ─── Action Labels ───────────────────────────────────────
46
57
  const ACTIONS = {
47
58
  router: { label: "SMART ROUTER", glyph: "⬢" },
@@ -76,7 +87,7 @@ const ACTIONS = {
76
87
  // ─── State Reading ───────────────────────────────────────
77
88
  function readState() {
78
89
  try {
79
- const statePath = path.join(os.homedir(), ".claude", "bin", "state.js");
90
+ const statePath = path.join(QUALIA_HOME, "bin", "state.js");
80
91
  const r = spawnSync(process.execPath, [statePath, "check"], {
81
92
  encoding: "utf8",
82
93
  timeout: 3000,
@@ -91,7 +102,7 @@ function readState() {
91
102
 
92
103
  function readConfig() {
93
104
  try {
94
- const f = path.join(os.homedir(), ".claude", ".qualia-config.json");
105
+ const f = path.join(QUALIA_HOME, ".qualia-config.json");
95
106
  return JSON.parse(fs.readFileSync(f, "utf8"));
96
107
  } catch {
97
108
  return {};
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const { spawnSync } = require("child_process");
6
+
7
+ function readJson(file, fallback = {}) {
8
+ try {
9
+ return JSON.parse(fs.readFileSync(file, "utf8"));
10
+ } catch {
11
+ return fallback;
12
+ }
13
+ }
14
+
15
+ function readText(file, fallback = "") {
16
+ try {
17
+ return fs.readFileSync(file, "utf8");
18
+ } catch {
19
+ return fallback;
20
+ }
21
+ }
22
+
23
+ function qualiaHome(home = os.homedir()) {
24
+ if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
25
+ const parent = path.basename(path.dirname(__dirname));
26
+ if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
27
+ const claude = path.join(home, ".claude");
28
+ if (fs.existsSync(path.join(claude, ".qualia-config.json"))) return claude;
29
+ return claude;
30
+ }
31
+
32
+ function git(args, cwd) {
33
+ const result = spawnSync("git", args, {
34
+ cwd,
35
+ encoding: "utf8",
36
+ timeout: 3000,
37
+ });
38
+ return result.status === 0 ? result.stdout.trim() : "";
39
+ }
40
+
41
+ function repoSlug(remote) {
42
+ return (remote || "")
43
+ .replace(/^git@github\.com:/, "github.com/")
44
+ .replace(/^https?:\/\//, "")
45
+ .replace(/\.git$/, "")
46
+ .split("/")
47
+ .filter(Boolean)
48
+ .pop();
49
+ }
50
+
51
+ function uuid(value) {
52
+ if (typeof value !== "string") return "";
53
+ const trimmed = value.trim();
54
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)
55
+ ? trimmed
56
+ : "";
57
+ }
58
+
59
+ function sessionDurationMinutes(startedAt, submittedAt) {
60
+ if (!startedAt) return 0;
61
+ const startMs = Date.parse(startedAt);
62
+ const endMs = Date.parse(submittedAt) || Date.now();
63
+ if (Number.isNaN(startMs) || endMs <= startMs) return 0;
64
+ return Math.round((endMs - startMs) / 60000);
65
+ }
66
+
67
+ function recentCommitHashes(cwd) {
68
+ const out = git(["log", "--oneline", "--since=8 hours ago", "--format=%h"], cwd);
69
+ return out ? out.split("\n").filter(Boolean) : [];
70
+ }
71
+
72
+ function buildPayload(options = {}) {
73
+ const cwd = options.cwd || process.cwd();
74
+ const home = options.home || os.homedir();
75
+ const env = options.env || process.env;
76
+ const trackingPath = options.trackingPath || path.join(cwd, ".planning", "tracking.json");
77
+ const configPath = options.configPath || path.join(qualiaHome(home), ".qualia-config.json");
78
+ const reportFile = options.reportFile || env.REPORT_FILE;
79
+ const submittedAt = env.SUBMITTED_AT || new Date().toISOString();
80
+
81
+ const tracking = readJson(trackingPath, {});
82
+ const config = readJson(configPath, {});
83
+ const notes = readText(reportFile, "").substring(0, 60000);
84
+ const gitRemote = tracking.git_remote || git(["config", "--get", "remote.origin.url"], cwd);
85
+ const projectKey = tracking.project_id || repoSlug(gitRemote) || path.basename(cwd);
86
+ const phase = tracking.phase;
87
+
88
+ return {
89
+ project: tracking.project || path.basename(cwd),
90
+ project_id: projectKey,
91
+ team_id: tracking.team_id || "qualia-solutions",
92
+ git_remote: gitRemote,
93
+ ...(uuid(tracking.erp_project_id) ? { erp_project_id: uuid(tracking.erp_project_id) } : {}),
94
+ ...(uuid(tracking.client_id) ? { client_id: uuid(tracking.client_id) } : {}),
95
+ ...(uuid(tracking.workspace_id) ? { workspace_id: uuid(tracking.workspace_id) } : {}),
96
+ client: tracking.client || "",
97
+ client_report_id: env.CLIENT_REPORT_ID || "",
98
+ framework_version: config.version || "",
99
+ milestone: tracking.milestone || 1,
100
+ milestone_name: tracking.milestone_name || "",
101
+ milestones: Array.isArray(tracking.milestones) ? tracking.milestones : [],
102
+ phase,
103
+ phase_name: tracking.phase_name,
104
+ total_phases: tracking.total_phases,
105
+ status: tracking.status,
106
+ tasks_done: tracking.tasks_done || 0,
107
+ tasks_total: tracking.tasks_total || 0,
108
+ verification: tracking.verification || "pending",
109
+ gap_cycles: (tracking.gap_cycles || {})[String(phase)] || 0,
110
+ build_count: tracking.build_count || 0,
111
+ deploy_count: tracking.deploy_count || 0,
112
+ deployed_url: tracking.deployed_url || "",
113
+ ...(tracking.session_started_at ? { session_started_at: tracking.session_started_at } : {}),
114
+ ...(tracking.last_pushed_at ? { last_pushed_at: tracking.last_pushed_at } : {}),
115
+ session_duration_minutes: sessionDurationMinutes(tracking.session_started_at, submittedAt),
116
+ lifetime: tracking.lifetime || {},
117
+ commits: recentCommitHashes(cwd),
118
+ notes,
119
+ submitted_by: env.SUBMITTED_BY || "unknown",
120
+ submitted_at: submittedAt,
121
+ };
122
+ }
123
+
124
+ if (require.main === module) {
125
+ try {
126
+ process.stdout.write(`${JSON.stringify(buildPayload())}\n`);
127
+ } catch (error) {
128
+ console.error(`report-payload failed: ${error.message}`);
129
+ process.exit(1);
130
+ }
131
+ }
132
+
133
+ module.exports = {
134
+ buildPayload,
135
+ repoSlug,
136
+ uuid,
137
+ };
package/bin/state.js CHANGED
@@ -145,7 +145,11 @@ function _pruneTraces(traceDir) {
145
145
 
146
146
  function _trace(event, result, data) {
147
147
  try {
148
- const traceDir = path.join(require("os").homedir(), ".claude", ".qualia-traces");
148
+ const os = require("os");
149
+ const parent = path.basename(path.dirname(__dirname));
150
+ const qualiaHome = process.env.QUALIA_HOME ||
151
+ (parent === ".codex" || parent === ".claude" ? path.dirname(__dirname) : path.join(os.homedir(), ".claude"));
152
+ const traceDir = path.join(qualiaHome, ".qualia-traces");
149
153
  if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
150
154
  // Prune ~1% of the time (cheap on most invocations, bounded over time).
151
155
  if (Math.random() < 0.01) _pruneTraces(traceDir);
@@ -867,6 +871,9 @@ function cmdInit(opts) {
867
871
  assigned_to: opts.assigned_to || (prevLife ? prevLife.assigned_to : ""),
868
872
  team_id: opts.team_id || (prevLife ? prevLife.team_id || "" : ""),
869
873
  project_id: opts.project_id || (prevLife ? prevLife.project_id || "" : ""),
874
+ erp_project_id: opts.erp_project_id || (prevLife ? prevLife.erp_project_id || "" : ""),
875
+ client_id: opts.client_id || (prevLife ? prevLife.client_id || "" : ""),
876
+ workspace_id: opts.workspace_id || (prevLife ? prevLife.workspace_id || "" : ""),
870
877
  git_remote: opts.git_remote || (prevLife ? prevLife.git_remote || "" : ""),
871
878
  milestone: prevLife ? prevLife.milestone : 1,
872
879
  milestone_name: opts.milestone_name || (prevLife ? prevLife.milestone_name || "" : ""),
package/bin/statusline.js CHANGED
@@ -13,6 +13,18 @@ const path = require("path");
13
13
  const { spawnSync } = require("child_process");
14
14
  const HOME = os.homedir();
15
15
 
16
+ function qualiaHome() {
17
+ if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
18
+ const normalized = __dirname.split(path.sep);
19
+ const parent = path.basename(path.dirname(__dirname));
20
+ if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
21
+ const claude = path.join(HOME, ".claude");
22
+ if (fs.existsSync(path.join(claude, ".qualia-config.json"))) return claude;
23
+ return claude;
24
+ }
25
+
26
+ const QUALIA_HOME = qualiaHome();
27
+
16
28
  // ─── Colors (matches bin/qualia-ui.js palette) ───────────
17
29
  const TEAL = "\x1b[38;2;0;206;209m";
18
30
  const TEAL_GLOW = "\x1b[38;2;0;170;175m";
@@ -230,7 +242,7 @@ try {
230
242
  // BOTH forward and backward slashes so Windows installs (where DIR contains
231
243
  // `\`) get a correct key and the memory count renders.
232
244
  const dirKey = DIR.replace(/[\/\\]/g, "-");
233
- const memDir = path.join(HOME, ".claude", "projects", dirKey, "memory");
245
+ const memDir = path.join(QUALIA_HOME, "projects", dirKey, "memory");
234
246
  if (fs.existsSync(memDir)) {
235
247
  const files = fs.readdirSync(memDir).filter(f => f.endsWith(".md") && f !== "MEMORY.md");
236
248
  MEMORY_COUNT = files.length;
@@ -243,7 +255,7 @@ try {
243
255
  // missing (pre-install, broken install, or running outside a Qualia env).
244
256
  let QUALIA_FIRST_NAME = "";
245
257
  try {
246
- const configPath = path.join(HOME, ".claude", ".qualia-config.json");
258
+ const configPath = path.join(QUALIA_HOME, ".qualia-config.json");
247
259
  if (fs.existsSync(configPath)) {
248
260
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
249
261
  const fullName = String(cfg.installed_by || "").trim();