great-cto 3.22.0 → 3.24.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/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 +28 -3
- 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.0",
|
|
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
|
@@ -322,6 +322,31 @@ dirty-commits: false
|
|
|
322
322
|
*
|
|
323
323
|
* @param skillDir - absolute path to ~/.codex/skills/great_cto (used to build hook commands)
|
|
324
324
|
*/
|
|
325
|
+
/**
|
|
326
|
+
* hooks.json for Codex.
|
|
327
|
+
*
|
|
328
|
+
* Codex implements our hook contract exactly — same events, same wire format,
|
|
329
|
+
* and `permissionDecision` takes `allow | deny | ask`, the words we already
|
|
330
|
+
* write. No translation layer is needed; see
|
|
331
|
+
* docs/analysis/2026-09-05-codex-phase0-findings.md.
|
|
332
|
+
*
|
|
333
|
+
* What needed care is the shell wrapper. Every command here was written as
|
|
334
|
+
* `node "…" 2>/dev/null || true`, which swallows the exit code AND the reason.
|
|
335
|
+
* On secret-scan — a BLOCKING guard — that produced a guard that cannot block
|
|
336
|
+
* and cannot say why: it would have reported clean on every write.
|
|
337
|
+
*
|
|
338
|
+
* So the wrapper is now split by intent, and the test pins both halves:
|
|
339
|
+
*
|
|
340
|
+
* MATCHERS carry both hosts' tool names. Codex writes through `apply_patch` and
|
|
341
|
+
* runs commands through `shell`; the Claude Code names are `edit`/`write`/
|
|
342
|
+
* `str_replace_editor`. This was found by running it: Codex wrote a file
|
|
343
|
+
* containing an API key and secret-scan never fired — the hook was correct, it
|
|
344
|
+
* was never called. A matcher that matches nothing looks exactly like a guard
|
|
345
|
+
* that passed.
|
|
346
|
+
* blocking (secret-scan) bare, so a non-zero exit reaches Codex
|
|
347
|
+
* advisory (format-check, tool-failure) keeps `|| true` — a formatter that
|
|
348
|
+
* dies must not stop the user's write
|
|
349
|
+
*/
|
|
325
350
|
export function getCodexHooksJson(skillDir) {
|
|
326
351
|
const s = skillDir; // shorthand
|
|
327
352
|
return JSON.stringify({
|
|
@@ -341,15 +366,15 @@ export function getCodexHooksJson(skillDir) {
|
|
|
341
366
|
]
|
|
342
367
|
},
|
|
343
368
|
{
|
|
344
|
-
"matcher": "edit|write|str_replace_editor|computer",
|
|
369
|
+
"matcher": "edit|write|str_replace_editor|computer|apply_patch",
|
|
345
370
|
"hooks": [
|
|
346
|
-
{ "command": `node "${s}/scripts/hooks/secret-scan.mjs"
|
|
371
|
+
{ "command": `node "${s}/scripts/hooks/secret-scan.mjs"`, "timeout": 8 }
|
|
347
372
|
]
|
|
348
373
|
}
|
|
349
374
|
],
|
|
350
375
|
"PostToolUse": [
|
|
351
376
|
{
|
|
352
|
-
"matcher": "edit|write|str_replace_editor",
|
|
377
|
+
"matcher": "edit|write|str_replace_editor|apply_patch",
|
|
353
378
|
"hooks": [
|
|
354
379
|
{ "command": `node "${s}/scripts/hooks/format-check.mjs" 2>/dev/null; true`, "timeout": 12, "async": true },
|
|
355
380
|
{ "command": `node "${s}/scripts/hooks/tool-failure.mjs" 2>/dev/null || true`, "timeout": 3, "async": true }
|
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