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.
@@ -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.22.0",
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 -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
@@ -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" 2>/dev/null || true`, "timeout": 8 }
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 { 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.22.0",
3
+ "version": "3.24.0",
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",