qualia-framework 6.1.0 → 6.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -26
- package/agents/roadmapper.md +1 -1
- package/bin/cli.js +339 -200
- package/bin/codex-goal.js +92 -0
- package/bin/erp-retry.js +11 -3
- package/bin/install.js +483 -55
- package/bin/knowledge-flush.js +25 -13
- package/bin/knowledge.js +11 -1
- package/bin/project-snapshot.js +293 -0
- package/bin/qualia-ui.js +13 -2
- package/bin/report-payload.js +137 -0
- package/bin/state.js +8 -1
- package/bin/statusline.js +14 -2
- package/docs/changelog-v6.html +864 -0
- package/docs/ecosystem-operating-model.md +121 -0
- package/docs/erp-contract.md +74 -21
- package/docs/onboarding.html +1 -1
- package/docs/release.md +44 -0
- package/docs/reviews/v6.2.1-revival-audit.md +53 -0
- package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
- package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
- package/guide.md +16 -4
- package/hooks/auto-update.js +14 -7
- package/hooks/branch-guard.js +10 -2
- package/hooks/env-empty-guard.js +10 -1
- package/hooks/git-guardrails.js +10 -1
- package/hooks/migration-guard.js +4 -1
- package/hooks/pre-deploy-gate.js +38 -1
- package/hooks/pre-push.js +56 -157
- package/hooks/session-start.js +22 -14
- package/hooks/stop-session-log.js +11 -3
- package/hooks/supabase-destructive-guard.js +11 -1
- package/hooks/vercel-account-guard.js +12 -3
- package/package.json +3 -2
- package/rules/codex-goal.md +46 -0
- package/skills/qualia-build/SKILL.md +4 -0
- package/skills/qualia-feature/SKILL.md +4 -0
- package/skills/qualia-map/SKILL.md +1 -1
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +4 -0
- package/skills/qualia-polish/SKILL.md +2 -2
- package/skills/qualia-report/SKILL.md +6 -43
- package/skills/qualia-road/SKILL.md +1 -1
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/help.html +1 -1
- package/templates/knowledge/agents.md +3 -3
- package/templates/knowledge/index.md +1 -1
- package/templates/tracking.json +3 -0
- package/templates/work-packet.md +46 -0
- package/tests/bin.test.sh +411 -13
- package/tests/hooks.test.sh +1 -8
- package/tests/install-smoke.test.sh +137 -0
- package/tests/published-install-smoke.test.sh +126 -0
- package/tests/refs.test.sh +42 -0
- package/tests/run-all.sh +1 -0
- package/tests/runner.js +19 -33
- package/tests/state.test.sh +4 -1
- package/hooks/pre-compact.js +0 -127
package/bin/knowledge-flush.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// CI/scheduled job) without an interactive Claude Code session. Closes the
|
|
6
6
|
// memory loop end-to-end:
|
|
7
7
|
//
|
|
8
|
-
// Stop hook (auto, every turn) →
|
|
9
|
-
// THIS SCRIPT (weekly cron) → spawns
|
|
8
|
+
// Stop hook (auto, every turn) → <install-home>/knowledge/daily-log/{date}.md
|
|
9
|
+
// THIS SCRIPT (weekly cron) → spawns the installed agent CLI
|
|
10
10
|
// /qualia-flush → promotes raw → curated tier
|
|
11
11
|
// bin/knowledge.js (every spawn) → reads index.md → reaches the right file
|
|
12
12
|
//
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
// 0 3 * * 0 node ~/.claude/bin/knowledge-flush.js >> ~/.claude/.qualia-flush.log 2>&1
|
|
21
21
|
//
|
|
22
22
|
// Behavior:
|
|
23
|
-
// • If
|
|
23
|
+
// • If the required agent CLI isn't on PATH, exits 0 with a logged warning. Cron
|
|
24
24
|
// spam is worse than a missed flush — a real failure surfaces in the
|
|
25
25
|
// log file the user is presumably watching.
|
|
26
26
|
// • If the daily-log dir is empty (nothing to flush), exits 0 silently.
|
|
27
|
-
// • If
|
|
27
|
+
// • If the agent CLI returns non-zero, exits 1 with the error captured in
|
|
28
28
|
// the log so cron can be configured to alert on it.
|
|
29
29
|
// • Writes one structured JSONL line per run to ~/.claude/.qualia-flush.log
|
|
30
30
|
// so the user can audit "when did the last 5 flushes run, what did they
|
|
@@ -38,9 +38,17 @@ const os = require("os");
|
|
|
38
38
|
const { spawnSync } = require("child_process");
|
|
39
39
|
|
|
40
40
|
const HOME = os.homedir();
|
|
41
|
-
|
|
41
|
+
function qualiaHome() {
|
|
42
|
+
if (process.env.QUALIA_HOME) return process.env.QUALIA_HOME;
|
|
43
|
+
const parent = path.basename(path.dirname(__dirname));
|
|
44
|
+
if (parent === ".codex" || parent === ".claude") return path.dirname(__dirname);
|
|
45
|
+
return path.join(HOME, ".claude");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const QUALIA_HOME = qualiaHome();
|
|
49
|
+
const KNOWLEDGE_DIR = path.join(QUALIA_HOME, "knowledge");
|
|
42
50
|
const DAILY_DIR = path.join(KNOWLEDGE_DIR, "daily-log");
|
|
43
|
-
const LOG_FILE = path.join(
|
|
51
|
+
const LOG_FILE = path.join(QUALIA_HOME, ".qualia-flush.log");
|
|
44
52
|
|
|
45
53
|
const _start = Date.now();
|
|
46
54
|
|
|
@@ -103,9 +111,11 @@ function dailyLogHasRecentEntries(windowDays) {
|
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
// ── Preflight ────────────────────────────────────────────
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
const IS_CODEX_INSTALL = path.basename(QUALIA_HOME) === ".codex";
|
|
115
|
+
const agentCli = IS_CODEX_INSTALL ? "codex" : "claude";
|
|
116
|
+
const agentBin = which(agentCli);
|
|
117
|
+
if (!agentBin) {
|
|
118
|
+
logEvent({ event: "skipped", reason: `${agentCli}-cli-not-on-path` });
|
|
109
119
|
// Exit 0 — a missing CLI on the host running cron is a config issue, not
|
|
110
120
|
// a flush failure. Don't spam alerts.
|
|
111
121
|
process.exit(0);
|
|
@@ -117,11 +127,13 @@ if (!dailyLogHasRecentEntries(days)) {
|
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
// ── Run ──────────────────────────────────────────────────
|
|
120
|
-
// `claude -p "<prompt>"`
|
|
121
|
-
// invocation matches what the user would
|
|
130
|
+
// `claude -p "<prompt>"` and `codex exec "<prompt>"` run a single
|
|
131
|
+
// non-interactive turn. The skill body invocation matches what the user would
|
|
132
|
+
// type at the prompt.
|
|
122
133
|
const prompt = `/qualia-flush ${argv.join(" ")}`.trim();
|
|
123
134
|
|
|
124
|
-
const
|
|
135
|
+
const cliArgs = IS_CODEX_INSTALL ? ["exec", prompt] : ["-p", prompt];
|
|
136
|
+
const result = spawnSync(agentBin, cliArgs, {
|
|
125
137
|
encoding: "utf8",
|
|
126
138
|
timeout: 5 * 60 * 1000, // 5 min hard cap — flush should never take this long
|
|
127
139
|
shell: process.platform === "win32",
|
|
@@ -140,7 +152,7 @@ if (status !== 0) {
|
|
|
140
152
|
stderr_tail: stderr.slice(-1000),
|
|
141
153
|
});
|
|
142
154
|
// Surface to stderr so cron's MAILTO sends an alert.
|
|
143
|
-
console.error(`knowledge-flush:
|
|
155
|
+
console.error(`knowledge-flush: ${agentCli} exited ${status}`);
|
|
144
156
|
if (stderr) console.error(stderr.slice(-2000));
|
|
145
157
|
process.exit(1);
|
|
146
158
|
}
|
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();
|