qualia-framework 4.4.0 → 5.1.0
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/AGENTS.md +24 -0
- package/CLAUDE.md +12 -63
- package/README.md +24 -18
- package/agents/builder.md +13 -33
- package/agents/plan-checker.md +18 -0
- package/agents/planner.md +17 -0
- package/agents/verifier.md +70 -0
- package/agents/visual-evaluator.md +132 -0
- package/bin/cli.js +64 -23
- package/bin/install.js +375 -29
- package/bin/qualia-ui.js +208 -1
- package/bin/slop-detect.mjs +362 -0
- package/bin/state.js +218 -2
- package/docs/erp-contract.md +5 -0
- package/docs/install-redesign-builder-prompt.md +290 -0
- package/docs/install-redesign-pilot.md +234 -0
- package/docs/playwright-loop-builder-prompt.md +185 -0
- package/docs/playwright-loop-design-notes.md +108 -0
- package/docs/playwright-loop-pilot-results.md +170 -0
- package/docs/playwright-loop-review-2026-05-03.md +65 -0
- package/docs/playwright-loop-tester-prompt.md +213 -0
- package/docs/reviews/matt-pocock-skills-analysis.md +300 -0
- package/guide.md +9 -5
- package/hooks/env-empty-guard.js +74 -0
- package/hooks/pre-compact.js +19 -9
- package/hooks/pre-deploy-gate.js +8 -2
- package/hooks/pre-push.js +26 -12
- package/hooks/supabase-destructive-guard.js +62 -0
- package/hooks/vercel-account-guard.js +91 -0
- package/package.json +2 -1
- package/rules/design-brand.md +114 -0
- package/rules/design-laws.md +148 -0
- package/rules/design-product.md +114 -0
- package/rules/design-rubric.md +157 -0
- package/rules/grounding.md +4 -0
- package/skills/qualia-build/SKILL.md +40 -46
- package/skills/qualia-discuss/SKILL.md +51 -68
- package/skills/qualia-handoff/SKILL.md +1 -0
- package/skills/qualia-issues/SKILL.md +151 -0
- package/skills/qualia-map/SKILL.md +78 -35
- package/skills/qualia-new/REFERENCE.md +139 -0
- package/skills/qualia-new/SKILL.md +85 -124
- package/skills/qualia-optimize/REFERENCE.md +202 -0
- package/skills/qualia-optimize/SKILL.md +72 -237
- package/skills/qualia-plan/SKILL.md +58 -65
- package/skills/qualia-polish/SKILL.md +180 -136
- package/skills/qualia-polish-loop/REFERENCE.md +265 -0
- package/skills/qualia-polish-loop/SKILL.md +201 -0
- package/skills/qualia-polish-loop/fixtures/broken.html +117 -0
- package/skills/qualia-polish-loop/fixtures/clean.html +196 -0
- package/skills/qualia-polish-loop/scripts/loop.mjs +302 -0
- package/skills/qualia-polish-loop/scripts/playwright-capture.mjs +197 -0
- package/skills/qualia-polish-loop/scripts/score.mjs +176 -0
- package/skills/qualia-report/SKILL.md +141 -180
- package/skills/qualia-research/SKILL.md +28 -33
- package/skills/qualia-road/SKILL.md +103 -0
- package/skills/qualia-ship/SKILL.md +1 -0
- package/skills/qualia-task/SKILL.md +1 -1
- package/skills/qualia-test/SKILL.md +50 -2
- package/skills/qualia-triage/SKILL.md +152 -0
- package/skills/qualia-verify/SKILL.md +63 -104
- package/skills/qualia-zoom/SKILL.md +51 -0
- package/skills/zoho-workflow/SKILL.md +64 -0
- package/templates/CONTEXT.md +36 -0
- package/templates/DESIGN.md +229 -435
- package/templates/PRODUCT.md +95 -0
- package/templates/decisions/ADR-template.md +30 -0
- package/tests/bin.test.sh +451 -7
- package/tests/state.test.sh +58 -0
- package/skills/qualia-design/SKILL.md +0 -169
package/bin/cli.js
CHANGED
|
@@ -160,10 +160,15 @@ const QUALIA_AGENT_FILES = [
|
|
|
160
160
|
];
|
|
161
161
|
|
|
162
162
|
// 3 Qualia bin scripts.
|
|
163
|
-
const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js", "knowledge.js", "knowledge-flush.js", "plan-contract.js", "agent-runs.js"];
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
163
|
+
const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js", "knowledge.js", "knowledge-flush.js", "plan-contract.js", "agent-runs.js", "slop-detect.mjs"];
|
|
164
|
+
|
|
165
|
+
// Qualia rules — security, deployment, infra, grounding, plus the v4.5.0 design substrate.
|
|
166
|
+
// frontend.md and design-reference.md are kept for backward compat; new projects use design-laws/brand/product/rubric.
|
|
167
|
+
const QUALIA_RULE_FILES = [
|
|
168
|
+
"security.md", "deployment.md", "infrastructure.md", "grounding.md",
|
|
169
|
+
"frontend.md", "design-reference.md",
|
|
170
|
+
"design-laws.md", "design-brand.md", "design-product.md", "design-rubric.md",
|
|
171
|
+
];
|
|
167
172
|
|
|
168
173
|
function promptYesNo(question, defaultYes) {
|
|
169
174
|
return new Promise((resolve) => {
|
|
@@ -819,7 +824,7 @@ function cmdAnalytics() {
|
|
|
819
824
|
// validity, and endpoint health. Uses a distinct dry_run=true flag in the
|
|
820
825
|
// payload so receivers can filter these out of real report views.
|
|
821
826
|
|
|
822
|
-
function cmdErpPing() {
|
|
827
|
+
async function cmdErpPing() {
|
|
823
828
|
banner();
|
|
824
829
|
console.log("");
|
|
825
830
|
|
|
@@ -882,22 +887,45 @@ function cmdErpPing() {
|
|
|
882
887
|
dry_run: true,
|
|
883
888
|
});
|
|
884
889
|
|
|
890
|
+
// v5.0 — use Node's native https.request instead of `curl -H "Authorization: Bearer $KEY"`.
|
|
891
|
+
// Reason: passing the bearer token as a curl CLI argument exposes it via /proc/<pid>/cmdline,
|
|
892
|
+
// readable by any local process during the curl invocation. https.request keeps the auth
|
|
893
|
+
// header in-process — never visible to other users.
|
|
894
|
+
const httpsLib = require("https");
|
|
895
|
+
const httpLib = require("http");
|
|
896
|
+
const urlLib = require("url");
|
|
897
|
+
const u = urlLib.parse(`${erpUrl}/api/v1/reports`);
|
|
898
|
+
const lib = u.protocol === "https:" ? httpsLib : httpLib;
|
|
885
899
|
const started = Date.now();
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
900
|
+
const { code: httpCode, body, error: reqErr } = await new Promise((resolve) => {
|
|
901
|
+
const req = lib.request({
|
|
902
|
+
method: "POST",
|
|
903
|
+
hostname: u.hostname,
|
|
904
|
+
port: u.port || (u.protocol === "https:" ? 443 : 80),
|
|
905
|
+
path: u.path,
|
|
906
|
+
headers: {
|
|
907
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
908
|
+
"Content-Type": "application/json",
|
|
909
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
910
|
+
},
|
|
911
|
+
timeout: 10000,
|
|
912
|
+
}, (res) => {
|
|
913
|
+
let chunks = "";
|
|
914
|
+
res.setEncoding("utf8");
|
|
915
|
+
res.on("data", (c) => { chunks += c; });
|
|
916
|
+
res.on("end", () => resolve({ code: String(res.statusCode), body: chunks.trim(), error: null }));
|
|
917
|
+
});
|
|
918
|
+
req.on("error", (e) => resolve({ code: "—", body: "", error: e.message }));
|
|
919
|
+
req.on("timeout", () => { req.destroy(new Error("timeout")); });
|
|
920
|
+
req.write(payload);
|
|
921
|
+
req.end();
|
|
922
|
+
});
|
|
895
923
|
|
|
896
924
|
const duration = Date.now() - started;
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
925
|
+
if (reqErr) {
|
|
926
|
+
console.log(` ${RED}✗${RESET} Network error: ${reqErr}`);
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
901
929
|
|
|
902
930
|
console.log(` ${DIM}Response:${RESET} ${WHITE}HTTP ${httpCode}${RESET} ${DIM}(${duration}ms)${RESET}`);
|
|
903
931
|
if (body) {
|
|
@@ -951,16 +979,29 @@ function cmdSetErpKey() {
|
|
|
951
979
|
return;
|
|
952
980
|
}
|
|
953
981
|
|
|
954
|
-
|
|
955
|
-
|
|
982
|
+
// v5.0 — refuse positional argument for ERP key. Positional args leak into
|
|
983
|
+
// shell history (~/.bash_history, ~/.zsh_history) where any local user with
|
|
984
|
+
// file access can read them. Read from stdin only (piped or env-piped).
|
|
985
|
+
const positional = rawArgs.find((a) => a && !a.startsWith("--"));
|
|
986
|
+
if (positional) {
|
|
987
|
+
console.log(` ${RED}✗${RESET} Refusing to accept ERP key as a positional CLI argument.`);
|
|
988
|
+
console.log(` ${DIM}Reason:${RESET} positional args land in shell history (~/.bash_history, ~/.zsh_history).`);
|
|
989
|
+
console.log(` ${DIM}Safe usage:${RESET} ${TEAL}printf '%s' "\$QUALIA_ERP_KEY" | qualia-framework set-erp-key${RESET}`);
|
|
990
|
+
console.log(` ${DIM}Or piped:${RESET} ${TEAL}cat /tmp/key | qualia-framework set-erp-key${RESET} ${DIM}(then shred /tmp/key)${RESET}`);
|
|
991
|
+
console.log("");
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
let key = "";
|
|
996
|
+
if (!process.stdin.isTTY) {
|
|
956
997
|
try { key = fs.readFileSync(0, "utf8").trim(); } catch {}
|
|
957
998
|
}
|
|
958
999
|
|
|
959
1000
|
key = String(key || "").trim();
|
|
960
1001
|
if (!key) {
|
|
961
1002
|
console.log(` ${RED}✗${RESET} Missing ERP API key.`);
|
|
962
|
-
console.log(` ${DIM}Usage:${RESET} qualia-framework set-erp-key
|
|
963
|
-
console.log(` ${DIM}
|
|
1003
|
+
console.log(` ${DIM}Usage:${RESET} ${TEAL}printf '%s' "\$QUALIA_ERP_KEY" | qualia-framework set-erp-key${RESET}`);
|
|
1004
|
+
console.log(` ${DIM}Or:${RESET} ${TEAL}cat /tmp/key | qualia-framework set-erp-key${RESET} ${DIM}(then shred /tmp/key)${RESET}`);
|
|
964
1005
|
console.log("");
|
|
965
1006
|
process.exit(1);
|
|
966
1007
|
}
|
|
@@ -1215,7 +1256,7 @@ function cmdHelp() {
|
|
|
1215
1256
|
console.log(` ${TG}/qualia-plan${RESET} Plan a phase`);
|
|
1216
1257
|
console.log(` ${TG}/qualia-build${RESET} Build it (parallel tasks)`);
|
|
1217
1258
|
console.log(` ${TG}/qualia-verify${RESET} Verify it works`);
|
|
1218
|
-
console.log(` ${TG}/qualia-
|
|
1259
|
+
console.log(` ${TG}/qualia-polish${RESET} Design pass — any scope (component, route, app, redesign)`);
|
|
1219
1260
|
console.log(` ${TG}/qualia-debug${RESET} Structured debugging`);
|
|
1220
1261
|
console.log(` ${TG}/qualia-review${RESET} Production audit`);
|
|
1221
1262
|
console.log(` ${TG}/qualia-ship${RESET} Deploy to production`);
|
package/bin/install.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
const { createInterface } = require("readline");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fs = require("fs");
|
|
6
|
+
const ui = require("./qualia-ui.js");
|
|
6
7
|
|
|
7
|
-
// ─── Colors
|
|
8
|
+
// ─── Colors (kept for legacy log lines; new sections route through qualia-ui) ─
|
|
8
9
|
const TEAL = "\x1b[38;2;0;206;209m";
|
|
9
10
|
const DIM = "\x1b[38;2;80;90;100m";
|
|
11
|
+
const DIM2 = "\x1b[38;2;70;80;90m";
|
|
10
12
|
const GREEN = "\x1b[38;2;52;211;153m";
|
|
11
13
|
const WHITE = "\x1b[38;2;220;225;230m";
|
|
12
14
|
const YELLOW = "\x1b[38;2;234;179;8m";
|
|
@@ -14,8 +16,17 @@ const RED = "\x1b[38;2;239;68;68m";
|
|
|
14
16
|
const RESET = "\x1b[0m";
|
|
15
17
|
|
|
16
18
|
const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
|
|
19
|
+
const CODEX_DIR = path.join(require("os").homedir(), ".codex");
|
|
17
20
|
const FRAMEWORK_DIR = path.resolve(__dirname, "..");
|
|
18
21
|
|
|
22
|
+
// Target IDs match the menu numbers shown to the user.
|
|
23
|
+
const TARGET_CLAUDE_ONLY = "1";
|
|
24
|
+
const TARGET_CODEX_ONLY = "2";
|
|
25
|
+
const TARGET_BOTH = "3";
|
|
26
|
+
|
|
27
|
+
// Total install timer — set in main(), read by the final summary card.
|
|
28
|
+
const installStart = Date.now();
|
|
29
|
+
|
|
19
30
|
// ─── Team codes ──────────────────────────────────────────
|
|
20
31
|
const DEFAULT_TEAM = {
|
|
21
32
|
"QS-FAWZI-01": {
|
|
@@ -64,16 +75,30 @@ const TEAM = loadTeam();
|
|
|
64
75
|
let installed = 0;
|
|
65
76
|
let errors = 0;
|
|
66
77
|
|
|
78
|
+
// Per-section timer state. Started by printSection(), read by closeSection().
|
|
79
|
+
let sectionStart = 0;
|
|
80
|
+
let sectionLabel = "";
|
|
81
|
+
let sectionCount = 0;
|
|
82
|
+
|
|
67
83
|
function log(msg) {
|
|
68
84
|
console.log(` ${msg}`);
|
|
69
85
|
}
|
|
70
86
|
function ok(label) {
|
|
71
87
|
installed++;
|
|
88
|
+
sectionCount++;
|
|
72
89
|
log(`${GREEN}✓${RESET} ${label}`);
|
|
73
90
|
}
|
|
74
91
|
function warn(label) {
|
|
75
92
|
errors++;
|
|
76
|
-
log(`${YELLOW}
|
|
93
|
+
log(`${YELLOW}!${RESET} ${label}`);
|
|
94
|
+
}
|
|
95
|
+
// step(text) → handle returned by qualia-ui. Use for slow ops where the
|
|
96
|
+
// "doing → done" lifecycle helps the user trust the install isn't hung.
|
|
97
|
+
function step(text) {
|
|
98
|
+
return ui.step(text);
|
|
99
|
+
}
|
|
100
|
+
function spin(text) {
|
|
101
|
+
return ui.spinner(text);
|
|
77
102
|
}
|
|
78
103
|
function copy(src, dest) {
|
|
79
104
|
const destDir = path.dirname(dest);
|
|
@@ -161,23 +186,135 @@ function printHeader() {
|
|
|
161
186
|
}
|
|
162
187
|
|
|
163
188
|
function printSection(title) {
|
|
189
|
+
// Close prior section if one was open (shows count + elapsed).
|
|
190
|
+
closeSection();
|
|
191
|
+
sectionStart = Date.now();
|
|
192
|
+
sectionLabel = title;
|
|
193
|
+
sectionCount = 0;
|
|
164
194
|
console.log("");
|
|
165
195
|
console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}${title}${RESET}`);
|
|
166
|
-
console.log(` ${
|
|
196
|
+
console.log(` ${DIM2}${"─".repeat(40)}${RESET}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function closeSection() {
|
|
200
|
+
if (!sectionLabel) return;
|
|
201
|
+
const elapsedMs = Date.now() - sectionStart;
|
|
202
|
+
const elapsed = elapsedMs >= 1000
|
|
203
|
+
? `${(elapsedMs / 1000).toFixed(1)}s`
|
|
204
|
+
: `${elapsedMs}ms`;
|
|
205
|
+
if (sectionCount > 0) {
|
|
206
|
+
console.log(` ${DIM2}└─${RESET} ${DIM}${sectionCount} ${sectionLabel.toLowerCase()} · ${elapsed}${RESET}`);
|
|
207
|
+
}
|
|
208
|
+
sectionLabel = "";
|
|
209
|
+
sectionCount = 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Prompt helpers ──────────────────────────────────────
|
|
213
|
+
// Two prompts (team code, install target) need to coexist with two stdin
|
|
214
|
+
// modes: interactive TTY and piped. The earlier two-readline approach
|
|
215
|
+
// raced 'close' against the second question on piped installs (`echo CODE
|
|
216
|
+
// | install.js` would EOF before the second question could attach a line
|
|
217
|
+
// listener) — the target prompt always saw "" and defaulted to "1" even
|
|
218
|
+
// when the user piped "CODE\n2\n".
|
|
219
|
+
//
|
|
220
|
+
// Fix: in piped mode, pre-buffer all stdin lines synchronously up front and
|
|
221
|
+
// hand them out one by one. In TTY mode, use a shared readline.
|
|
222
|
+
let SHARED_RL = null;
|
|
223
|
+
let PIPED_LINES = null;
|
|
224
|
+
let PIPED_INDEX = 0;
|
|
225
|
+
const IS_INTERACTIVE = !!(process.stdin && process.stdin.isTTY);
|
|
226
|
+
|
|
227
|
+
function getRl() {
|
|
228
|
+
if (!SHARED_RL) {
|
|
229
|
+
SHARED_RL = createInterface({ input: process.stdin, output: process.stdout });
|
|
230
|
+
}
|
|
231
|
+
return SHARED_RL;
|
|
232
|
+
}
|
|
233
|
+
function closeRl() {
|
|
234
|
+
if (SHARED_RL) { try { SHARED_RL.close(); } catch {} SHARED_RL = null; }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Read every available stdin line into an array. Resolves immediately on
|
|
238
|
+
// 'end'. Used only when stdin is piped (legacy `echo ... | install`).
|
|
239
|
+
function bufferStdin() {
|
|
240
|
+
return new Promise((resolve) => {
|
|
241
|
+
let buf = "";
|
|
242
|
+
process.stdin.setEncoding("utf8");
|
|
243
|
+
process.stdin.on("data", (chunk) => { buf += chunk; });
|
|
244
|
+
process.stdin.on("end", () => {
|
|
245
|
+
const lines = buf.split(/\r?\n/);
|
|
246
|
+
// Trim a trailing empty entry from the final newline, but preserve
|
|
247
|
+
// intentional empties (so an empty target line still defaults to "1"
|
|
248
|
+
// rather than swallowing the team-code line).
|
|
249
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
250
|
+
resolve(lines);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function nextPipedLine() {
|
|
256
|
+
if (!PIPED_LINES) return "";
|
|
257
|
+
if (PIPED_INDEX >= PIPED_LINES.length) return "";
|
|
258
|
+
return PIPED_LINES[PIPED_INDEX++];
|
|
167
259
|
}
|
|
168
260
|
|
|
169
|
-
// ─── Prompt for code ─────────────────────────────────────
|
|
170
261
|
function askCode() {
|
|
171
262
|
return new Promise((resolve) => {
|
|
172
|
-
|
|
263
|
+
if (!IS_INTERACTIVE) {
|
|
264
|
+
printHeader();
|
|
265
|
+
const line = nextPipedLine();
|
|
266
|
+
// Echo the prompt + answer for log readability.
|
|
267
|
+
process.stdout.write(` ${WHITE}Enter install code:${RESET} ${line}\n`);
|
|
268
|
+
resolve(String(line || "").trim());
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const rl = getRl();
|
|
173
272
|
printHeader();
|
|
174
273
|
rl.question(` ${WHITE}Enter install code:${RESET} `, (answer) => {
|
|
175
|
-
|
|
176
|
-
resolve(answer.trim());
|
|
274
|
+
resolve(String(answer || "").trim());
|
|
177
275
|
});
|
|
178
276
|
});
|
|
179
277
|
}
|
|
180
278
|
|
|
279
|
+
// ─── Prompt for install target (Claude / Codex / Both) ──
|
|
280
|
+
// Backward-compat: a piped install with only the team code (single stdin
|
|
281
|
+
// line) closes stdin before this prompt; we silently default to "1"
|
|
282
|
+
// (Claude only) so existing scripts keep working untouched.
|
|
283
|
+
function askTarget() {
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
console.log("");
|
|
286
|
+
console.log(` ${WHITE}Where would you like to install Qualia?${RESET}`);
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log(` ${TEAL}[1]${RESET} ${WHITE}Claude Code only${RESET} ${DIM}— recommended, full feature set${RESET}`);
|
|
289
|
+
console.log(` ${TEAL}[2]${RESET} ${WHITE}OpenAI Codex only${RESET} ${DIM}— AGENTS.md (Codex's open standard)${RESET}`);
|
|
290
|
+
console.log(` ${TEAL}[3]${RESET} ${WHITE}Both${RESET} ${DIM}— max compatibility${RESET}`);
|
|
291
|
+
console.log("");
|
|
292
|
+
|
|
293
|
+
const normalize = (val) => {
|
|
294
|
+
const trimmed = String(val || "").trim();
|
|
295
|
+
// Empty or unrecognized → default to Claude only (preserves legacy
|
|
296
|
+
// single-line piped install: `echo CODE | npx qualia-framework install`).
|
|
297
|
+
if (trimmed === TARGET_CODEX_ONLY || trimmed === TARGET_BOTH) return trimmed;
|
|
298
|
+
return TARGET_CLAUDE_ONLY;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
if (!IS_INTERACTIVE) {
|
|
302
|
+
const line = nextPipedLine();
|
|
303
|
+
process.stdout.write(` ${WHITE}Choice [1]:${RESET} ${line}\n`);
|
|
304
|
+
resolve(normalize(line));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const rl = getRl();
|
|
308
|
+
rl.question(` ${WHITE}Choice [1]:${RESET} `, (answer) => resolve(normalize(answer)));
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function targetLabel(t) {
|
|
313
|
+
if (t === TARGET_CODEX_ONLY) return "Codex";
|
|
314
|
+
if (t === TARGET_BOTH) return "Claude Code · Codex";
|
|
315
|
+
return "Claude Code";
|
|
316
|
+
}
|
|
317
|
+
|
|
181
318
|
// ─── Resolve team code (tolerates case + O/0 typo in suffix) ─
|
|
182
319
|
// Accepts "qs-fawzi-01", "QS-FAWZI-01", "QS-FAWZI-O1" (letter O in the
|
|
183
320
|
// numeric suffix), and returns the canonical key if found, else null.
|
|
@@ -196,6 +333,12 @@ function resolveTeamCode(input) {
|
|
|
196
333
|
|
|
197
334
|
// ─── Main ────────────────────────────────────────────────
|
|
198
335
|
async function main() {
|
|
336
|
+
// Piped install: drain stdin once up front. Avoids EOF/'close' racing
|
|
337
|
+
// ahead of the second prompt's 'line' listener (the bug v5.0 didn't have
|
|
338
|
+
// because v5.0 only had one prompt).
|
|
339
|
+
if (!IS_INTERACTIVE) {
|
|
340
|
+
PIPED_LINES = await bufferStdin();
|
|
341
|
+
}
|
|
199
342
|
const rawCode = await askCode();
|
|
200
343
|
const code = resolveTeamCode(rawCode);
|
|
201
344
|
const member = code ? TEAM[code] : null;
|
|
@@ -211,7 +354,23 @@ async function main() {
|
|
|
211
354
|
console.log("");
|
|
212
355
|
const roleColor = member.role === "OWNER" ? TEAL : GREEN;
|
|
213
356
|
console.log(` ${GREEN}✓${RESET} ${WHITE}${BOLD}Welcome, ${member.name}${RESET}`);
|
|
214
|
-
console.log(` ${DIM} Role:${RESET} ${roleColor}${member.role}${RESET}
|
|
357
|
+
console.log(` ${DIM} Role:${RESET} ${roleColor}${member.role}${RESET}`);
|
|
358
|
+
|
|
359
|
+
// ─── Ask install target (Claude / Codex / Both) ────────
|
|
360
|
+
const target = await askTarget();
|
|
361
|
+
closeRl();
|
|
362
|
+
const installClaude = target === TARGET_CLAUDE_ONLY || target === TARGET_BOTH;
|
|
363
|
+
const installCodexTarget = target === TARGET_CODEX_ONLY || target === TARGET_BOTH;
|
|
364
|
+
|
|
365
|
+
console.log("");
|
|
366
|
+
console.log(` ${DIM} Target:${RESET} ${WHITE}${targetLabel(target)}${RESET}`);
|
|
367
|
+
|
|
368
|
+
if (!installClaude) {
|
|
369
|
+
// Codex-only path: skip the entire Claude install block. Jump straight
|
|
370
|
+
// to the Codex installer + final summary.
|
|
371
|
+
await installCodex(member, target);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
215
374
|
|
|
216
375
|
// ─── Skills ──────────────────────────────────────────
|
|
217
376
|
const skillsDir = path.join(FRAMEWORK_DIR, "skills");
|
|
@@ -226,6 +385,21 @@ async function main() {
|
|
|
226
385
|
path.join(skillsDir, skill, "SKILL.md"),
|
|
227
386
|
path.join(CLAUDE_DIR, "skills", skill, "SKILL.md")
|
|
228
387
|
);
|
|
388
|
+
// Copy REFERENCE.md if the skill has one (progressive-disclosure pattern)
|
|
389
|
+
const refSrc = path.join(skillsDir, skill, "REFERENCE.md");
|
|
390
|
+
if (fs.existsSync(refSrc)) {
|
|
391
|
+
copy(refSrc, path.join(CLAUDE_DIR, "skills", skill, "REFERENCE.md"));
|
|
392
|
+
}
|
|
393
|
+
// v5.1: Copy scripts/ subfolder if present (e.g. qualia-polish-loop ships
|
|
394
|
+
// playwright-capture.mjs, loop.mjs, score.mjs that the skill invokes at
|
|
395
|
+
// runtime). Recursive — preserves nested files. fixtures/ also copied
|
|
396
|
+
// for self-test scenarios.
|
|
397
|
+
for (const sub of ["scripts", "fixtures"]) {
|
|
398
|
+
const subSrc = path.join(skillsDir, skill, sub);
|
|
399
|
+
if (fs.existsSync(subSrc) && fs.statSync(subSrc).isDirectory()) {
|
|
400
|
+
copyTree(subSrc, path.join(CLAUDE_DIR, "skills", skill, sub));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
229
403
|
ok(skill);
|
|
230
404
|
} catch (e) {
|
|
231
405
|
warn(`${skill} — ${e.message}`);
|
|
@@ -383,6 +557,17 @@ async function main() {
|
|
|
383
557
|
claudeMd = claudeMd.replace("{{ROLE}}", member.role);
|
|
384
558
|
claudeMd = claudeMd.replace("{{ROLE_DESCRIPTION}}", member.description);
|
|
385
559
|
const claudeDest = path.join(CLAUDE_DIR, "CLAUDE.md");
|
|
560
|
+
// v5.0: backup existing CLAUDE.md before overwrite. Users may have added
|
|
561
|
+
// personal instructions; without a backup, re-install silently destroys
|
|
562
|
+
// them. .bak files are harmless and easy to clean up.
|
|
563
|
+
if (fs.existsSync(claudeDest)) {
|
|
564
|
+
const existing = fs.readFileSync(claudeDest, "utf8");
|
|
565
|
+
if (existing !== claudeMd) {
|
|
566
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
567
|
+
const bak = `${claudeDest}.bak.${ts}`;
|
|
568
|
+
try { fs.copyFileSync(claudeDest, bak); ok(`Backed up existing CLAUDE.md → ${path.basename(bak)}`); } catch {}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
386
571
|
fs.writeFileSync(claudeDest, claudeMd, "utf8");
|
|
387
572
|
ok(`Configured as ${member.role}`);
|
|
388
573
|
} catch (e) {
|
|
@@ -432,6 +617,12 @@ async function main() {
|
|
|
432
617
|
path.join(binDest, "agent-runs.js")
|
|
433
618
|
);
|
|
434
619
|
ok("agent-runs.js (agent telemetry writer)");
|
|
620
|
+
copy(
|
|
621
|
+
path.join(FRAMEWORK_DIR, "bin", "slop-detect.mjs"),
|
|
622
|
+
path.join(binDest, "slop-detect.mjs")
|
|
623
|
+
);
|
|
624
|
+
fs.chmodSync(path.join(binDest, "slop-detect.mjs"), 0o755);
|
|
625
|
+
ok("slop-detect.mjs (anti-pattern scanner — runs pre-commit on frontend builds)");
|
|
435
626
|
} catch (e) {
|
|
436
627
|
warn(`scripts — ${e.message}`);
|
|
437
628
|
}
|
|
@@ -681,6 +872,8 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
681
872
|
"session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
|
|
682
873
|
"pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
|
|
683
874
|
"git-guardrails.js", "stop-session-log.js",
|
|
875
|
+
// v5.0 — insights-driven destructive-op + wrong-account guards
|
|
876
|
+
"vercel-account-guard.js", "env-empty-guard.js", "supabase-destructive-guard.js",
|
|
684
877
|
]);
|
|
685
878
|
const isQualiaHookCmd = (cmd) => {
|
|
686
879
|
if (typeof cmd !== "string") return false;
|
|
@@ -707,6 +900,10 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
707
900
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
|
|
708
901
|
{ type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
|
|
709
902
|
{ type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
|
|
903
|
+
// v5.0 hooks — insights-driven friction prevention
|
|
904
|
+
{ type: "command", if: "Bash(vercel --prod*)|Bash(vercel deploy*)", command: nodeCmd("vercel-account-guard.js"), timeout: 8, statusMessage: "⬢ Verifying Vercel account..." },
|
|
905
|
+
{ type: "command", if: "Bash(vercel env*)", command: nodeCmd("env-empty-guard.js"), timeout: 5, statusMessage: "⬢ Checking env value..." },
|
|
906
|
+
{ type: "command", if: "Bash(supabase*)|Bash(npx supabase*)", command: nodeCmd("supabase-destructive-guard.js"), timeout: 5, statusMessage: "⬢ Checking Supabase safety..." },
|
|
710
907
|
],
|
|
711
908
|
},
|
|
712
909
|
{
|
|
@@ -767,54 +964,203 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
|
|
|
767
964
|
ok("MCP: next-devtools (runtime error visibility for Next.js projects)");
|
|
768
965
|
}
|
|
769
966
|
|
|
770
|
-
|
|
967
|
+
// v5.0: backup existing settings.json before overwrite. The merge logic above
|
|
968
|
+
// preserves user fields, but a partial-write or merger bug could destroy MCP
|
|
969
|
+
// configs / custom permissions. Atomic write (tmp + rename) avoids partial
|
|
970
|
+
// writes; the .bak file is the recovery point if the merger ever misbehaves.
|
|
971
|
+
if (fs.existsSync(settingsPath)) {
|
|
972
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
973
|
+
const bak = `${settingsPath}.bak.${ts}`;
|
|
974
|
+
try { fs.copyFileSync(settingsPath, bak); } catch {}
|
|
975
|
+
}
|
|
976
|
+
const settingsTmp = `${settingsPath}.tmp.${process.pid}`;
|
|
977
|
+
fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
|
|
978
|
+
fs.renameSync(settingsTmp, settingsPath);
|
|
771
979
|
|
|
772
|
-
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact, git-guardrails, stop-session-log");
|
|
980
|
+
ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact, git-guardrails, stop-session-log, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
|
|
773
981
|
ok("Status line + spinner configured");
|
|
774
982
|
ok("Environment variables + permissions");
|
|
775
983
|
|
|
984
|
+
// ─── Codex (optional second target) ──────────────────────
|
|
985
|
+
if (installCodexTarget) {
|
|
986
|
+
await installCodex(member, target);
|
|
987
|
+
}
|
|
988
|
+
|
|
776
989
|
// ─── Summary ───────────────────────────────────────────
|
|
990
|
+
closeSection();
|
|
991
|
+
printSummary({ member, target, claudeInstalled: true });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ─── Final summary card (shared by Claude / Codex / Both paths) ──
|
|
995
|
+
function printSummary({ member, target, claudeInstalled }) {
|
|
996
|
+
const roleColor = member.role === "OWNER" ? TEAL : GREEN;
|
|
997
|
+
const totalMs = Date.now() - installStart;
|
|
998
|
+
const totalSec = totalMs >= 1000
|
|
999
|
+
? `${(totalMs / 1000).toFixed(1)}s`
|
|
1000
|
+
: `${totalMs}ms`;
|
|
1001
|
+
|
|
777
1002
|
console.log("");
|
|
778
|
-
console.log(` ${
|
|
1003
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
779
1004
|
console.log(` ${TEAL}${BOLD}⬢ INSTALLED${RESET}`);
|
|
780
|
-
console.log(` ${
|
|
1005
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
781
1006
|
console.log("");
|
|
782
1007
|
console.log(` ${WHITE}${BOLD}${member.name}${RESET} ${DIM}·${RESET} ${roleColor}${member.role}${RESET} ${DIM}·${RESET} ${DIM}v${PKG_VERSION}${RESET}`);
|
|
783
1008
|
console.log("");
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1009
|
+
console.log(` ${DIM}Targets${RESET} ${TEAL}${targetLabel(target)}${RESET}`);
|
|
1010
|
+
console.log(` ${DIM}Time${RESET} ${TEAL}${totalSec}${RESET}`);
|
|
1011
|
+
|
|
1012
|
+
if (claudeInstalled) {
|
|
1013
|
+
const agentsDir = path.join(FRAMEWORK_DIR, "agents");
|
|
1014
|
+
const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
|
|
1015
|
+
const rulesDir = path.join(FRAMEWORK_DIR, "rules");
|
|
1016
|
+
const tmplDir = path.join(FRAMEWORK_DIR, "templates");
|
|
1017
|
+
const skillsDir = path.join(FRAMEWORK_DIR, "skills");
|
|
1018
|
+
const skillCount = fs
|
|
1019
|
+
.readdirSync(skillsDir)
|
|
1020
|
+
.filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory()).length;
|
|
1021
|
+
const agentCount = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md")).length;
|
|
1022
|
+
const hookCount = fs.readdirSync(hooksSource).length;
|
|
1023
|
+
const ruleCount = fs.readdirSync(rulesDir).length;
|
|
1024
|
+
const tmplCount = fs.readdirSync(tmplDir).length;
|
|
1025
|
+
const installedBinDir = path.join(CLAUDE_DIR, "bin");
|
|
1026
|
+
const scriptCount = fs.existsSync(installedBinDir)
|
|
1027
|
+
? fs.readdirSync(installedBinDir).filter((f) => f.endsWith(".js")).length
|
|
1028
|
+
: 0;
|
|
1029
|
+
console.log("");
|
|
1030
|
+
console.log(` ${DIM}Skills${RESET} ${TEAL}${skillCount}${RESET} ${DIM}Agents${RESET} ${TEAL}${agentCount}${RESET} ${DIM}Hooks${RESET} ${TEAL}${hookCount}${RESET}`);
|
|
1031
|
+
console.log(` ${DIM}Rules${RESET} ${TEAL}${ruleCount}${RESET} ${DIM}Scripts${RESET} ${TEAL}${scriptCount}${RESET} ${DIM}Templates${RESET} ${TEAL}${tmplCount}${RESET}`);
|
|
1032
|
+
}
|
|
794
1033
|
|
|
795
1034
|
if (errors > 0) {
|
|
796
1035
|
console.log("");
|
|
797
1036
|
console.log(` ${YELLOW}${errors} warning(s)${RESET} — check output above`);
|
|
798
1037
|
}
|
|
799
1038
|
|
|
1039
|
+
// Contextual first-command suggestion: if we're in a Qualia project,
|
|
1040
|
+
// recommend /qualia (router); otherwise /qualia-new (kickoff).
|
|
1041
|
+
const inProject = (() => {
|
|
1042
|
+
try { return fs.existsSync(path.join(process.cwd(), ".planning")); }
|
|
1043
|
+
catch { return false; }
|
|
1044
|
+
})();
|
|
1045
|
+
const firstCmd = inProject ? "/qualia" : "/qualia-new";
|
|
1046
|
+
const firstCmdHint = inProject ? "router — tells you the next command" : "kickoff a new project";
|
|
1047
|
+
|
|
800
1048
|
console.log("");
|
|
801
|
-
console.log(` ${
|
|
1049
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
802
1050
|
console.log(` ${WHITE}${BOLD}Quick Start${RESET}`);
|
|
803
|
-
console.log(` ${
|
|
1051
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
804
1052
|
console.log("");
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1053
|
+
if (claudeInstalled) {
|
|
1054
|
+
console.log(` ${TEAL}1.${RESET} ${WHITE}Restart Claude Code${RESET} ${DIM}(loads new settings)${RESET}`);
|
|
1055
|
+
console.log(` ${TEAL}2.${RESET} ${WHITE}cd into any project${RESET} ${DIM}and run${RESET} ${TEAL}claude${RESET}`);
|
|
1056
|
+
console.log(` ${TEAL}3.${RESET} ${WHITE}Try${RESET} ${TEAL}${BOLD}${firstCmd}${RESET} ${DIM}— ${firstCmdHint}${RESET}`);
|
|
1057
|
+
} else {
|
|
1058
|
+
// Codex-only path
|
|
1059
|
+
console.log(` ${TEAL}1.${RESET} ${WHITE}Open Codex in any project${RESET}`);
|
|
1060
|
+
console.log(` ${TEAL}2.${RESET} ${WHITE}Codex picks up${RESET} ${TEAL}~/.codex/AGENTS.md${RESET} ${DIM}automatically${RESET}`);
|
|
1061
|
+
console.log(` ${TEAL}3.${RESET} ${WHITE}Ask Codex${RESET} ${DIM}about Qualia rules — they're in AGENTS.md${RESET}`);
|
|
1062
|
+
}
|
|
808
1063
|
console.log("");
|
|
809
1064
|
console.log(` ${DIM}New project?${RESET} ${TEAL}/qualia-new${RESET}`);
|
|
810
1065
|
console.log(` ${DIM}Quick fix?${RESET} ${TEAL}/qualia-quick${RESET}`);
|
|
811
1066
|
console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(mandatory)${RESET}`);
|
|
812
1067
|
console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
|
|
813
1068
|
console.log("");
|
|
814
|
-
console.log(` ${
|
|
1069
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
815
1070
|
console.log(` ${TEAL}${BOLD}Welcome to the future with Qualia.${RESET}`);
|
|
816
|
-
console.log(` ${
|
|
1071
|
+
console.log(` ${DIM2}${RULE}${RESET}`);
|
|
1072
|
+
console.log("");
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ─── Codex install (writes AGENTS.md to ~/.codex/) ───────
|
|
1076
|
+
// Scope is intentionally minimal: AGENTS.md is the open standard adopted
|
|
1077
|
+
// by Codex / Cursor / Continue / Aider / Devin. Codex's runtime does not
|
|
1078
|
+
// today consume Claude-style skills/agents/hooks on disk in a way the
|
|
1079
|
+
// framework can map 1:1, so we write the convention file and document the
|
|
1080
|
+
// scope honestly. If Codex grows skill/hook support, we extend this here.
|
|
1081
|
+
async function installCodex(member, target) {
|
|
817
1082
|
console.log("");
|
|
1083
|
+
console.log(` ${TEAL}▸${RESET} ${WHITE}${BOLD}Codex${RESET}`);
|
|
1084
|
+
console.log(` ${DIM2}${"─".repeat(40)}${RESET}`);
|
|
1085
|
+
|
|
1086
|
+
// Detect Codex CLI; soft-warn if missing (file write still proceeds).
|
|
1087
|
+
const { spawnSync } = require("child_process");
|
|
1088
|
+
let codexDetected = false;
|
|
1089
|
+
try {
|
|
1090
|
+
const r = spawnSync("codex", ["--version"], {
|
|
1091
|
+
encoding: "utf8",
|
|
1092
|
+
timeout: 3000,
|
|
1093
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1094
|
+
});
|
|
1095
|
+
codexDetected = r.status === 0;
|
|
1096
|
+
} catch { codexDetected = false; }
|
|
1097
|
+
|
|
1098
|
+
if (!codexDetected) {
|
|
1099
|
+
console.log(` ${YELLOW}!${RESET} ${WHITE}Codex CLI not detected on this system${RESET}`);
|
|
1100
|
+
console.log(` ${DIM} Installing AGENTS.md to ~/.codex/AGENTS.md anyway — Codex will pick it up${RESET}`);
|
|
1101
|
+
console.log(` ${DIM} when you install via:${RESET} ${TEAL}npm install -g @openai/codex${RESET}`);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Make sure the dir exists.
|
|
1105
|
+
if (!fs.existsSync(CODEX_DIR)) {
|
|
1106
|
+
try {
|
|
1107
|
+
fs.mkdirSync(CODEX_DIR, { recursive: true });
|
|
1108
|
+
} catch (e) {
|
|
1109
|
+
warn(`Codex — could not create ${CODEX_DIR}: ${e.message}`);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Render AGENTS.md with role substitution. Source is the same AGENTS.md
|
|
1115
|
+
// already shipped in the framework root (it's the cross-vendor mirror of
|
|
1116
|
+
// CLAUDE.md, kept under 25 lines per Pocock's instruction-budget rule).
|
|
1117
|
+
let agentsContent;
|
|
1118
|
+
try {
|
|
1119
|
+
agentsContent = fs.readFileSync(path.join(FRAMEWORK_DIR, "AGENTS.md"), "utf8");
|
|
1120
|
+
agentsContent = agentsContent
|
|
1121
|
+
.replace("{{ROLE}}", member.role)
|
|
1122
|
+
.replace("{{ROLE_DESCRIPTION}}", member.description);
|
|
1123
|
+
} catch (e) {
|
|
1124
|
+
warn(`Codex — could not read framework AGENTS.md: ${e.message}`);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const dest = path.join(CODEX_DIR, "AGENTS.md");
|
|
1129
|
+
|
|
1130
|
+
// Backup if existing differs (matches v5.0 CLAUDE.md / settings.json
|
|
1131
|
+
// discipline — never silently destroy a hand-edited file).
|
|
1132
|
+
if (fs.existsSync(dest)) {
|
|
1133
|
+
try {
|
|
1134
|
+
const existing = fs.readFileSync(dest, "utf8");
|
|
1135
|
+
if (existing !== agentsContent) {
|
|
1136
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1137
|
+
const bak = `${dest}.bak.${ts}`;
|
|
1138
|
+
try { fs.copyFileSync(dest, bak); ok(`Backed up existing AGENTS.md → ${path.basename(bak)}`); } catch {}
|
|
1139
|
+
}
|
|
1140
|
+
} catch {}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Atomic write: tmp + rename. Same pattern as settings.json above.
|
|
1144
|
+
try {
|
|
1145
|
+
const tmp = `${dest}.tmp.${process.pid}`;
|
|
1146
|
+
fs.writeFileSync(tmp, agentsContent, "utf8");
|
|
1147
|
+
fs.renameSync(tmp, dest);
|
|
1148
|
+
sectionCount++;
|
|
1149
|
+
ok(`AGENTS.md (configured as ${member.role})`);
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
warn(`Codex AGENTS.md — ${e.message}`);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Honest scope note.
|
|
1156
|
+
console.log(` ${DIM}└─${RESET} ${DIM}Codex install scope: AGENTS.md only — Codex's runtime does not currently${RESET}`);
|
|
1157
|
+
console.log(` ${DIM}consume the framework's skills/hooks/agents on disk. AGENTS.md carries${RESET}`);
|
|
1158
|
+
console.log(` ${DIM}the rules; commands route through Claude Code.${RESET}`);
|
|
1159
|
+
|
|
1160
|
+
// Codex-only path: still need to write the role config and print summary.
|
|
1161
|
+
if (target === TARGET_CODEX_ONLY) {
|
|
1162
|
+
printSummary({ member, target, claudeInstalled: false });
|
|
1163
|
+
}
|
|
818
1164
|
}
|
|
819
1165
|
|
|
820
1166
|
main().catch((e) => {
|