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.
@@ -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.23.0",
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 -q '\"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",
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
- font-family: var(--mono); font-size: var(--fs-caption);
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
- .memory-doc h1 { font-family: var(--display); font-weight: 600; font-size: 28px; letter-spacing: -0.02em; margin: 0 0 14px; color: var(--text); }
1345
- .memory-doc h2 { font-family: var(--display); font-weight: 600; font-size: 20px; margin: 28px 0 10px; color: var(--text); }
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
- .memory-doc code {
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-gate .pl-name { font-weight: 600; }
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
- .budgets-table th {
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:600">⏳ ${fmtDT(t.updated_at)}</span>`
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:600">⏳ ${fmtDT(t.updated_at || t.created_at)}</span>` : '';
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
- * hooks.json content for ~/.codex/skills/great_cto/hooks.json.
328
+ * The config.toml fragment for Codex — only the part that runs.
314
329
  *
315
- * Phase 0 findings (2026-06-04, confirmed against Codex Desktop binary):
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
- * @param skillDir - absolute path to ~/.codex/skills/great_cto (used to build hook commands)
324
- */
325
- export function getCodexHooksJson(skillDir) {
326
- const s = skillDir; // shorthand
327
- return JSON.stringify({
328
- "SessionStart": [
329
- {
330
- "hooks": [
331
- { "command": `node "${s}/scripts/hooks/quota-check.mjs" 2>/dev/null || true`, "timeout": 15 }
332
- ]
333
- }
334
- ],
335
- "PreToolUse": [
336
- {
337
- "matcher": "computer|bash|shell",
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
- return `# great_cto Codex integration generated by great-cto adapt
375
- # Merge this into ~/.codex/config.toml or copy the sections manually.
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
- [features]
379
- hooks = true
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
- # Set startup timeout generously — npx downloads on first run
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 --platform codex to refresh.
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
- // Write hooks.json + config.toml fragment to .codex/ (project-scoped, trusted)
503
- const defaultSkillDir = `${process.env.HOME ?? "~"}/.codex/skills/great_cto`;
504
- const hooksPath = join(cwd, ".codex", "hooks.json");
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 sections into ~/.codex/config.toml");
512
- console.log(" Then restart Codex to activate hooks and MCP server.");
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
- out.push(` Your ${aiTools.join(" / ")} config is live — 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`.");
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 { homedir } = await import("node:os");
705
- const home = homedir();
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
- log(`Installing great_cto for ${cyan("OpenAI Codex")} (Desktop + CLI).`);
709
- log(`This writes hook, MCP, and agent config to your Codex skill directory.`);
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: showing what would be written."));
713
- log(` would write: ${home}/.codex/skills/great_cto/hooks.json`);
714
- log(` would write: ${home}/.codex/skills/great_cto/scripts/ (hook .mjs files)`);
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 { mkdirSync, copyFileSync, existsSync, writeFileSync } = await import("node:fs");
719
- const { join, dirname } = await import("node:path");
720
- const { fileURLToPath } = await import("node:url");
721
- // ── 1. locate the plugin source ──────────────────────────
722
- // Works whether invoked via npx (global cache) or a local build.
723
- // - npx run: dist/main.js → dist/ → ../ → package root → scripts/hooks/
724
- // - dev run from packages/cli/: same as above
725
- // - repo dev with ts-node: src/main.ts → src/ → ../ → packages/cli/ → ../../scripts/hooks/
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("✓")} Codex host install complete.`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.23.0",
3
+ "version": "3.24.1",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",