qualia-framework 6.1.0 → 6.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -26
- package/agents/roadmapper.md +1 -1
- package/bin/cli.js +339 -200
- package/bin/erp-retry.js +11 -3
- package/bin/install.js +383 -55
- package/bin/knowledge-flush.js +25 -13
- package/bin/knowledge.js +11 -1
- package/bin/project-snapshot.js +293 -0
- package/bin/qualia-ui.js +13 -2
- package/bin/report-payload.js +137 -0
- package/bin/state.js +8 -1
- package/bin/statusline.js +14 -2
- package/docs/changelog-v6.html +864 -0
- package/docs/ecosystem-operating-model.md +121 -0
- package/docs/erp-contract.md +74 -21
- package/docs/onboarding.html +1 -1
- package/docs/release.md +44 -0
- package/docs/reviews/v6.2.1-revival-audit.md +53 -0
- package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
- package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
- package/guide.md +16 -4
- package/hooks/auto-update.js +14 -7
- package/hooks/branch-guard.js +10 -2
- package/hooks/env-empty-guard.js +10 -1
- package/hooks/git-guardrails.js +10 -1
- package/hooks/migration-guard.js +4 -1
- package/hooks/pre-deploy-gate.js +11 -1
- package/hooks/pre-push.js +42 -162
- package/hooks/session-start.js +22 -14
- package/hooks/stop-session-log.js +11 -3
- package/hooks/supabase-destructive-guard.js +11 -1
- package/hooks/vercel-account-guard.js +12 -3
- package/package.json +3 -2
- package/skills/qualia-map/SKILL.md +1 -1
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-polish/SKILL.md +2 -2
- package/skills/qualia-report/SKILL.md +6 -43
- package/skills/qualia-road/SKILL.md +1 -1
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/help.html +1 -1
- package/templates/knowledge/agents.md +3 -3
- package/templates/knowledge/index.md +1 -1
- package/templates/tracking.json +3 -0
- package/templates/work-packet.md +46 -0
- package/tests/bin.test.sh +411 -13
- package/tests/hooks.test.sh +1 -8
- package/tests/install-smoke.test.sh +137 -0
- package/tests/published-install-smoke.test.sh +126 -0
- package/tests/refs.test.sh +42 -0
- package/tests/run-all.sh +1 -0
- package/tests/runner.js +19 -33
- package/tests/state.test.sh +4 -1
- package/hooks/pre-compact.js +0 -127
package/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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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();
|