great-cto 3.23.0 → 3.24.1
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/board/.claude-plugin/plugin.json +2 -2
- package/board/packages/board/public/index.html +30 -10
- package/board/scripts/lib/gate-reversibility.mjs +6 -0
- package/dist/adapt.js +73 -97
- package/dist/codex.js +60 -0
- package/dist/main.js +42 -87
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
3
|
"description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 70 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.24.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"hooks": [
|
|
56
56
|
{
|
|
57
57
|
"type": "command",
|
|
58
|
-
"command": "PLUGIN_DIR=$(ls -d ~/.claude/plugins/cache/local/great_cto/*/ 2>/dev/null | sort -V | tail -1 | sed 's|/$||'); if [ -f .claude-plugin/plugin.json ] && grep -
|
|
58
|
+
"command": "PLUGIN_DIR=$(ls -d ~/.claude/plugins/cache/local/great_cto/*/ 2>/dev/null | sort -V | tail -1 | sed 's|/$||'); if [ -f .claude-plugin/plugin.json ] && grep -qE '\"name\": *\"great[_-]cto\"' .claude-plugin/plugin.json 2>/dev/null; then : ; elif [ -n \"$PLUGIN_DIR\" ] && [ -f \"${PLUGIN_DIR}/shared/orchestrator.toml\" ]; then mkdir -p shared; cp \"${PLUGIN_DIR}/shared/orchestrator.toml\" shared/orchestrator.toml 2>/dev/null || true; cp \"${PLUGIN_DIR}/shared/pipeline.toml\" shared/pipeline.toml 2>/dev/null || true; fi",
|
|
59
59
|
"timeout": 5,
|
|
60
60
|
"statusMessage": "Refreshing orchestrator contract..."
|
|
61
61
|
},
|
|
@@ -772,7 +772,9 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
772
772
|
.card-footer > * { flex-shrink: 0; }
|
|
773
773
|
.card-time {
|
|
774
774
|
margin-left: auto;
|
|
775
|
-
|
|
775
|
+
/* Explicit, because the weight is inherited from an ancestor while the family
|
|
776
|
+
is switched here. Geist Mono stops at 500; anything above is synthetic. */
|
|
777
|
+
font-family: var(--mono); font-weight: 500; font-size: var(--fs-caption);
|
|
776
778
|
color: var(--text3);
|
|
777
779
|
}
|
|
778
780
|
|
|
@@ -1341,13 +1343,27 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
1341
1343
|
max-width: 880px;
|
|
1342
1344
|
margin: 0 auto;
|
|
1343
1345
|
}
|
|
1344
|
-
|
|
1345
|
-
.
|
|
1346
|
+
/* 500, not 600: --display is Geist Mono and the vendored face's weight axis
|
|
1347
|
+
ends at 500. Asking for 600 does not get a bolder cut — the browser paints a
|
|
1348
|
+
synthetic bold, which is a smeared stroke rather than a designed one, and
|
|
1349
|
+
tests/lib/rendered-contrast.test.mjs fails on exactly that. The tell is that
|
|
1350
|
+
600 and 700 measure identically: a real axis moves, a synthetic stroke does
|
|
1351
|
+
not. Both headings are fixed together; the test only caught the h1 because
|
|
1352
|
+
the h2 happened not to be on screen. */
|
|
1353
|
+
.memory-doc h1 { font-family: var(--display); font-weight: 500; font-size: 28px; letter-spacing: -0.02em; margin: 0 0 14px; color: var(--text); }
|
|
1354
|
+
.memory-doc h2 { font-family: var(--display); font-weight: 500; font-size: 20px; margin: 28px 0 10px; color: var(--text); }
|
|
1346
1355
|
.memory-doc h3 { font-family: var(--sans); font-weight: 600; font-size: var(--fs-title-s); margin: 18px 0 8px; color: var(--text); }
|
|
1347
1356
|
.memory-doc p { font-size: var(--fs-base); line-height: 1.65; color: var(--text2); margin: 0 0 10px; }
|
|
1348
1357
|
.memory-doc ul, .memory-doc ol { padding-left: 22px; margin: 0 0 12px; color: var(--text2); }
|
|
1349
1358
|
.memory-doc li { line-height: 1.6; font-size: var(--fs-base); }
|
|
1350
|
-
.
|
|
1359
|
+
/* An explicit weight, because inheritance crosses the family boundary. A
|
|
1360
|
+
heading is sans at 600 — legitimate — and `code` inside it switches the family
|
|
1361
|
+
to Geist Mono while KEEPING the inherited 600. That face's axis ends at 500,
|
|
1362
|
+
so the browser paints a synthetic bold. No CSS rule sets a mono weight above
|
|
1363
|
+
500 anywhere in this file; the offending value arrives from a parent, which
|
|
1364
|
+
is why grepping for it found nothing. */
|
|
1365
|
+
.memory-doc code {
|
|
1366
|
+
font-weight: 500;
|
|
1351
1367
|
font-family: var(--mono); font-size: var(--fs-small);
|
|
1352
1368
|
background: var(--bg-muted);
|
|
1353
1369
|
padding: 1px 6px; border-radius: 0;
|
|
@@ -1546,7 +1562,10 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
1546
1562
|
padding: 12px 12px 10px;
|
|
1547
1563
|
}
|
|
1548
1564
|
.pl-gate .gate-glyph { font-size: var(--fs-small); line-height: 1; flex-shrink: 0; filter: grayscale(1); opacity: 0.6; }
|
|
1549
|
-
.pl-
|
|
1565
|
+
/* 500: .pl-name is mono (see .pl-stage .pl-name) and Geist Mono's axis ends
|
|
1566
|
+
there. 600 buys a synthetic bold, not a bolder cut. The .gate-glyph inside it
|
|
1567
|
+
inherits this, which is why it was failing too. */
|
|
1568
|
+
.pl-gate .pl-name { font-weight: 500; }
|
|
1550
1569
|
.pl-gate.active {
|
|
1551
1570
|
border-left-color: var(--status-blocked);
|
|
1552
1571
|
background: color-mix(in srgb, var(--status-blocked) 8%, var(--surface-translucent));
|
|
@@ -1664,7 +1683,7 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
1664
1683
|
border-bottom: 1px solid var(--border);
|
|
1665
1684
|
font-size: var(--fs-small); white-space: nowrap;
|
|
1666
1685
|
}
|
|
1667
|
-
.cost-table th { font-family: var(--mono); font-size: var(--fs-eyebrow); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text2); }
|
|
1686
|
+
.cost-table th { font-family: var(--mono); font-weight: 500; font-size: var(--fs-eyebrow); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text2); }
|
|
1668
1687
|
.cost-table td.num { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; }
|
|
1669
1688
|
.cost-axis {
|
|
1670
1689
|
display: flex; justify-content: space-between;
|
|
@@ -2082,10 +2101,11 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
2082
2101
|
background: var(--bg-card); color: var(--text); min-width: 26ch;
|
|
2083
2102
|
}
|
|
2084
2103
|
.budgets-table { width: 100%; border-collapse: collapse; font-size: var(--fs-body); }
|
|
2085
|
-
.
|
|
2086
|
-
text-align: left; font-family: var(--mono); font-size: var(--fs-eyebrow);
|
|
2104
|
+
.
|
|
2105
|
+
text-align: left; font-family: var(--mono); font-weight: 500; font-size: var(--fs-eyebrow);
|
|
2087
2106
|
letter-spacing: 0.08em; text-transform: uppercase; color: var(--text2);
|
|
2088
2107
|
padding: 0 12px 8px 0; border-bottom: 1px solid var(--border);
|
|
2108
|
+
id var(--border);
|
|
2089
2109
|
}
|
|
2090
2110
|
.budgets-table td { padding: 10px 12px 10px 0; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
|
2091
2111
|
/* A row has to answer three questions: what is this agent, what does it cost,
|
|
@@ -4190,7 +4210,7 @@ function renderInboxList(rootId, items, countId, opts = {}) {
|
|
|
4190
4210
|
// Gates (the showApprove list) get the exact date+time the decision has been
|
|
4191
4211
|
// waiting since; other rows carry their age in the right-hand column instead.
|
|
4192
4212
|
(opts.showApprove || t.is_gate === true) && t.updated_at
|
|
4193
|
-
? `<span title="${esc(fmtDate(t.updated_at))}" style="color:var(--status-gate);font-weight:
|
|
4213
|
+
? `<span title="${esc(fmtDate(t.updated_at))}" style="color:var(--status-gate);font-weight:500">⏳ ${fmtDT(t.updated_at)}</span>`
|
|
4194
4214
|
: '',
|
|
4195
4215
|
].filter(Boolean).join(' · ');
|
|
4196
4216
|
// 48h is this board's own staleness line — the "Stale (in progress > 48h)"
|
|
@@ -6708,7 +6728,7 @@ function cardHTML(t) {
|
|
|
6708
6728
|
? `<span class="card-time">${relTime(t.updated_at)}</span>`
|
|
6709
6729
|
// A gate is a human decision — show exactly WHEN it's been waiting since (date + time).
|
|
6710
6730
|
: (isGate && (t.updated_at || t.created_at))
|
|
6711
|
-
? `<span class="card-time" title="${esc(fmtDate(t.updated_at || t.created_at))}" style="color:var(--status-gate);font-weight:
|
|
6731
|
+
? `<span class="card-time" title="${esc(fmtDate(t.updated_at || t.created_at))}" style="color:var(--status-gate);font-weight:500">⏳ ${fmtDT(t.updated_at || t.created_at)}</span>` : '';
|
|
6712
6732
|
const descPreview = t.description ? `<div class="card-desc">${esc(t.description.slice(0, 110))}${t.description.length > 110 ? '…' : ''}</div>` : '';
|
|
6713
6733
|
const statusColor = { backlog:'var(--text3)', in_progress:'var(--status-progress)', done:'var(--status-done)', blocked:'var(--status-blocked)', gate:'var(--status-gate)' };
|
|
6714
6734
|
const statusDotColor = statusColor[t.status] || 'var(--text3)';
|
|
@@ -31,6 +31,12 @@ export const CATEGORIES = Object.freeze({
|
|
|
31
31
|
// Different currency — sunk work rather than an external effect — so it is
|
|
32
32
|
// named separately instead of being folded in.
|
|
33
33
|
'sunk-work': 'reversing it means redoing every stage that ran after it',
|
|
34
|
+
// Also not one of ADR-009's four, and added for `agent-posture.mjs`, which
|
|
35
|
+
// reuses this vocabulary rather than inventing a second one for the same axis.
|
|
36
|
+
// A leaked credential is not undone by reverting anything: a key that reached
|
|
37
|
+
// 605 transcripts stayed valid until it was revoked, and revocation was the
|
|
38
|
+
// only fix. Distinct currency again — so, named, not folded in.
|
|
39
|
+
'unrevocable-disclosure': 'a secret that has been read cannot be un-read; only revocation ends it',
|
|
34
40
|
});
|
|
35
41
|
|
|
36
42
|
/**
|
package/dist/adapt.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// great-cto adapt --dry-run Show what would be written
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
|
+
import { codexInstallPlan } from "./codex.js";
|
|
12
13
|
const VALID_AI_TOOLS = ["claude-code", "cursor", "copilot", "windsurf", "aider", "codex"];
|
|
13
14
|
function parseAiTools(text) {
|
|
14
15
|
const line = text.match(/^ai_tools:\s*(.+)$/m)?.[1] ?? "";
|
|
@@ -309,109 +310,68 @@ dirty-commits: false
|
|
|
309
310
|
`;
|
|
310
311
|
}
|
|
311
312
|
// ── Codex adapter helpers ──────────────────────────────────────────────────
|
|
313
|
+
// `getCodexHooksJson` used to live here, and `adapt` wrote its output to
|
|
314
|
+
// `.codex/hooks.json`. Six tests certified its shape — the split between
|
|
315
|
+
// blocking and advisory wrappers, the matchers covering `apply_patch` — and all
|
|
316
|
+
// six passed while the file went to a path Codex does not read.
|
|
317
|
+
//
|
|
318
|
+
// Two upstream issues settle it (openai/codex #16430, #39895): hooks load from
|
|
319
|
+
// the config layer only, a plugin cannot carry them, and the `hooks` field is
|
|
320
|
+
// read by nothing "with no warning, error, or log line". `.codex/hooks.json`,
|
|
321
|
+
// project-scoped, was never even the config layer. See
|
|
322
|
+
// docs/analysis/2026-09-05-codex-phase0-findings.md.
|
|
323
|
+
//
|
|
324
|
+
// So the generator is gone rather than corrected. A tested generator writing to
|
|
325
|
+
// a path nothing reads is the exact defect this repository exists to delete: it
|
|
326
|
+
// looked more finished than an absent one.
|
|
312
327
|
/**
|
|
313
|
-
*
|
|
328
|
+
* The config.toml fragment for Codex — only the part that runs.
|
|
314
329
|
*
|
|
315
|
-
*
|
|
316
|
-
* - Hook events: PreToolUse, PostToolUse, SessionStart, SubagentStart,
|
|
317
|
-
* SubagentStop, PreCompact, PostCompact, UserPromptSubmit, PermissionRequest.
|
|
318
|
-
* - stdin payload: same keys as Claude Code (tool_name, tool_input, is_error).
|
|
319
|
-
* - block/deny: hookSpecificOutput.permissionDecision (same struct as CC)
|
|
320
|
-
* confirmed by binary string literals.
|
|
321
|
-
* - Hooks enabled by [features].hooks = true in config.toml.
|
|
330
|
+
* What this used to emit, and why each line is gone:
|
|
322
331
|
*
|
|
323
|
-
*
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
"hooks": [
|
|
339
|
-
{ "command": `node "${s}/scripts/hooks/orchestrator-check.mjs" 2>/dev/null || true`, "timeout": 5 },
|
|
340
|
-
{ "command": `node "${s}/scripts/hooks/cost-guard.mjs" 2>/dev/null || true`, "timeout": 5 }
|
|
341
|
-
]
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
"matcher": "edit|write|str_replace_editor|computer",
|
|
345
|
-
"hooks": [
|
|
346
|
-
{ "command": `node "${s}/scripts/hooks/secret-scan.mjs" 2>/dev/null || true`, "timeout": 8 }
|
|
347
|
-
]
|
|
348
|
-
}
|
|
349
|
-
],
|
|
350
|
-
"PostToolUse": [
|
|
351
|
-
{
|
|
352
|
-
"matcher": "edit|write|str_replace_editor",
|
|
353
|
-
"hooks": [
|
|
354
|
-
{ "command": `node "${s}/scripts/hooks/format-check.mjs" 2>/dev/null; true`, "timeout": 12, "async": true },
|
|
355
|
-
{ "command": `node "${s}/scripts/hooks/tool-failure.mjs" 2>/dev/null || true`, "timeout": 3, "async": true }
|
|
356
|
-
]
|
|
357
|
-
},
|
|
358
|
-
{
|
|
359
|
-
"matcher": "",
|
|
360
|
-
"hooks": [
|
|
361
|
-
{ "command": `node "${s}/scripts/hooks/tool-failure.mjs" 2>/dev/null || true`, "timeout": 3, "async": true }
|
|
362
|
-
]
|
|
363
|
-
}
|
|
364
|
-
]
|
|
365
|
-
}, null, 2);
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* config.toml fragment to merge into ~/.codex/config.toml.
|
|
369
|
-
* Adds: [features].hooks=true, [mcp_servers.great_cto], [agents.great_cto_skill].
|
|
370
|
-
* Written as a separate file (.codex/great_cto.toml) since merging TOML is
|
|
371
|
-
* non-trivial; installer prints merge instructions.
|
|
332
|
+
* [features] hooks = true turns on a feature a plugin cannot reach
|
|
333
|
+
* (openai/codex#16430) and which has an open
|
|
334
|
+
* timeout regression on 0.148.0+ (#42279)
|
|
335
|
+
* CODEX_SKILL_DIR = ~/.codex/skills/great_cto
|
|
336
|
+
* a path a working Codex install does not have;
|
|
337
|
+
* Phase 0 checked, the directory is not created
|
|
338
|
+
* [agents.great_cto_*] six role descriptions presented as "the
|
|
339
|
+
* specialist fleet", with a comment claiming
|
|
340
|
+
* "full 57-agent routing" — we ship seventy, and
|
|
341
|
+
* Codex routes none of them
|
|
342
|
+
*
|
|
343
|
+
* An MCP server is the one thing here Codex actually starts, so it is the one
|
|
344
|
+
* thing left. The rest of the fragment is now a statement of what does NOT come
|
|
345
|
+
* with it, taken from `codexInstallPlan` so the two cannot drift apart again —
|
|
346
|
+
* which is precisely how they drifted the first time.
|
|
372
347
|
*/
|
|
373
348
|
function getCodexConfigFragment(meta) {
|
|
374
|
-
|
|
375
|
-
|
|
349
|
+
const plan = codexInstallPlan({ repoDir: "<path-to-great_cto>", codexOnPath: true });
|
|
350
|
+
const notes = plan.notSupported.map((n) => `# - ${n}`).join("\n");
|
|
351
|
+
return `# great_cto ⇄ Codex — generated by great-cto adapt
|
|
376
352
|
# Generated: ${new Date().toISOString().slice(0, 10)}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
353
|
+
#
|
|
354
|
+
# This file contains ONE section, because one section is what Codex runs from a
|
|
355
|
+
# config fragment. Merge it into ~/.codex/config.toml.
|
|
356
|
+
#
|
|
357
|
+
# For skills, install the plugin instead of copying anything by hand:
|
|
358
|
+
# ${plan.commands.join("\n# ")}
|
|
359
|
+
#
|
|
360
|
+
# What does NOT come with either, on codex-cli as it stands today:
|
|
361
|
+
${notes}
|
|
362
|
+
#
|
|
363
|
+
# Details, measured rather than inferred:
|
|
364
|
+
# docs/analysis/2026-09-05-codex-phase0-findings.md
|
|
380
365
|
|
|
381
366
|
[mcp_servers.great_cto]
|
|
382
367
|
command = "npx"
|
|
383
368
|
args = ["great-cto@latest", "mcp"]
|
|
384
|
-
#
|
|
369
|
+
# Generous: npx downloads on first run.
|
|
385
370
|
startup_timeout_sec = 60
|
|
386
371
|
|
|
387
|
-
[mcp_servers.great_cto.env]
|
|
388
|
-
# Point hooks at the skill dir so hook scripts resolve correctly.
|
|
389
|
-
CODEX_SKILL_DIR = "${process.env.HOME ?? "~"}/.codex/skills/great_cto"
|
|
390
|
-
|
|
391
|
-
# Agents exposed via MCP (great_cto specialist fleet)
|
|
392
|
-
# Codex will use these as subagent personalities via the [agents] table.
|
|
393
|
-
# Full 57-agent routing is handled inside AGENTS.md / SKILL.md.
|
|
394
|
-
[agents.great_cto_architect]
|
|
395
|
-
description = "Deep architectural reasoning, ADRs, cross-cutting design decisions"
|
|
396
|
-
|
|
397
|
-
[agents.great_cto_senior_dev]
|
|
398
|
-
description = "Feature implementation with TDD — RED→GREEN→REFACTOR"
|
|
399
|
-
|
|
400
|
-
[agents.great_cto_qa_engineer]
|
|
401
|
-
description = "QA reports, coverage, regression check after implementation"
|
|
402
|
-
|
|
403
|
-
[agents.great_cto_security_officer]
|
|
404
|
-
description = "Security audit — OWASP Top 10, auth, PII, secrets"
|
|
405
|
-
|
|
406
|
-
[agents.great_cto_devops]
|
|
407
|
-
description = "Deploy, canary rollout, rollback, SLO monitoring"
|
|
408
|
-
|
|
409
|
-
[agents.great_cto_pm]
|
|
410
|
-
description = "Feature decomposition into tasks, timeline, dependency graph"
|
|
411
|
-
|
|
412
372
|
# Archetype: ${meta.archetype}
|
|
413
373
|
# Compliance: ${meta.compliance.join(", ") || "none"}
|
|
414
|
-
# Re-run great-cto adapt
|
|
374
|
+
# Re-run great-cto adapt to refresh.
|
|
415
375
|
`;
|
|
416
376
|
}
|
|
417
377
|
function getCliVersion() {
|
|
@@ -499,17 +459,30 @@ conventions. Generated by \`great-cto adapt\`.
|
|
|
499
459
|
break;
|
|
500
460
|
}
|
|
501
461
|
case "codex": {
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
462
|
+
// One file, and only the MCP section in it. The previous version also
|
|
463
|
+
// wrote `.codex/hooks.json` and then said "restart Codex to activate
|
|
464
|
+
// hooks and MCP server" — half of which was true. A user who followed
|
|
465
|
+
// that line came away believing the gate chain and secret-scan were
|
|
466
|
+
// guarding their session on Codex. They were not, and nothing said so.
|
|
505
467
|
const configPath = join(cwd, ".codex", "great_cto.toml");
|
|
506
|
-
if (writeFile(hooksPath, getCodexHooksJson(defaultSkillDir), dryRun))
|
|
507
|
-
out.push(".codex/hooks.json");
|
|
508
468
|
if (writeFile(configPath, getCodexConfigFragment(meta), dryRun))
|
|
509
469
|
out.push(".codex/great_cto.toml");
|
|
470
|
+
// A file we used to write and no longer do stays on disk. Say so rather
|
|
471
|
+
// than delete it: it is in the user's project, and it may be theirs now.
|
|
472
|
+
const stale = join(cwd, ".codex", "hooks.json");
|
|
510
473
|
if (!dryRun) {
|
|
511
|
-
console.log(" ⚠ Codex: merge .codex/great_cto.toml
|
|
512
|
-
console.log("
|
|
474
|
+
console.log(" ⚠ Codex: merge .codex/great_cto.toml into ~/.codex/config.toml, then restart Codex.");
|
|
475
|
+
console.log(" That gives you the MCP server. For the skills, install the plugin:");
|
|
476
|
+
for (const c of codexInstallPlan({ repoDir: "<path-to-great_cto>", codexOnPath: true }).commands) {
|
|
477
|
+
console.log(` ${c}`);
|
|
478
|
+
}
|
|
479
|
+
console.log(" Hooks, slash commands and the role agents do NOT carry over to Codex today —");
|
|
480
|
+
console.log(" see docs/analysis/2026-09-05-codex-phase0-findings.md.");
|
|
481
|
+
if (existsSync(stale)) {
|
|
482
|
+
console.log("");
|
|
483
|
+
console.log(" ⚠ .codex/hooks.json exists and an older great-cto wrote it. Codex never read it");
|
|
484
|
+
console.log(" (openai/codex#16430) — delete it yourself if it is not yours.");
|
|
485
|
+
}
|
|
513
486
|
}
|
|
514
487
|
break;
|
|
515
488
|
}
|
|
@@ -568,7 +541,10 @@ export function nextStepsAfterAdapt(aiTools = []) {
|
|
|
568
541
|
out.push(" 1. Restart Claude Code so it picks up the regenerated CLAUDE.md.", " 2. /audit — full analysis of this codebase (existing project)", " /start — scope and build a new feature (greenfield)", " /inbox — what needs your decision right now", " 3. great-cto board — Kanban + CTO dashboard at localhost:3141");
|
|
569
542
|
}
|
|
570
543
|
else {
|
|
571
|
-
|
|
544
|
+
// "is live" was wrong for Codex, whose fragment the user still has to merge
|
|
545
|
+
// into ~/.codex/config.toml by hand — and a line that says a thing is live
|
|
546
|
+
// is exactly how an unmerged file passes for a working one.
|
|
547
|
+
out.push(` Config written for ${aiTools.join(" / ")}. A fragment that says to merge it is not active`, " until you do — then reload the tool to pick it up.", " The agent pipeline (architecture review, QA, security gates) runs inside", " Claude Code. To use it here: add claude-code to ai_tools in PROJECT.md,", " then `npx great-cto install`.");
|
|
572
548
|
}
|
|
573
549
|
return out;
|
|
574
550
|
}
|
package/dist/codex.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Installing great_cto for OpenAI Codex.
|
|
2
|
+
//
|
|
3
|
+
// The first version of this wrote hook scripts into `~/.codex/skills/great_cto/`,
|
|
4
|
+
// emitted a TOML fragment for the user to merge by hand, and set
|
|
5
|
+
// `[features] hooks = true` alongside `[hooks_files]`. Phase 0 checked every one
|
|
6
|
+
// of those against a real codex-cli 0.153.4
|
|
7
|
+
// (docs/analysis/2026-09-05-codex-phase0-findings.md):
|
|
8
|
+
//
|
|
9
|
+
// ~/.codex/skills/ does not exist on a working install
|
|
10
|
+
// [hooks_files] is used by no shipped plugin
|
|
11
|
+
// hooks on Codex unverified — the config key parses, and that is all
|
|
12
|
+
//
|
|
13
|
+
// None of it was covered by a test, so it had shipped as a working feature while
|
|
14
|
+
// writing to a path nothing reads.
|
|
15
|
+
//
|
|
16
|
+
// Codex ships `codex plugin marketplace add`, taking a local or Git source, and
|
|
17
|
+
// that path was verified end to end: the plugin reports `installed, enabled
|
|
18
|
+
// 3.23.0` with all 40 skills present. So this emits the supported commands and,
|
|
19
|
+
// as importantly, says what does not come with them.
|
|
20
|
+
/**
|
|
21
|
+
* What installing for Codex means, decided separately from doing it.
|
|
22
|
+
*
|
|
23
|
+
* Pure so the decision can be tested without touching a real Codex install —
|
|
24
|
+
* the previous version was untestable by construction and therefore untested.
|
|
25
|
+
*/
|
|
26
|
+
export function codexInstallPlan({ repoDir, codexOnPath }) {
|
|
27
|
+
if (!codexOnPath) {
|
|
28
|
+
// Refuse rather than write half of it. Config for a CLI that is not present
|
|
29
|
+
// leaves the user with files and no way to use them, which is how the old
|
|
30
|
+
// path failed by construction.
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
why: "codex is not on PATH — install it first: npm i -g @openai/codex",
|
|
34
|
+
commands: [],
|
|
35
|
+
supported: [],
|
|
36
|
+
notSupported: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
why: "",
|
|
42
|
+
commands: [
|
|
43
|
+
`codex plugin marketplace add ${repoDir}`,
|
|
44
|
+
`codex plugin add great-cto@great-cto`,
|
|
45
|
+
],
|
|
46
|
+
// Verified present after install, not assumed.
|
|
47
|
+
supported: [
|
|
48
|
+
"skills — all of them, the same tree Claude Code reads",
|
|
49
|
+
"the MCP server (great_cto), resolved from npm",
|
|
50
|
+
],
|
|
51
|
+
// Checked across every shipped Codex plugin: no manifest declares any of
|
|
52
|
+
// these. Saying "installed" without saying this would promise a pipeline the
|
|
53
|
+
// host cannot run.
|
|
54
|
+
notSupported: [
|
|
55
|
+
"hooks — no plugin surface; the gate chain and secret-scan do not carry over",
|
|
56
|
+
"slash commands — no plugin surface",
|
|
57
|
+
"role agents — Codex plugins carry interface metadata, not agent roles",
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -701,101 +701,56 @@ ${bold("Links:")}
|
|
|
701
701
|
`);
|
|
702
702
|
}
|
|
703
703
|
async function runInitCodex(args) {
|
|
704
|
-
const {
|
|
705
|
-
const
|
|
704
|
+
const { codexInstallPlan } = await import("./codex.js");
|
|
705
|
+
const { execFileSync, spawnSync } = await import("node:child_process");
|
|
706
706
|
log(bold("great-cto · Codex host install"));
|
|
707
707
|
log("");
|
|
708
|
-
|
|
709
|
-
|
|
708
|
+
// Is the host actually there? The previous version of this function wrote
|
|
709
|
+
// hook scripts and a config fragment without checking, into
|
|
710
|
+
// `~/.codex/skills/great_cto/` — a directory a working Codex install does not
|
|
711
|
+
// have. It reported success either way.
|
|
712
|
+
let codexOnPath = false;
|
|
713
|
+
try {
|
|
714
|
+
execFileSync("codex", ["--version"], { stdio: "ignore", timeout: 15000 });
|
|
715
|
+
codexOnPath = true;
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
codexOnPath = false;
|
|
719
|
+
}
|
|
720
|
+
const plan = codexInstallPlan({ repoDir: args.dir, codexOnPath });
|
|
721
|
+
if (!plan.ok) {
|
|
722
|
+
error(plan.why);
|
|
723
|
+
return 1;
|
|
724
|
+
}
|
|
725
|
+
log(`This registers ${cyan(args.dir)} as a Codex plugin marketplace and installs from it.`);
|
|
726
|
+
log("");
|
|
727
|
+
log(bold("Carries over:"));
|
|
728
|
+
for (const s2 of plan.supported)
|
|
729
|
+
log(` ${green("✓")} ${s2}`);
|
|
730
|
+
log("");
|
|
731
|
+
// Printed on every run, not buried in docs. An install that lists only what it
|
|
732
|
+
// gives promises a pipeline this host cannot run.
|
|
733
|
+
log(bold("Does NOT carry over on Codex:"));
|
|
734
|
+
for (const s2 of plan.notSupported)
|
|
735
|
+
log(` ${yellow("—")} ${s2}`);
|
|
710
736
|
log("");
|
|
711
737
|
if (args.dryRun) {
|
|
712
|
-
log(yellow("dry-run:
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
log(` would update: .codex/great_cto.toml (merge into ~/.codex/config.toml)`);
|
|
738
|
+
log(yellow("dry-run: these commands would run."));
|
|
739
|
+
for (const c of plan.commands)
|
|
740
|
+
log(` ${dim(c)}`);
|
|
716
741
|
return 0;
|
|
717
742
|
}
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
727
|
-
const hooksSearchPaths = [
|
|
728
|
-
join(here, "..", "scripts", "hooks"), // npm install: dist/../scripts/hooks
|
|
729
|
-
join(here, "..", "..", "scripts", "hooks"), // monorepo: packages/cli/dist/../../scripts
|
|
730
|
-
join(here, "..", "..", "..", "scripts", "hooks"), // ts-node: src/../../../scripts
|
|
731
|
-
];
|
|
732
|
-
const hooksDir = hooksSearchPaths.find(p => existsSync(p));
|
|
733
|
-
if (!hooksDir) {
|
|
734
|
-
error("Could not locate scripts/hooks/. Try: npx great-cto@latest --host codex");
|
|
735
|
-
return 1;
|
|
743
|
+
for (const c of plan.commands) {
|
|
744
|
+
log(dim(` $ ${c}`));
|
|
745
|
+
const parts = c.split(" ");
|
|
746
|
+
const r = spawnSync(parts[0], parts.slice(1), { stdio: "inherit", timeout: 120000 });
|
|
747
|
+
if (r.status !== 0) {
|
|
748
|
+
error(`command failed: ${c}`);
|
|
749
|
+
return 1;
|
|
750
|
+
}
|
|
736
751
|
}
|
|
737
|
-
// ── 2. install skill dir ──────────────────────────────────
|
|
738
|
-
const skillDir = join(home, ".codex", "skills", "great_cto");
|
|
739
|
-
const skillScriptsDir = join(skillDir, "scripts", "hooks");
|
|
740
|
-
mkdirSync(skillScriptsDir, { recursive: true });
|
|
741
|
-
log(dim(` skill dir: ${skillDir}`));
|
|
742
|
-
// Copy hook scripts
|
|
743
|
-
const HOOKS_TO_COPY = [
|
|
744
|
-
"quota-check.mjs",
|
|
745
|
-
"secret-scan.mjs",
|
|
746
|
-
"cost-guard.mjs",
|
|
747
|
-
"orchestrator-check.mjs",
|
|
748
|
-
"format-check.mjs",
|
|
749
|
-
"tool-failure.mjs",
|
|
750
|
-
"summary-enforce.mjs",
|
|
751
|
-
];
|
|
752
|
-
let copied = 0;
|
|
753
|
-
for (const hook of HOOKS_TO_COPY) {
|
|
754
|
-
const src = join(hooksDir, hook);
|
|
755
|
-
const dst = join(skillScriptsDir, hook);
|
|
756
|
-
if (existsSync(src)) {
|
|
757
|
-
copyFileSync(src, dst);
|
|
758
|
-
copied++;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
log(` ${green("✓")} ${copied} hook scripts → ${skillScriptsDir}`);
|
|
762
|
-
// Write hooks.json into the skill dir
|
|
763
|
-
const { getCodexHooksJson } = await import("./adapt.js");
|
|
764
|
-
const hooksJson = getCodexHooksJson(skillDir);
|
|
765
|
-
writeFileSync(join(skillDir, "hooks.json"), hooksJson);
|
|
766
|
-
log(` ${green("✓")} wrote ~/.codex/skills/great_cto/hooks.json`);
|
|
767
|
-
// ── 3. write project-scoped config fragment ───────────────
|
|
768
|
-
const configFrag = join(args.dir, ".codex", "great_cto.toml");
|
|
769
|
-
mkdirSync(dirname(configFrag), { recursive: true });
|
|
770
|
-
writeFileSync(configFrag, [
|
|
771
|
-
`# great_cto Codex integration — generated ${new Date().toISOString().slice(0, 10)}`,
|
|
772
|
-
`# Add these sections to ~/.codex/config.toml`,
|
|
773
|
-
``,
|
|
774
|
-
`[features]`,
|
|
775
|
-
`hooks = true`,
|
|
776
|
-
``,
|
|
777
|
-
`[mcp_servers.great_cto]`,
|
|
778
|
-
`command = "npx"`,
|
|
779
|
-
`args = ["great-cto@latest", "mcp"]`,
|
|
780
|
-
`startup_timeout_sec = 60`,
|
|
781
|
-
``,
|
|
782
|
-
`[mcp_servers.great_cto.env]`,
|
|
783
|
-
`CODEX_SKILL_DIR = "${skillDir}"`,
|
|
784
|
-
``,
|
|
785
|
-
`[hooks_files]`,
|
|
786
|
-
`paths = ["${join(skillDir, "hooks.json")}"]`,
|
|
787
|
-
].join("\n") + "\n");
|
|
788
|
-
log(` ${green("✓")} wrote .codex/great_cto.toml`);
|
|
789
|
-
log("");
|
|
790
|
-
log(bold("Next steps:"));
|
|
791
|
-
log(` 1. Merge ${cyan(".codex/great_cto.toml")} sections into ${cyan("~/.codex/config.toml")}`);
|
|
792
|
-
log(` 2. Add ${cyan(args.dir)} to trusted projects in config.toml if not already:`);
|
|
793
|
-
log(` ${dim(`[projects."${args.dir}"]`)}`);
|
|
794
|
-
log(` ${dim(`trust_level = "trusted"`)}`);
|
|
795
|
-
log(` 3. Restart Codex Desktop / CLI to activate hooks and MCP server.`);
|
|
796
|
-
log(` 4. Verify: run ${cyan("great-cto mcp")} in a terminal — should list great_cto tools.`);
|
|
797
752
|
log("");
|
|
798
|
-
log(`${green("✓")}
|
|
753
|
+
log(`${green("✓")} installed. Verify with: ${cyan("codex plugin list")}`);
|
|
799
754
|
return 0;
|
|
800
755
|
}
|
|
801
756
|
async function runInit(args) {
|
package/package.json
CHANGED