qualia-framework 6.2.7 → 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 +5 -1
- package/bin/codex-goal.js +92 -0
- package/bin/install.js +110 -10
- package/hooks/pre-deploy-gate.js +27 -0
- package/hooks/pre-push.js +19 -0
- package/package.json +1 -1
- 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-plan/SKILL.md +4 -0
- package/tests/bin.test.sh +1 -1
- package/tests/install-smoke.test.sh +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
# Qualia Framework v6.2.
|
|
1
|
+
# Qualia Framework v6.2.9
|
|
2
2
|
|
|
3
3
|
A harness engineering framework for Claude Code and OpenAI Codex. It installs into `~/.claude/` and/or `~/.codex/` and wraps your AI-assisted development workflow with structured planning, execution, verification, and deployment gates.
|
|
4
4
|
|
|
5
5
|
It is not an application framework like Rails or Next.js. It doesn't generate code, run servers, or process data. It's an opinionated workflow layer that tells Claude how to plan, build, and verify your projects end-to-end, from "tell me what you want to make" to "here's the handoff doc for your client."
|
|
6
6
|
|
|
7
|
+
**v6.2.9** — Codex hook noise + status line. Conditional PreToolUse hooks no longer status-message on every Bash call (Codex was printing 8 "Running hook…" lines on every command). Self-filtering added to `pre-deploy-gate.js` and `pre-push.js` so they never trip on unrelated commands (Claude's substring matcher was firing them on for-loop arguments). Installer now writes `[tui] status_line = [...]` to Codex's `config.toml` for the rich native bottom status line.
|
|
8
|
+
|
|
9
|
+
**v6.2.8** — Codex `/goal` integration + install hardening. Phase-start skills now set the Codex thread goal (with token budget) via `bin/codex-goal.js` and `rules/codex-goal.md`. Installer fixes: agent TOMLs now emit `name = "..."` (Codex 0.133 was rejecting all 9), ERP API key is mirrored from `~/.claude/` → `~/.codex/`, and deprecated skills (`qualia-task`, `qualia-quick`, `qualia-polish-loop`, `qualia-design`, `qualia-prd`) are pruned on upgrade.
|
|
10
|
+
|
|
7
11
|
**v6.2.7** — Codex runtime compatibility. The installer now writes Codex-native hooks, TOML agents, bin scripts, rules, skills, templates, knowledge, guide, and role config under `~/.codex/`, not just `AGENTS.md`.
|
|
8
12
|
|
|
9
13
|
**The v5 line (preserved):**
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// codex-goal — suggest a Codex /goal objective + token budget from current
|
|
3
|
+
// .planning/STATE.md + ROADMAP.md state.
|
|
4
|
+
//
|
|
5
|
+
// Output is a single line meant to be pasted into a Codex session, or quoted
|
|
6
|
+
// in an agent prompt that will then issue update_goal itself. The framework
|
|
7
|
+
// does NOT write directly to ~/.codex/goals_1.sqlite — Codex owns the schema
|
|
8
|
+
// and the thread_id; the /goal slash command (and the update_goal tool the
|
|
9
|
+
// model can call) are the stable interfaces.
|
|
10
|
+
//
|
|
11
|
+
// Usage: node bin/codex-goal.js [phase|task|feature|quick]
|
|
12
|
+
// default scope = phase
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
|
|
17
|
+
const SCOPE = (process.argv[2] || "phase").toLowerCase();
|
|
18
|
+
|
|
19
|
+
// Token budgets are calibrated to Codex's per-thread context window and the
|
|
20
|
+
// typical Qualia work-unit size. Phase = full builder loop with verification.
|
|
21
|
+
// Task/feature = single fresh-context spawn. Quick = one-shot inline edit.
|
|
22
|
+
const TOKEN_BUDGETS = {
|
|
23
|
+
phase: 80000,
|
|
24
|
+
task: 30000,
|
|
25
|
+
feature: 30000,
|
|
26
|
+
quick: 10000,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const PLANNING = path.join(process.cwd(), ".planning");
|
|
30
|
+
const STATE_FILE = path.join(PLANNING, "STATE.md");
|
|
31
|
+
const ROADMAP_FILE = path.join(PLANNING, "ROADMAP.md");
|
|
32
|
+
|
|
33
|
+
function readSafe(p) {
|
|
34
|
+
try { return fs.readFileSync(p, "utf8"); } catch { return ""; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseState(content) {
|
|
38
|
+
if (!content) return null;
|
|
39
|
+
const phaseMatch = content.match(/^Phase:\s*(\d+)\s+of\s+(\d+)\s*[—-]\s*(.+?)\r?$/m);
|
|
40
|
+
if (!phaseMatch) return null;
|
|
41
|
+
return {
|
|
42
|
+
phase: parseInt(phaseMatch[1], 10),
|
|
43
|
+
total: parseInt(phaseMatch[2], 10),
|
|
44
|
+
name: phaseMatch[3].trim(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readPhaseGoal(roadmap, phaseNum) {
|
|
49
|
+
if (!roadmap) return "";
|
|
50
|
+
// Match a heading like "## Phase 2 — Name" or "## Phase 2: Name", then
|
|
51
|
+
// grab the "Goal:" line that follows.
|
|
52
|
+
const heading = new RegExp(`##\\s*Phase\\s*${phaseNum}\\s*[—:\\-]\\s*[^\\n]+`, "i");
|
|
53
|
+
const idx = roadmap.search(heading);
|
|
54
|
+
if (idx === -1) return "";
|
|
55
|
+
const body = roadmap.slice(idx, idx + 2000);
|
|
56
|
+
const goalMatch = body.match(/^\s*\*?\*?Goal:?\*?\*?\s*(.+?)$/im);
|
|
57
|
+
return goalMatch ? goalMatch[1].trim() : "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clip(s, max) {
|
|
61
|
+
return s.length <= max ? s : s.slice(0, max - 1).trimEnd() + "…";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function main() {
|
|
65
|
+
const budget = TOKEN_BUDGETS[SCOPE];
|
|
66
|
+
if (budget == null) {
|
|
67
|
+
process.stderr.write(`unknown scope: ${SCOPE} (use phase|task|feature|quick)\n`);
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const state = parseState(readSafe(STATE_FILE));
|
|
72
|
+
if (!state) {
|
|
73
|
+
// No .planning/STATE.md — fall back to a generic shape so the helper
|
|
74
|
+
// still emits something useful for ad-hoc invocations.
|
|
75
|
+
const objective = `${SCOPE[0].toUpperCase() + SCOPE.slice(1)} in ${path.basename(process.cwd())}`;
|
|
76
|
+
process.stdout.write(`/goal ${objective}\n`);
|
|
77
|
+
process.stdout.write(`# token_budget suggestion: ${budget}\n`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const phaseGoal = readPhaseGoal(readSafe(ROADMAP_FILE), state.phase);
|
|
82
|
+
const objectivePrefix = `Phase ${state.phase}/${state.total} — ${state.name}`;
|
|
83
|
+
const objective = clip(
|
|
84
|
+
phaseGoal ? `${objectivePrefix}: ${phaseGoal}` : objectivePrefix,
|
|
85
|
+
280
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
process.stdout.write(`/goal ${objective}\n`);
|
|
89
|
+
process.stdout.write(`# token_budget suggestion: ${budget}\n`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main();
|
package/bin/install.js
CHANGED
|
@@ -205,19 +205,48 @@ function parseAgentMarkdown(content) {
|
|
|
205
205
|
return result;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
function renderCodexAgentToml(markdown) {
|
|
208
|
+
function renderCodexAgentToml(markdown, filenameFallback) {
|
|
209
209
|
const parsed = parseAgentMarkdown(markdown);
|
|
210
210
|
const body = parsed.body
|
|
211
211
|
.replaceAll("~/.claude/", "~/.codex/")
|
|
212
212
|
.replaceAll("@~/.claude/", "@~/.codex/");
|
|
213
|
+
const name = (parsed.name || filenameFallback || "").replace(/^qualia-/, "");
|
|
213
214
|
const description = parsed.description || "Qualia Framework specialist agent.";
|
|
214
215
|
return [
|
|
216
|
+
`name = ${tomlString(name)}`,
|
|
215
217
|
`description = ${tomlString(description)}`,
|
|
216
218
|
`developer_instructions = ${tomlString(body)}`,
|
|
217
219
|
"",
|
|
218
220
|
].join("\n");
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
// Skills removed in past versions but still present in older installs.
|
|
224
|
+
// Pruned from BOTH ~/.claude/skills/ and ~/.codex/skills/ on every install run
|
|
225
|
+
// so the active surface matches what the framework currently ships.
|
|
226
|
+
const DEPRECATED_SKILLS = [
|
|
227
|
+
"qualia-task", // v5.7.0 — folded into qualia-feature
|
|
228
|
+
"qualia-quick", // v5.7.0 — folded into qualia-feature
|
|
229
|
+
"qualia-polish-loop", // v5.8.0 — folded into qualia-polish --loop
|
|
230
|
+
"qualia-design", // v4 wave 2 — folded into scope-adaptive qualia-polish
|
|
231
|
+
"qualia-prd", // v5.8.0 — surface cleanup
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
function pruneDeprecatedSkills(baseDir) {
|
|
235
|
+
const skillsDir = path.join(baseDir, "skills");
|
|
236
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
237
|
+
const removed = [];
|
|
238
|
+
for (const name of DEPRECATED_SKILLS) {
|
|
239
|
+
const target = path.join(skillsDir, name);
|
|
240
|
+
try {
|
|
241
|
+
if (fs.existsSync(target)) {
|
|
242
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
243
|
+
removed.push(name);
|
|
244
|
+
}
|
|
245
|
+
} catch {}
|
|
246
|
+
}
|
|
247
|
+
return removed;
|
|
248
|
+
}
|
|
249
|
+
|
|
221
250
|
// Surgically remove orphaned v2.6 install cruft from ~/.claude/ on upgrade.
|
|
222
251
|
// v2.6 installed a separate ~/.claude/qualia-framework/ directory with workflows/,
|
|
223
252
|
// references/, assets/, bin/qualia-tools.js. v3 doesn't use any of that — it was
|
|
@@ -474,6 +503,8 @@ async function main() {
|
|
|
474
503
|
.filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory());
|
|
475
504
|
|
|
476
505
|
printSection("Skills");
|
|
506
|
+
const claudePruned = pruneDeprecatedSkills(CLAUDE_DIR);
|
|
507
|
+
for (const name of claudePruned) ok(`pruned deprecated: ${name}`);
|
|
477
508
|
for (const skill of skills) {
|
|
478
509
|
try {
|
|
479
510
|
copy(
|
|
@@ -783,6 +814,12 @@ async function main() {
|
|
|
783
814
|
);
|
|
784
815
|
fs.chmodSync(path.join(binDest, "project-snapshot.js"), 0o755);
|
|
785
816
|
ok("project-snapshot.js (ERP/admin project progress snapshot)");
|
|
817
|
+
copy(
|
|
818
|
+
path.join(FRAMEWORK_DIR, "bin", "codex-goal.js"),
|
|
819
|
+
path.join(binDest, "codex-goal.js")
|
|
820
|
+
);
|
|
821
|
+
fs.chmodSync(path.join(binDest, "codex-goal.js"), 0o755);
|
|
822
|
+
ok("codex-goal.js (Codex /goal objective + token-budget suggester)");
|
|
786
823
|
} catch (e) {
|
|
787
824
|
warn(`scripts — ${e.message}`);
|
|
788
825
|
}
|
|
@@ -1317,10 +1354,40 @@ async function installCodex(member, target) {
|
|
|
1317
1354
|
"hooks = true",
|
|
1318
1355
|
"plugin_hooks = true",
|
|
1319
1356
|
"",
|
|
1357
|
+
"# Codex's built-in status line is rendered at the bottom of the TUI.",
|
|
1358
|
+
"# It takes an ARRAY of pre-defined segment names; Codex does NOT support",
|
|
1359
|
+
"# custom-command status lines (unlike Claude's settings.json statusLine),",
|
|
1360
|
+
"# so the Qualia phase/state info is rendered via the SessionStart banner",
|
|
1361
|
+
"# at the top of the session instead. The segment list below mirrors the",
|
|
1362
|
+
"# Codex default rich layout.",
|
|
1363
|
+
"[tui]",
|
|
1364
|
+
'status_line = ["model-with-reasoning", "task-progress", "current-dir", "git-branch", "context-used", "five-hour-limit", "weekly-limit"]',
|
|
1365
|
+
"status_line_use_colors = true",
|
|
1366
|
+
"",
|
|
1320
1367
|
].join("\n"));
|
|
1321
1368
|
ok("config.toml (minimal Codex config)");
|
|
1322
1369
|
} else {
|
|
1323
|
-
|
|
1370
|
+
// Existing user config — append [tui] block only if absent. Leaves
|
|
1371
|
+
// every other user setting untouched.
|
|
1372
|
+
try {
|
|
1373
|
+
const existing = fs.readFileSync(configToml, "utf8");
|
|
1374
|
+
if (!/^\[tui\]/m.test(existing) && !/^status_line\s*=/m.test(existing)) {
|
|
1375
|
+
const append = [
|
|
1376
|
+
"",
|
|
1377
|
+
"# Added by qualia-framework — Codex bottom status line.",
|
|
1378
|
+
"[tui]",
|
|
1379
|
+
'status_line = ["model-with-reasoning", "task-progress", "current-dir", "git-branch", "context-used", "five-hour-limit", "weekly-limit"]',
|
|
1380
|
+
"status_line_use_colors = true",
|
|
1381
|
+
"",
|
|
1382
|
+
].join("\n");
|
|
1383
|
+
fs.appendFileSync(configToml, append);
|
|
1384
|
+
ok("config.toml (appended [tui] status line block)");
|
|
1385
|
+
} else {
|
|
1386
|
+
log(`${DIM}config.toml (kept — user has customized)${RESET}`);
|
|
1387
|
+
}
|
|
1388
|
+
} catch {
|
|
1389
|
+
log(`${DIM}config.toml (kept — user has customized)${RESET}`);
|
|
1390
|
+
}
|
|
1324
1391
|
}
|
|
1325
1392
|
} catch (e) {
|
|
1326
1393
|
warn(`Codex config.toml — ${e.message}`);
|
|
@@ -1347,6 +1414,24 @@ async function installCodex(member, target) {
|
|
|
1347
1414
|
warn(`Codex config — ${e.message}`);
|
|
1348
1415
|
}
|
|
1349
1416
|
|
|
1417
|
+
// Mirror the ERP API key from Claude → Codex so erp-retry/report-payload can
|
|
1418
|
+
// post from Codex sessions without a separate provisioning step. The key
|
|
1419
|
+
// resolver at runtime looks in $CODEX_DIR/.erp-api-key only; without this
|
|
1420
|
+
// copy, every Codex ERP write 401s and the queue grows silently.
|
|
1421
|
+
try {
|
|
1422
|
+
const claudeKey = path.join(CLAUDE_DIR, ".erp-api-key");
|
|
1423
|
+
const codexKey = path.join(CODEX_DIR, ".erp-api-key");
|
|
1424
|
+
if (fs.existsSync(claudeKey) && !fs.existsSync(codexKey)) {
|
|
1425
|
+
const key = fs.readFileSync(claudeKey, "utf8");
|
|
1426
|
+
atomicWrite(codexKey, key, 0o600);
|
|
1427
|
+
ok(".erp-api-key (mirrored from ~/.claude/)");
|
|
1428
|
+
} else if (fs.existsSync(codexKey)) {
|
|
1429
|
+
log(`${DIM}.erp-api-key (existing — preserved)${RESET}`);
|
|
1430
|
+
}
|
|
1431
|
+
} catch (e) {
|
|
1432
|
+
warn(`Codex .erp-api-key — ${e.message}`);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1350
1435
|
// Scripts
|
|
1351
1436
|
try {
|
|
1352
1437
|
const binDest = path.join(CODEX_DIR, "bin");
|
|
@@ -1363,6 +1448,7 @@ async function installCodex(member, target) {
|
|
|
1363
1448
|
"erp-retry.js",
|
|
1364
1449
|
"report-payload.js",
|
|
1365
1450
|
"project-snapshot.js",
|
|
1451
|
+
"codex-goal.js",
|
|
1366
1452
|
];
|
|
1367
1453
|
for (const script of scripts) {
|
|
1368
1454
|
const src = path.join(FRAMEWORK_DIR, "bin", script);
|
|
@@ -1386,7 +1472,7 @@ async function installCodex(member, target) {
|
|
|
1386
1472
|
const parsed = parseAgentMarkdown(source);
|
|
1387
1473
|
const base = parsed.name ? parsed.name.replace(/^qualia-/, "") : path.basename(file, ".md");
|
|
1388
1474
|
const out = path.join(agentsDest, `${base}.toml`);
|
|
1389
|
-
const toml = renderCodexAgentToml(source);
|
|
1475
|
+
const toml = renderCodexAgentToml(source, base);
|
|
1390
1476
|
backupIfDifferent(out, toml, `agents/${base}.toml`);
|
|
1391
1477
|
atomicWrite(out, toml);
|
|
1392
1478
|
}
|
|
@@ -1415,6 +1501,8 @@ async function installCodex(member, target) {
|
|
|
1415
1501
|
try {
|
|
1416
1502
|
const skillsSrc = path.join(FRAMEWORK_DIR, "skills");
|
|
1417
1503
|
const skillsDest = path.join(CODEX_DIR, "skills");
|
|
1504
|
+
const codexPruned = pruneDeprecatedSkills(CODEX_DIR);
|
|
1505
|
+
for (const name of codexPruned) ok(`pruned deprecated: ${name}`);
|
|
1418
1506
|
for (const skill of fs.readdirSync(skillsSrc)) {
|
|
1419
1507
|
const src = path.join(skillsSrc, skill);
|
|
1420
1508
|
if (!fs.statSync(src).isDirectory()) continue;
|
|
@@ -1471,6 +1559,18 @@ async function installCodex(member, target) {
|
|
|
1471
1559
|
try { fs.chmodSync(out, 0o755); } catch {}
|
|
1472
1560
|
}
|
|
1473
1561
|
const nodeCmd = (hookFile) => `node "${path.join(hooksDest, hookFile)}"`;
|
|
1562
|
+
// Codex's hook schema does NOT include an `if` field — only `command`,
|
|
1563
|
+
// `commandWindows`, `timeout`, `async`, `statusMessage`. Filtering on
|
|
1564
|
+
// tool_input.command happens inside each hook script (they read stdin
|
|
1565
|
+
// JSON and `process.exit(0)` fast when the command doesn't match).
|
|
1566
|
+
//
|
|
1567
|
+
// Codex prints `statusMessage` for every entry in the matched group BEFORE
|
|
1568
|
+
// running the hook. Including statusMessage on conditional hooks produced
|
|
1569
|
+
// 8 lines of "Running PreToolUse hook: Qualia X..." on every Bash call
|
|
1570
|
+
// even when 6 of the 8 immediately exited 0. We only set statusMessage on
|
|
1571
|
+
// hooks that always do real work (auto-update + git-guardrails). The
|
|
1572
|
+
// conditional hooks stay registered (so they still fire when applicable)
|
|
1573
|
+
// but render silently.
|
|
1474
1574
|
const qualiaHooks = {
|
|
1475
1575
|
hooks: {
|
|
1476
1576
|
SessionStart: [
|
|
@@ -1482,18 +1582,18 @@ async function installCodex(member, target) {
|
|
|
1482
1582
|
hooks: [
|
|
1483
1583
|
{ type: "command", command: nodeCmd("auto-update.js"), timeout: 5, statusMessage: "Qualia update check..." },
|
|
1484
1584
|
{ type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "Qualia git safety..." },
|
|
1485
|
-
{ type: "command",
|
|
1486
|
-
{ type: "command",
|
|
1487
|
-
{ type: "command",
|
|
1488
|
-
{ type: "command",
|
|
1489
|
-
{ type: "command",
|
|
1490
|
-
{ type: "command",
|
|
1585
|
+
{ type: "command", command: nodeCmd("branch-guard.js"), timeout: 5 },
|
|
1586
|
+
{ type: "command", command: nodeCmd("pre-push.js"), timeout: 15 },
|
|
1587
|
+
{ type: "command", command: nodeCmd("pre-deploy-gate.js"), timeout: 180 },
|
|
1588
|
+
{ type: "command", command: nodeCmd("vercel-account-guard.js"), timeout: 8 },
|
|
1589
|
+
{ type: "command", command: nodeCmd("env-empty-guard.js"), timeout: 5 },
|
|
1590
|
+
{ type: "command", command: nodeCmd("supabase-destructive-guard.js"), timeout: 5 },
|
|
1491
1591
|
],
|
|
1492
1592
|
},
|
|
1493
1593
|
{
|
|
1494
1594
|
matcher: "Edit|Write",
|
|
1495
1595
|
hooks: [
|
|
1496
|
-
{ type: "command",
|
|
1596
|
+
{ type: "command", command: nodeCmd("migration-guard.js"), timeout: 10 },
|
|
1497
1597
|
],
|
|
1498
1598
|
},
|
|
1499
1599
|
],
|
package/hooks/pre-deploy-gate.js
CHANGED
|
@@ -22,6 +22,33 @@ function qualiaHome() {
|
|
|
22
22
|
|
|
23
23
|
const QUALIA_HOME = qualiaHome();
|
|
24
24
|
|
|
25
|
+
// Self-filter on the proposed bash command — only act when the user is
|
|
26
|
+
// actually trying to deploy. Claude Code's `if: "Bash(vercel --prod*)"` does
|
|
27
|
+
// substring matching (not glob), so commands that contain the literal text
|
|
28
|
+
// "vercel --prod" inside a for-loop or grep argument trip the gate. Codex
|
|
29
|
+
// ignores the `if` field entirely. Both runtimes therefore need this hook to
|
|
30
|
+
// inspect tool_input.command itself before doing any work.
|
|
31
|
+
//
|
|
32
|
+
// Direct invocation (no stdin — `node pre-deploy-gate.js`) is treated as
|
|
33
|
+
// "run the gate" so the test suite and manual `qualia-framework verify` flows
|
|
34
|
+
// keep working. Only when stdin contains a parseable tool_input.command that
|
|
35
|
+
// does NOT match a deploy pattern do we exit early.
|
|
36
|
+
(function selfFilter() {
|
|
37
|
+
if (process.stdin.isTTY) return; // direct invocation — run full gate
|
|
38
|
+
let command = null; // null = no stdin payload, "" = empty command, "str" = parsed
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
41
|
+
if (raw) {
|
|
42
|
+
const payload = JSON.parse(raw);
|
|
43
|
+
command = (payload && payload.tool_input && payload.tool_input.command) || "";
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
if (command === null) return; // malformed or empty stdin — run full gate
|
|
47
|
+
if (!/^\s*(npx\s+)?vercel\s+(--prod|deploy\s+--prod)\b/.test(command)) {
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
|
|
25
52
|
function _trace(hookName, result, extra) {
|
|
26
53
|
try {
|
|
27
54
|
const os = require("os");
|
package/hooks/pre-push.js
CHANGED
|
@@ -40,6 +40,25 @@ const QUALIA_HOME = qualiaHome();
|
|
|
40
40
|
const TRACKING = path.join(".planning", "tracking.json");
|
|
41
41
|
const SHELL = process.platform === "win32";
|
|
42
42
|
|
|
43
|
+
// Self-filter — only stamp on actual `git push`. Claude's `if` matcher does
|
|
44
|
+
// substring matching (so unrelated commands with "git push" as a literal
|
|
45
|
+
// argument trip the hook) and Codex ignores the `if` field entirely. Direct
|
|
46
|
+
// invocation (no stdin) falls through to the legacy behavior so tests and
|
|
47
|
+
// manual runs still stamp tracking.json.
|
|
48
|
+
(function selfFilter() {
|
|
49
|
+
if (process.stdin.isTTY) return;
|
|
50
|
+
let command = null;
|
|
51
|
+
try {
|
|
52
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
53
|
+
if (raw) {
|
|
54
|
+
const payload = JSON.parse(raw);
|
|
55
|
+
command = (payload && payload.tool_input && payload.tool_input.command) || "";
|
|
56
|
+
}
|
|
57
|
+
} catch {}
|
|
58
|
+
if (command === null) return;
|
|
59
|
+
if (!/^\s*git\s+push\b/.test(command)) process.exit(0);
|
|
60
|
+
})();
|
|
61
|
+
|
|
43
62
|
function git(args) {
|
|
44
63
|
return spawnSync("git", args, { encoding: "utf8", timeout: 5000, shell: SHELL });
|
|
45
64
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Codex /goal integration
|
|
2
|
+
|
|
3
|
+
When this skill spawns a unit of work on **Codex** (not Claude Code), set the thread goal at the start so Codex's native token-budget + status tracking takes over.
|
|
4
|
+
|
|
5
|
+
## Runtime detection
|
|
6
|
+
|
|
7
|
+
You are on Codex when `~/.codex/` exists and `~/.claude/` is absent or stale. The simplest probe:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
test -f ~/.codex/AGENTS.md && echo codex || echo claude
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If the answer is `claude`, **skip this entire rule** — Claude Code has no equivalent surface and emitting `/goal` text would just be noise.
|
|
14
|
+
|
|
15
|
+
## How to set the goal
|
|
16
|
+
|
|
17
|
+
1. Run the helper to produce the objective string + suggested token budget:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node ~/.codex/bin/codex-goal.js {scope}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`{scope}` is one of: `phase` · `task` · `feature` · `quick`. Use the scope of the current skill.
|
|
24
|
+
|
|
25
|
+
2. The output is two lines:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
/goal {objective text from STATE.md + ROADMAP.md}
|
|
29
|
+
# token_budget suggestion: {N}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. **If the `update_goal` tool is available** to you (Codex exposes it as a model-callable tool), call it directly with:
|
|
33
|
+
- `objective` = the text after `/goal ` on line 1
|
|
34
|
+
- `token_budget` = the integer suggestion on line 2
|
|
35
|
+
|
|
36
|
+
4. **If `update_goal` is not available**, surface the `/goal` line to the user in your next message and let them paste it. Do not silently skip — the goal-set takes 1 second and is the only way Codex's budget telemetry knows what to track.
|
|
37
|
+
|
|
38
|
+
## When NOT to set a goal
|
|
39
|
+
|
|
40
|
+
- The user is on Claude Code (no `/goal` surface).
|
|
41
|
+
- A goal is already active for this thread (Codex rejects `update_goal` when one exists — call `thread/goal/get` first if you're using the tool API directly).
|
|
42
|
+
- The work is open-ended exploration with no clear objective (e.g. `/qualia-idk`, `/qualia-discuss`). Goals are for executing a defined scope.
|
|
43
|
+
|
|
44
|
+
## Why
|
|
45
|
+
|
|
46
|
+
Codex's `thread_goals` table tracks `objective`, `token_budget`, `tokens_used`, and a `status` enum (`active | paused | blocked | usage_limited | budget_limited | complete`). Setting the goal lets the user see burn-vs-budget in the TUI without the framework reinventing it. The token-budget number also makes the model self-aware of how much context it has left for the current unit of work.
|
|
@@ -24,6 +24,10 @@ Execute phase plan. Each task = fresh subagent. Independent tasks run parallel.
|
|
|
24
24
|
|
|
25
25
|
## Process
|
|
26
26
|
|
|
27
|
+
### 0. Codex goal (Codex runtime only)
|
|
28
|
+
|
|
29
|
+
Per `rules/codex-goal.md` — set the thread goal at phase start with scope `phase`.
|
|
30
|
+
|
|
27
31
|
### 1. Load Plan
|
|
28
32
|
|
|
29
33
|
```bash
|
|
@@ -39,6 +39,10 @@ One command for everything between a typo and a phase. Auto-detects scope from t
|
|
|
39
39
|
|
|
40
40
|
## Process
|
|
41
41
|
|
|
42
|
+
### 0. Codex goal (Codex runtime only)
|
|
43
|
+
|
|
44
|
+
Per `rules/codex-goal.md` — set the thread goal with scope matching the auto-detected bucket (`quick` for inline, `feature` for spawn). Do this AFTER Step 2 (auto-detect scope) so the budget matches the actual work shape.
|
|
45
|
+
|
|
42
46
|
### 1. Capture description
|
|
43
47
|
|
|
44
48
|
If invoked without args, ask: **"What do you want to build?"**
|
|
@@ -27,6 +27,10 @@ Spawn planner to break phase into tasks, validate with checker (max 2 revision c
|
|
|
27
27
|
|
|
28
28
|
## Process
|
|
29
29
|
|
|
30
|
+
### 0. Codex goal (Codex runtime only)
|
|
31
|
+
|
|
32
|
+
Per `rules/codex-goal.md` — set the thread goal at plan start with scope `phase`. The objective covers both planning and the subsequent build, so a single goal-set at this stage is enough.
|
|
33
|
+
|
|
30
34
|
### 1. Determine Phase & Load Context
|
|
31
35
|
|
|
32
36
|
```bash
|
package/tests/bin.test.sh
CHANGED
|
@@ -1360,7 +1360,7 @@ if [ "$EXIT" -eq 0 ] \
|
|
|
1360
1360
|
&& [ ! -d "$TMP/.claude" ] \
|
|
1361
1361
|
&& grep -q "Role: OWNER" "$TMP/.codex/AGENTS.md" \
|
|
1362
1362
|
&& ! grep -q "{{ROLE}}" "$TMP/.codex/AGENTS.md" \
|
|
1363
|
-
&& grep -q "
|
|
1363
|
+
&& grep -q "pre-deploy-gate.js" "$TMP/.codex/hooks.json" \
|
|
1364
1364
|
&& grep -q "developer_instructions" "$TMP/.codex/agents/planner.toml" \
|
|
1365
1365
|
&& ! grep -R "\.claude/bin" "$TMP/.codex/skills" >/dev/null 2>&1; then
|
|
1366
1366
|
pass "target=2 → Codex runtime files with Role: OWNER, ~/.claude/ skipped"
|
|
@@ -96,7 +96,7 @@ if [ -f "$HOME_DIR/.codex/AGENTS.md" ] \
|
|
|
96
96
|
&& [ -f "$HOME_DIR/.codex/qualia-references/questioning.md" ] \
|
|
97
97
|
&& grep -q "Role: OWNER" "$HOME_DIR/.codex/AGENTS.md" \
|
|
98
98
|
&& ! grep -q "{{ROLE}}" "$HOME_DIR/.codex/AGENTS.md" \
|
|
99
|
-
&& grep -q "
|
|
99
|
+
&& grep -q "pre-deploy-gate.js" "$HOME_DIR/.codex/hooks.json" \
|
|
100
100
|
&& ! grep -R "\.claude/bin" "$HOME_DIR/.codex/skills" >/dev/null 2>&1; then
|
|
101
101
|
pass "Codex runtime installed with OWNER role"
|
|
102
102
|
else
|