great-cto 3.63.0 → 3.63.2

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 72 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.63.0",
4
+ "version": "3.63.2",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -60,7 +60,7 @@
60
60
  },
61
61
  {
62
62
  "type": "command",
63
- "command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; export PATH=\"/opt/homebrew/bin:$HOME/.local/bin:/usr/local/bin:$PATH\"; PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed 's|/$||')}; if [ -z \"$PLUGIN_DIR\" ]; then echo 'GREAT_CTO: plugin dir not found \u2014 run /update'; else mkdir -p ~/.great_cto .great_cto; node \"${PLUGIN_DIR}/scripts/lib/sync-managed.mjs\" --plugin-dir \"${PLUGIN_DIR}\" 2>/dev/null || true; cp \"${PLUGIN_DIR}/skills/great_cto/ARCHETYPES.md\" .great_cto/ARCHETYPES.md 2>/dev/null || true; cp \"${PLUGIN_DIR}/skills/great_cto/SKILL.md\" .great_cto/SKILL.md 2>/dev/null || true; { printf \"export PATH=/opt/homebrew/bin:$HOME/.local/bin:/usr/local/bin:$PATH\\nexport ARCHETYPES_MD=.great_cto/ARCHETYPES.md\\n\"; } > .great_cto/env.sh; fi; if [ ! -d ~/.great_cto/catalog ]; then git clone --depth=1 https://github.com/davila7/claude-code-templates.git ~/.great_cto/catalog 2>/dev/null & fi; if [ ! -d ~/.great_cto/anthropic-skills ]; then git clone --depth=1 https://github.com/anthropics/skills.git ~/.great_cto/anthropic-skills 2>/dev/null & elif [ $(($(date +%s) - $(stat -f %m ~/.great_cto/anthropic-skills/.git 2>/dev/null || stat -c %Y ~/.great_cto/anthropic-skills/.git 2>/dev/null || echo 0))) -gt 604800 ]; then (cd ~/.great_cto/anthropic-skills && git pull --quiet 2>/dev/null) & fi; if [ ! -d ~/.great_cto/personal-skills ]; then git clone --depth=1 https://github.com/avelikiy/ai-agent-skills.git ~/.great_cto/personal-skills 2>/dev/null & elif [ $(($(date +%s) - $(stat -f %m ~/.great_cto/personal-skills/.git 2>/dev/null || stat -c %Y ~/.great_cto/personal-skills/.git 2>/dev/null || echo 0))) -gt 86400 ]; then (cd ~/.great_cto/personal-skills && git pull --quiet 2>/dev/null) & fi; if [ ! -f ~/.great_cto/skills-registry.json ] || [ $(($(date +%s) - $(stat -f %m ~/.great_cto/skills-registry.json 2>/dev/null || stat -c %Y ~/.great_cto/skills-registry.json 2>/dev/null || echo 0))) -gt 86400 ]; then [ -x \"${PLUGIN_DIR}/scripts/skill-discover.sh\" ] && bash \"${PLUGIN_DIR}/scripts/skill-discover.sh\" >/dev/null 2>&1 & fi; [ -x \"${PLUGIN_DIR}/scripts/hooks/welcome.sh\" ] && bash \"${PLUGIN_DIR}/scripts/hooks/welcome.sh\" 2>/dev/null || true; bash \"${PLUGIN_DIR}/scripts/apply-model-override.sh\" 2>/dev/null || true; bd prime 2>/dev/null || true; cat \"${PLUGIN_DIR}/scripts/hooks/operating-rules.md\" 2>/dev/null || true; echo '=== PREFERENCES ==='; node \"${PLUGIN_DIR}/scripts/hooks/read-global-memory.mjs\" 2>&1 || true; echo '=== PROJECT ==='; cat .great_cto/PROJECT.md 2>/dev/null || echo 'GREAT_CTO: No PROJECT.md \u2014 run /start'; PHASE=$(grep '^phase:' .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}' || echo implementation); echo '=== PHASE ==='; echo \"$PHASE\"; { [ \"$PHASE\" != review ] && [ \"$PHASE\" != release ]; } && { echo '=== BRAIN ==='; head -30 .great_cto/brain.md 2>/dev/null; } || true; [ \"$PHASE\" = implementation ] && { echo '=== CODEBASE ==='; head -20 .great_cto/CODEBASE.md 2>/dev/null; } || true; echo '=== LOCAL ==='; cat .great_cto/local.md 2>/dev/null || true; [ \"$PHASE\" != planning ] && { echo '=== HANDOFF ==='; cat .great_cto/HANDOFF.md 2>/dev/null || echo 'No handoff state \u2014 fresh session'; } || true; [ \"$PHASE\" = review ] && { echo '=== LATEST QA ==='; ls docs/qa-reports/QA-*.md 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | xargs head -20 2>/dev/null; echo '=== LATEST CSO ==='; ls docs/security/CSO-*.md 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | xargs head -20 2>/dev/null; } || true; [ \"$PHASE\" = release ] && { echo '=== PERF BASELINE ==='; tail -10 .great_cto/perf-baseline.log 2>/dev/null; } || true; echo '=== STATUS ==='; OPEN_GATES=$(bd list --label gate --status open 2>/dev/null | wc -l | tr -d ' '); OPEN_TASKS=$(bd list --status open 2>/dev/null | wc -l | tr -d ' '); BRANCH=$(git branch --show-current 2>/dev/null || echo '?'); LAST_AGENT=$(ls -t .great_cto/verdicts/*.log 2>/dev/null | head -1 | xargs tail -1 2>/dev/null | awk '{print $2}' || echo 'none'); printf 'branch=%s | gates=%s | tasks=%s | last_agent=%s\\n' \"$BRANCH\" \"${OPEN_GATES:-0}\" \"${OPEN_TASKS:-0}\" \"${LAST_AGENT:-none}\"; [ \"${OPEN_GATES:-0}\" -gt 0 ] && echo '\u2192 run /inbox to see pending decisions' || true; P0_OPEN=$(bd list --status open 2>/dev/null | grep -c 'P0'); P0_OPEN=${P0_OPEN:-0}; if [ \"$P0_OPEN\" -gt 0 ]; then echo ''; printf '\\033[41;97m \ud83d\udea8 P0 OPEN: %s \u2014 run /inbox NOW \\033[0m\\n' \"$P0_OPEN\"; bd list --status open 2>/dev/null | grep 'P0' | head -3 | sed 's/^/ /'; fi; NOW_EPOCH=$(date +%s); LAST_AUDIT_FILE=$(find docs/audit -maxdepth 1 -name 'AUDIT-*.md' 2>/dev/null | sort | tail -1); if [ -n \"$LAST_AUDIT_FILE\" ]; then AUD_M=$(stat -f %m \"$LAST_AUDIT_FILE\" 2>/dev/null || stat -c %Y \"$LAST_AUDIT_FILE\" 2>/dev/null || echo $NOW_EPOCH); AUD_AGE=$(( (NOW_EPOCH - AUD_M) / 86400 )); [ \"$AUD_AGE\" -gt 30 ] && echo \"\u26a0 audit $AUD_AGE days old \u2014 run /audit\"; else echo '\u26a0 no audit artefact \u2014 run /audit or /doctor'; fi; if [ -f .great_cto/digest-latest.md ]; then DIG_M=$(stat -f %m .great_cto/digest-latest.md 2>/dev/null || stat -c %Y .great_cto/digest-latest.md 2>/dev/null || echo $NOW_EPOCH); DIG_AGE=$(( (NOW_EPOCH - DIG_M) / 86400 )); [ \"$DIG_AGE\" -gt 8 ] && echo \"\u26a0 digest $DIG_AGE days old \u2014 run /digest 7\"; fi; [ \"$PHASE\" != implementation ] && cat .great_cto/digest-latest.md 2>/dev/null | head -5 || true; [ -f \"${PLUGIN_DIR}/scripts/hooks/auto-attach-reviewers.mjs\" ] && node \"${PLUGIN_DIR}/scripts/hooks/auto-attach-reviewers.mjs\" 2>/dev/null; [ -n \"$PLUGIN_DIR\" ] && [ -f .great_cto/PROJECT.md ] && [ ! -f .great_cto/CODEBASE.md ] && node \"${PLUGIN_DIR}/scripts/first-run-codebase.mjs\" >/dev/null 2>&1 & node \"${PLUGIN_DIR}/scripts/hooks/quota-check.mjs\" 2>/dev/null || true; echo '=== PATTERNS ==='; GP_DIR=\"$HOME/.great_cto/global-patterns\"; ARCH=$(grep '^primary:' .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}' | head -1); if [ -d \"$GP_DIR\" ] && ls \"$GP_DIR\"/GP-*.md >/dev/null 2>&1; then ACTIVE_COUNT=$(grep -rl 'status: active' \"$GP_DIR\" 2>/dev/null | wc -l | tr -d ' '); echo \"Active patterns: ${ACTIVE_COUNT:-0}\"; if [ -n \"$ARCH\" ] && [ \"${ACTIVE_COUNT:-0}\" -gt 0 ]; then grep -rl 'status: active' \"$GP_DIR\" 2>/dev/null | xargs grep -lE \"applies_to:.*$ARCH\" 2>/dev/null | sort -V | tail -5 | while read f; do SLUG=$(basename \"$f\" .md); SYMPT=$(grep -m1 '^symptom:' \"$f\" 2>/dev/null | sed 's/symptom: //'); DETECT=$(grep -A 2 '^detection_order:' \"$f\" 2>/dev/null | grep '^ - ' | head -1 | sed 's/^ - //'); HITS=$(grep -m1 '^hits:' \"$f\" 2>/dev/null | awk '{print $2}'); printf ' %s (hits=%s): %s\\n \\u2192 Check first: %s\\n' \"$SLUG\" \"${HITS:-0}\" \"$SYMPT\" \"$DETECT\"; done || echo \" no patterns match archetype=$ARCH yet \u2014 /crystallize builds the library\"; fi; else echo 'no global patterns yet \u2014 run /crystallize after first incident'; fi; [ -x \"${PLUGIN_DIR}/scripts/check-update.sh\" ] && bash \"${PLUGIN_DIR}/scripts/check-update.sh\" \"${PLUGIN_DIR}/.claude-plugin/plugin.json\" 2>/dev/null || true ; # great_cto-cache-cleanup: keep the newest 3 cached versions, and never one a live session runs from (prune-versions; the check install-local --prune got after 2026-09-11). for CR in \"$HOME\"/.claude/plugins/cache/*/great_cto; do [ -d \"$CR\" ] || continue; node \"${PLUGIN_DIR}/scripts/lib/prune-versions.mjs\" --cache-root \"$CR\" --keep \"${PLUGIN_DIR:-$CR/none}\" --keep-newest 3 2>/dev/null | while IFS= read -r d; do case \"$d\" in \"$CR\"/*) rm -rf \"$d\" ;; esac; done; done 2>/dev/null || true",
63
+ "command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; export PATH=\"/opt/homebrew/bin:$HOME/.local/bin:/usr/local/bin:$PATH\"; PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed 's|/$||')}; if [ -z \"$PLUGIN_DIR\" ]; then echo 'GREAT_CTO: plugin dir not found \u2014 run /update'; else mkdir -p ~/.great_cto; node \"${PLUGIN_DIR}/scripts/lib/sync-managed.mjs\" --plugin-dir \"${PLUGIN_DIR}\" 2>/dev/null || true; if [ -f .great_cto/PROJECT.md ]; then cp \"${PLUGIN_DIR}/skills/great_cto/ARCHETYPES.md\" .great_cto/ARCHETYPES.md 2>/dev/null || true; cp \"${PLUGIN_DIR}/skills/great_cto/SKILL.md\" .great_cto/SKILL.md 2>/dev/null || true; { printf \"export PATH=/opt/homebrew/bin:$HOME/.local/bin:/usr/local/bin:$PATH\\nexport ARCHETYPES_MD=.great_cto/ARCHETYPES.md\\n\"; } > .great_cto/env.sh; fi; fi; if [ ! -d ~/.great_cto/catalog ]; then git clone --depth=1 https://github.com/davila7/claude-code-templates.git ~/.great_cto/catalog 2>/dev/null & fi; if [ ! -d ~/.great_cto/anthropic-skills ]; then git clone --depth=1 https://github.com/anthropics/skills.git ~/.great_cto/anthropic-skills 2>/dev/null & elif [ $(($(date +%s) - $(stat -f %m ~/.great_cto/anthropic-skills/.git 2>/dev/null || stat -c %Y ~/.great_cto/anthropic-skills/.git 2>/dev/null || echo 0))) -gt 604800 ]; then (cd ~/.great_cto/anthropic-skills && git pull --quiet 2>/dev/null) & fi; if [ ! -d ~/.great_cto/personal-skills ]; then git clone --depth=1 https://github.com/avelikiy/ai-agent-skills.git ~/.great_cto/personal-skills 2>/dev/null & elif [ $(($(date +%s) - $(stat -f %m ~/.great_cto/personal-skills/.git 2>/dev/null || stat -c %Y ~/.great_cto/personal-skills/.git 2>/dev/null || echo 0))) -gt 86400 ]; then (cd ~/.great_cto/personal-skills && git pull --quiet 2>/dev/null) & fi; if [ ! -f ~/.great_cto/skills-registry.json ] || [ $(($(date +%s) - $(stat -f %m ~/.great_cto/skills-registry.json 2>/dev/null || stat -c %Y ~/.great_cto/skills-registry.json 2>/dev/null || echo 0))) -gt 86400 ]; then [ -x \"${PLUGIN_DIR}/scripts/skill-discover.sh\" ] && bash \"${PLUGIN_DIR}/scripts/skill-discover.sh\" >/dev/null 2>&1 & fi; [ -x \"${PLUGIN_DIR}/scripts/hooks/welcome.sh\" ] && bash \"${PLUGIN_DIR}/scripts/hooks/welcome.sh\" 2>/dev/null || true; bash \"${PLUGIN_DIR}/scripts/apply-model-override.sh\" 2>/dev/null || true; bd prime 2>/dev/null || true; cat \"${PLUGIN_DIR}/scripts/hooks/operating-rules.md\" 2>/dev/null || true; echo '=== PREFERENCES ==='; node \"${PLUGIN_DIR}/scripts/hooks/read-global-memory.mjs\" 2>&1 || true; echo '=== PROJECT ==='; cat .great_cto/PROJECT.md 2>/dev/null || echo 'GREAT_CTO: No PROJECT.md \u2014 run /start'; PHASE=$(grep '^phase:' .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}' || echo implementation); echo '=== PHASE ==='; echo \"$PHASE\"; { [ \"$PHASE\" != review ] && [ \"$PHASE\" != release ]; } && { echo '=== BRAIN ==='; head -30 .great_cto/brain.md 2>/dev/null; } || true; [ \"$PHASE\" = implementation ] && { echo '=== CODEBASE ==='; head -20 .great_cto/CODEBASE.md 2>/dev/null; } || true; echo '=== LOCAL ==='; cat .great_cto/local.md 2>/dev/null || true; [ \"$PHASE\" != planning ] && { echo '=== HANDOFF ==='; cat .great_cto/HANDOFF.md 2>/dev/null || echo 'No handoff state \u2014 fresh session'; } || true; [ \"$PHASE\" = review ] && { echo '=== LATEST QA ==='; ls docs/qa-reports/QA-*.md 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | xargs head -20 2>/dev/null; echo '=== LATEST CSO ==='; ls docs/security/CSO-*.md 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | xargs head -20 2>/dev/null; } || true; [ \"$PHASE\" = release ] && { echo '=== PERF BASELINE ==='; tail -10 .great_cto/perf-baseline.log 2>/dev/null; } || true; echo '=== STATUS ==='; OPEN_GATES=$(bd list --label gate --status open 2>/dev/null | wc -l | tr -d ' '); OPEN_TASKS=$(bd list --status open 2>/dev/null | wc -l | tr -d ' '); BRANCH=$(git branch --show-current 2>/dev/null || echo '?'); LAST_AGENT=$(ls -t .great_cto/verdicts/*.log 2>/dev/null | head -1 | xargs tail -1 2>/dev/null | awk '{print $2}' || echo 'none'); printf 'branch=%s | gates=%s | tasks=%s | last_agent=%s\\n' \"$BRANCH\" \"${OPEN_GATES:-0}\" \"${OPEN_TASKS:-0}\" \"${LAST_AGENT:-none}\"; [ \"${OPEN_GATES:-0}\" -gt 0 ] && echo '\u2192 run /inbox to see pending decisions' || true; P0_OPEN=$(bd list --status open 2>/dev/null | grep -c 'P0'); P0_OPEN=${P0_OPEN:-0}; if [ \"$P0_OPEN\" -gt 0 ]; then echo ''; printf '\\033[41;97m \ud83d\udea8 P0 OPEN: %s \u2014 run /inbox NOW \\033[0m\\n' \"$P0_OPEN\"; bd list --status open 2>/dev/null | grep 'P0' | head -3 | sed 's/^/ /'; fi; NOW_EPOCH=$(date +%s); LAST_AUDIT_FILE=$(find docs/audit -maxdepth 1 -name 'AUDIT-*.md' 2>/dev/null | sort | tail -1); if [ -n \"$LAST_AUDIT_FILE\" ]; then AUD_M=$(stat -f %m \"$LAST_AUDIT_FILE\" 2>/dev/null || stat -c %Y \"$LAST_AUDIT_FILE\" 2>/dev/null || echo $NOW_EPOCH); AUD_AGE=$(( (NOW_EPOCH - AUD_M) / 86400 )); [ \"$AUD_AGE\" -gt 30 ] && echo \"\u26a0 audit $AUD_AGE days old \u2014 run /audit\"; else echo '\u26a0 no audit artefact \u2014 run /audit or /doctor'; fi; if [ -f .great_cto/digest-latest.md ]; then DIG_M=$(stat -f %m .great_cto/digest-latest.md 2>/dev/null || stat -c %Y .great_cto/digest-latest.md 2>/dev/null || echo $NOW_EPOCH); DIG_AGE=$(( (NOW_EPOCH - DIG_M) / 86400 )); [ \"$DIG_AGE\" -gt 8 ] && echo \"\u26a0 digest $DIG_AGE days old \u2014 run /digest 7\"; fi; [ \"$PHASE\" != implementation ] && cat .great_cto/digest-latest.md 2>/dev/null | head -5 || true; [ -f \"${PLUGIN_DIR}/scripts/hooks/auto-attach-reviewers.mjs\" ] && node \"${PLUGIN_DIR}/scripts/hooks/auto-attach-reviewers.mjs\" 2>/dev/null; [ -n \"$PLUGIN_DIR\" ] && [ -f .great_cto/PROJECT.md ] && [ ! -f .great_cto/CODEBASE.md ] && node \"${PLUGIN_DIR}/scripts/first-run-codebase.mjs\" >/dev/null 2>&1 & node \"${PLUGIN_DIR}/scripts/hooks/quota-check.mjs\" 2>/dev/null || true; echo '=== PATTERNS ==='; GP_DIR=\"$HOME/.great_cto/global-patterns\"; ARCH=$(grep '^primary:' .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}' | head -1); if [ -d \"$GP_DIR\" ] && ls \"$GP_DIR\"/GP-*.md >/dev/null 2>&1; then ACTIVE_COUNT=$(grep -rl 'status: active' \"$GP_DIR\" 2>/dev/null | wc -l | tr -d ' '); echo \"Active patterns: ${ACTIVE_COUNT:-0}\"; if [ -n \"$ARCH\" ] && [ \"${ACTIVE_COUNT:-0}\" -gt 0 ]; then grep -rl 'status: active' \"$GP_DIR\" 2>/dev/null | xargs grep -lE \"applies_to:.*$ARCH\" 2>/dev/null | sort -V | tail -5 | while read f; do SLUG=$(basename \"$f\" .md); SYMPT=$(grep -m1 '^symptom:' \"$f\" 2>/dev/null | sed 's/symptom: //'); DETECT=$(grep -A 2 '^detection_order:' \"$f\" 2>/dev/null | grep '^ - ' | head -1 | sed 's/^ - //'); HITS=$(grep -m1 '^hits:' \"$f\" 2>/dev/null | awk '{print $2}'); printf ' %s (hits=%s): %s\\n \\u2192 Check first: %s\\n' \"$SLUG\" \"${HITS:-0}\" \"$SYMPT\" \"$DETECT\"; done || echo \" no patterns match archetype=$ARCH yet \u2014 /crystallize builds the library\"; fi; else echo 'no global patterns yet \u2014 run /crystallize after first incident'; fi; [ -x \"${PLUGIN_DIR}/scripts/check-update.sh\" ] && bash \"${PLUGIN_DIR}/scripts/check-update.sh\" \"${PLUGIN_DIR}/.claude-plugin/plugin.json\" 2>/dev/null || true ; # great_cto-cache-cleanup: keep the newest 3 cached versions, and never one a live session runs from (prune-versions; the check install-local --prune got after 2026-09-11). for CR in \"$HOME\"/.claude/plugins/cache/*/great_cto; do [ -d \"$CR\" ] || continue; node \"${PLUGIN_DIR}/scripts/lib/prune-versions.mjs\" --cache-root \"$CR\" --keep \"${PLUGIN_DIR:-$CR/none}\" --keep-newest 3 2>/dev/null | while IFS= read -r d; do case \"$d\" in \"$CR\"/*) rm -rf \"$d\" ;; esac; done; done 2>/dev/null || true",
64
64
  "timeout": 30,
65
65
  "statusMessage": "Loading Great CTO context..."
66
66
  },
@@ -24,7 +24,7 @@
24
24
  // CLI (for hooks written in shell):
25
25
  // node scripts/lib/agent-events.mjs --emit <kind> [--tool <name>] [--agent <name>]
26
26
 
27
- import { isGlobalLayer } from './great-cto-scope.mjs';
27
+ import { isGlobalLayer, isProjectState } from './great-cto-scope.mjs';
28
28
  import { appendFileSync, closeSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, statSync } from 'node:fs';
29
29
  import { join } from 'node:path';
30
30
 
@@ -32,7 +32,7 @@ export const EVENTS_FILE = 'events.jsonl';
32
32
  // `hint`: a context hook (edit-impact, lesson-tripwire) put text in front of the model.
33
33
  // Recorded so its noise can be measured — how often, on what, how long — not what it said.
34
34
  export const EVENT_KINDS = Object.freeze(['agent-start', 'agent-stop', 'tool', 'denied', 'stop', 'pipeline', 'hint']);
35
- export const EVENT_FIELDS = Object.freeze(['v', 'ts', 'kind', 'session', 'agent', 'tool', 'paths', 'ok', 'duration_ms', 'outcome', 'verdict', 'hook', 'chars', 'host']);
35
+ export const EVENT_FIELDS = Object.freeze(['v', 'ts', 'kind', 'session', 'agent', 'tool', 'paths', 'ok', 'duration_ms', 'outcome', 'verdict', 'hook', 'chars', 'host', 'invocation_id']);
36
36
  export const MAX_BYTES = 5 * 1024 * 1024;
37
37
 
38
38
  const MAX_PATHS = 10;
@@ -49,6 +49,7 @@ export function makeEvent(input, { now = Date.now() } = {}) {
49
49
  const e = { v: 1, ts: iso(now), kind: input.kind };
50
50
  const session = short(input.session, 80); if (session) e.session = session;
51
51
  const agent = short(input.agent, 80); if (agent) e.agent = agent;
52
+ if (typeof input.invocation_id === 'string' && /^[a-f0-9]{64}$/.test(input.invocation_id)) e.invocation_id = input.invocation_id;
52
53
  const tool = short(input.tool, 80); if (tool) e.tool = tool;
53
54
  if (Array.isArray(input.paths)) {
54
55
  const paths = input.paths.filter((p) => typeof p === 'string' && p.trim()).map((p) => p.slice(0, 300)).slice(0, MAX_PATHS);
@@ -70,11 +71,16 @@ export function makeEvent(input, { now = Date.now() } = {}) {
70
71
  * @param {string} dir the project's .great_cto directory
71
72
  * @returns {{ok:true} | {ok:false, why:string}}
72
73
  */
73
- export function appendEvent(dir, input, { now = Date.now(), env = process.env, maxBytes = MAX_BYTES } = {}) {
74
+ export function appendEvent(dir, input, { now = Date.now(), env = process.env, maxBytes = MAX_BYTES, explicit = false } = {}) {
74
75
  try {
75
76
  if (env.GREAT_CTO_DISABLE_EVENTS === '1') return { ok: false, why: 'disabled by GREAT_CTO_DISABLE_EVENTS=1' };
76
77
  // ~/.great_cto is every project's global layer, never one project's log.
77
78
  if (isGlobalLayer(dir)) return { ok: false, why: 'the global layer is not a project' };
79
+ // Hooks fire in every repository a session opens, great_cto project or not;
80
+ // creating .great_cto there wrote into someone else's tree (great_cto-tng1).
81
+ // So a hook's event needs a project (.great_cto/PROJECT.md or verdicts/).
82
+ // `explicit` is a command the user ran in this repository (great-cto run).
83
+ if (!explicit && !isProjectState(dir)) return { ok: false, why: 'not a great_cto project' };
78
84
  const e = makeEvent(input, { now });
79
85
  if (!e) return { ok: false, why: `not an event kind: ${JSON.stringify(input?.kind)}` };
80
86
  const line = JSON.stringify(e) + '\n';
@@ -23,8 +23,9 @@ export function projectCodexState(state) {
23
23
  controllerDispatch: dispatchEvidenceSummary(state),
24
24
  wave: state.wave ? { id: state.wave.id, roles: state.wave.roles, hosts: state.wave.hosts, status: state.wave.status } : null,
25
25
  rolesCompleted: Object.keys(state.results || {}),
26
- attempts: (state.attempts || []).map(({ id, role, host, number, phase, status, startedAt, finishedAt }) =>
27
- ({ id, role, host: host || 'codex', number, phase, status, startedAt, finishedAt })),
26
+ attempts: (state.attempts || []).map(({ id, role, host, number, phase, status, startedAt, finishedAt, invocationId }) =>
27
+ ({ id, role, host: host || 'codex', number, phase, status, startedAt, finishedAt,
28
+ ...(invocationId ? { invocationId } : {}) })),
28
29
  pending: state.pending ? { role: state.pending.role, gates: state.pending.gates || [], createdAt: state.pending.createdAt } : null,
29
30
  release: state.release ? {
30
31
  adapter: state.release.adapter,
@@ -18,6 +18,7 @@ import { readExecutionBudget, withAgentBudget, requireAgents, releaseAgent, budg
18
18
  import { validateSpecialistPolicy, assertSpecialistEpoch, schedulePreparation, scheduleSpecialists, specialistRole, validateReviewFiles, recordReviewFiles } from './controlled-specialists.mjs';
19
19
  import { scopedReviewInput, scopedReviewCandidate, attestScopedReview, completeScopeAttestation } from './scoped-review-reuse.mjs';
20
20
  import { observeControllerCall } from './controller-dispatch-evidence.mjs';
21
+ import { invocationIdentity } from './invocation-identity.mjs';
21
22
 
22
23
  export const PLUGIN_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
23
24
  const hash = text => createHash('sha256').update(text).digest('hex');
@@ -68,11 +69,12 @@ function cleanResponse(response) {
68
69
  * is excluded from receipts, so these writes cannot trip the tree checks below.
69
70
  */
70
71
  function emit(state, event) {
71
- return appendEvent(join(state.root, '.great_cto'), { ...event, session: state.id });
72
+ // The user ran this pipeline here, so it keeps its log here even before /start.
73
+ return appendEvent(join(state.root, '.great_cto'), { ...event, session: state.id }, { explicit: true });
72
74
  }
73
- const toolListener = (state, agent) => (ev) => {
75
+ const toolListener = (state, agent, invocationId = null) => (ev) => {
74
76
  const e = codexToolEvent(ev);
75
- if (e) emit(state, { ...e, agent });
77
+ if (e) emit(state, { ...e, agent, invocation_id: invocationId });
76
78
  };
77
79
 
78
80
  /**
@@ -171,16 +173,18 @@ function releaseSummary(state) {
171
173
 
172
174
  export async function verifyStage(state, role, proposal, execute) {
173
175
  const agent = 'codex-verifier';
176
+ const invocationId = invocationIdentity({ session_id: state.id,
177
+ agent_id: `${state.attempts?.findLast(a => a.role === role)?.id || randomUUID()}:verifier` });
174
178
  const t0 = Date.now();
175
179
  let ok = false;
176
- emit(state, { kind: 'agent-start', agent });
180
+ emit(state, { kind: 'agent-start', agent, invocation_id: invocationId });
177
181
  const wave = waveEvidence(state);
178
182
  const attestWorkflow = wave?.roles.includes(role);
179
183
  try {
180
184
  const result = cleanResponse(await execute({
181
185
  cwd: state.root, sandbox: 'read-only', ephemeral: true, extraArgs: workerArgs,
182
186
  bin: process.env.GREAT_CTO_CODEX_BIN || 'codex', timeoutMs: 300000,
183
- onEvent: toolListener(state, agent),
187
+ invocationId, onEvent: toolListener(state, agent, invocationId),
184
188
  prompt: `You are an independent verifier for the ${role} stage. Read the ACTUAL files and assess whether they satisfy the task for this stage.\n` +
185
189
  frozenPolicyContext(state) +
186
190
  (state.specialistStages?.[role] ? `Controlled phase: ${state.specialistStages[role].phase}, before implementation. Verify an implementable contract/threat model and its evidence; do not require nonexistent implementation or treat design sign-off as code approval. Capability: ${codexRoleProfile(specialistRole(state, role))}\n` : '') +
@@ -224,7 +228,7 @@ export async function verifyStage(state, role, proposal, execute) {
224
228
  ok = true;
225
229
  return result;
226
230
  } finally {
227
- emit(state, { kind: 'agent-stop', agent, ok, duration_ms: Date.now() - t0 });
231
+ emit(state, { kind: 'agent-stop', agent, invocation_id: invocationId, ok, duration_ms: Date.now() - t0 });
228
232
  }
229
233
  }
230
234
 
@@ -629,11 +633,12 @@ export async function runStage(state, { execute = null, runners = { codex: runCo
629
633
  const head = workerHead(state, role);
630
634
  // Additive v1 fields: old runs retain their original single-attempt policy.
631
635
  state.attempts ??= [];
632
- const attempt = { id: randomUUID(), role, number: state.attempts.filter(a => a.role === role).length + 1,
636
+ const attempt = { id: prepared?.attemptId || randomUUID(), role, number: state.attempts.filter(a => a.role === role).length + 1,
633
637
  host: roleHost(state, role), status: 'running', phase: 'worker', startedAt: new Date().toISOString(),
634
638
  inputReceipt: stageReceipt(state, prepared ? prepared.receipt : treeReceipt(state.root), 'stage input') };
635
639
  if (prepared?.callId) attempt.workerCallId = prepared.callId;
636
640
  if (attempt.number > (state.maxAttempts ?? 1)) throw Error('stage attempt limit reached');
641
+ attempt.invocationId = invocationIdentity({ session_id: state.id, agent_id: attempt.id });
637
642
  state.attempts.push(attempt);
638
643
  state.active = role; state.steps++; save(state);
639
644
  // Recorded only once the stage is really dispatched: a run awaiting a gate, or
@@ -692,10 +697,10 @@ export async function runStage(state, { execute = null, runners = { codex: runCo
692
697
  }
693
698
  if (!prepared && typeof runner !== 'function') throw Error(`no runner for ${roleHost(state, role)}`);
694
699
  const response = prepared ? prepared.response : reused || await withAgentBudget(state, { callId: `${attempt.id}:worker`, host: roleHost(state, role), role }, async () => {
695
- stageStarted = true; emit(state, { kind: 'agent-start', agent });
700
+ stageStarted = true; emit(state, { kind: 'agent-start', agent, invocation_id: attempt.invocationId });
696
701
  return observeControllerCall(state, { id: `${attempt.id}:worker`, host: roleHost(state, role), role, kind: 'worker' }, () => runner({ prompt, cwd: state.root, sandbox: 'read-only', ephemeral: true,
697
702
  bin: roleHost(state, role) === 'codex' ? process.env.GREAT_CTO_CODEX_BIN || 'codex' : process.env.GREAT_CTO_CLAUDE_BIN || 'claude',
698
- timeoutMs: 300000, extraArgs: roleHost(state, role) === 'codex' ? workerArgs : [], onEvent: toolListener(state, agent) }), save);
703
+ invocationId: attempt.invocationId, timeoutMs: 300000, extraArgs: roleHost(state, role) === 'codex' ? workerArgs : [], onEvent: toolListener(state, agent, attempt.invocationId) }), save);
699
704
  });
700
705
  // Codex can recover its session-index lookup without degrading the worker.
701
706
  // Keep the diagnostic in the receipt; every other warning/error blocks.
@@ -806,7 +811,7 @@ export async function runStage(state, { execute = null, runners = { codex: runCo
806
811
  // re-reviews the candidate. Human gates and repair limits remain unchanged.
807
812
  if (state.rework?.role === role) state.rework = null;
808
813
  state.results[role] = { verdict: proposal.verdict, summary: proposal.summary, meta: proposal.meta || {},
809
- attemptId: attempt.id, host: roleHost(state, role), checks: attempt.checks ?? null, receipt,
814
+ attemptId: attempt.id, invocationId: attempt.invocationId, host: roleHost(state, role), checks: attempt.checks ?? null, receipt,
810
815
  receiptEvidence: receipt ? 'git-tree' : 'report-artifacts-only', verification,
811
816
  digest: hash(JSON.stringify({ attemptId: attempt.id, proposal })), usage: response.usage ?? null,
812
817
  diagnostics: response.errors || [], at: new Date().toISOString() };
@@ -825,7 +830,7 @@ export async function runStage(state, { execute = null, runners = { codex: runCo
825
830
  // Keep active set: a partial write or interrupted process must not be replayed.
826
831
  save(state);
827
832
  } finally {
828
- if (stageStarted) emit(state, { kind: 'agent-stop', agent, ok: stageOk, duration_ms: Date.now() - t0 });
833
+ if (stageStarted) emit(state, { kind: 'agent-stop', agent, invocation_id: attempt.invocationId, ok: stageOk, duration_ms: Date.now() - t0 });
829
834
  // ADR-023: a Codex stage is a turn. Recorded after every tree check above, and it
830
835
  // writes only git objects and a ref, never a working file. Never throws.
831
836
  if (snapshotTurn(state.root, { session: state.id }).state === 'recorded') pruneTurns(state.root, { session: state.id, keep: 50 });
@@ -893,13 +898,15 @@ export async function runParallelWave(state, { runners = { codex: runCodexExec,
893
898
  const waveId = randomUUID();
894
899
  const leases = requireAgents(state.executionBudget, roles.map(role => ({ callId: `${waveId}:${role}`, host: roleHost(state, role), role, depth: 1 })));
895
900
  state.wave = { id: waveId, roles, status: 'running', receipt,
901
+ attemptIds: Object.fromEntries(roles.map(role => [role, randomUUID()])),
896
902
  hosts: Object.fromEntries(roles.map(role => [role, roleHost(state, role)])), startedAt: new Date().toISOString() };
897
903
  const context = inlineContext(state);
898
904
  state.wave.context = context;
899
905
  try { save(state); } catch (error) { for (const lease of leases) releaseAgent(state.executionBudget, lease); throw error; }
900
906
  const calls = roles.map(async (role, index) => {
901
907
  const agent = hostAgent(state, role), started = Date.now();
902
- emit(state, { kind: 'agent-start', agent });
908
+ const invocationId = invocationIdentity({ session_id: state.id, agent_id: state.wave.attemptIds[role] });
909
+ emit(state, { kind: 'agent-start', agent, invocation_id: invocationId });
903
910
  let ok = false;
904
911
  try {
905
912
  const runner = hostRunner(state, role, runners);
@@ -907,12 +914,12 @@ export async function runParallelWave(state, { runners = { codex: runCodexExec,
907
914
  const result = await observeControllerCall(state, { id: `${waveId}:${role}`, host: roleHost(state, role), role, kind: 'worker' }, () => runner({ prompt: workerHead(state, role) + context.text, cwd: state.root,
908
915
  sandbox: 'read-only', ephemeral: true, timeoutMs: 300000,
909
916
  bin: roleHost(state, role) === 'codex' ? process.env.GREAT_CTO_CODEX_BIN || 'codex' : process.env.GREAT_CTO_CLAUDE_BIN || 'claude',
910
- extraArgs: roleHost(state, role) === 'codex' ? workerArgs : [], onEvent: toolListener(state, agent) }), save);
917
+ invocationId, extraArgs: roleHost(state, role) === 'codex' ? workerArgs : [], onEvent: toolListener(state, agent, invocationId) }), save);
911
918
  ok = true;
912
919
  return result;
913
920
  } finally {
914
921
  releaseAgent(state.executionBudget, leases[index]);
915
- emit(state, { kind: 'agent-stop', agent, ok, duration_ms: Date.now() - started });
922
+ emit(state, { kind: 'agent-stop', agent, invocation_id: invocationId, ok, duration_ms: Date.now() - started });
916
923
  }
917
924
  });
918
925
  const settled = await Promise.allSettled(calls);
@@ -956,6 +963,7 @@ export async function runParallelWave(state, { runners = { codex: runCodexExec,
956
963
  save(state); return state;
957
964
  }
958
965
  await runStage(state, { prepared: { response: state.wave.responses[role], receipt: state.wave.receipt,
966
+ attemptId: state.wave.attemptIds?.[role],
959
967
  context: state.wave.context, callId: `${state.wave.id}:${role}` }, runners, verify, checks, save, contextStore });
960
968
  if (!state.results[role]) { state.wave.status = 'discarded'; break; } // Other snapshot is invalid.
961
969
  }
@@ -14,9 +14,9 @@
14
14
  * other plugins' agents (`feature-dev:code-reviewer`) record no verdict and
15
15
  * were being asked for one.
16
16
  */
17
- import { existsSync, readdirSync } from 'node:fs';
17
+ import { existsSync, readdirSync, mkdirSync } from 'node:fs';
18
18
  import { homedir } from 'node:os';
19
- import { dirname, join, resolve } from 'node:path';
19
+ import { dirname, join, resolve, sep } from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
21
 
22
22
  export function globalLayerDir(home = homedir()) {
@@ -38,6 +38,24 @@ export function isProjectState(dir, home = homedir()) {
38
38
  return !isGlobalLayer(d, home) && (existsSync(join(d, 'PROJECT.md')) || existsSync(join(d, 'verdicts')));
39
39
  }
40
40
 
41
+ /**
42
+ * Create a directory under a project's `.great_cto/`, and only there. Returns
43
+ * false — creating nothing — when the `.great_cto` it lives in is not a project
44
+ * (no PROJECT.md, no verdicts/), or is the global layer. Hooks fire in every
45
+ * repository a session opens; a plain mkdir wrote great_cto state into
46
+ * repositories that were not great_cto projects (great_cto-tng1).
47
+ */
48
+ export function ensureStateDir(dir, home = homedir()) {
49
+ const abs = resolve(String(dir || ''));
50
+ const parts = abs.split(sep);
51
+ const i = parts.lastIndexOf('.great_cto');
52
+ if (i < 0) return false;
53
+ const gc = parts.slice(0, i + 1).join(sep) || sep;
54
+ if (!isProjectState(gc, home)) return false;
55
+ mkdirSync(abs, { recursive: true });
56
+ return true;
57
+ }
58
+
41
59
  /** great_cto's agents, by name: the plugin's own agents/ directory (or GREAT_CTO_AGENTS_DIR). */
42
60
  export function roster(dir = process.env.GREAT_CTO_AGENTS_DIR || resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'agents')) {
43
61
  try { return new Set(readdirSync(dir).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3))); } catch { return new Set(); }
@@ -0,0 +1,10 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /** Host-provided identity, never inferred from a shared role log or session transcript. */
4
+ export function invocationIdentity(payload) {
5
+ const valid = (v) => typeof v === 'string' && /^[A-Za-z0-9_.:-]{1,160}$/.test(v);
6
+ if (!valid(payload?.agent_id) || !valid(payload?.session_id)) return null;
7
+ // Tuple encoding avoids ambiguous delimiters; a bounded digest fits marker
8
+ // filenames even when the provider supplies long identifiers.
9
+ return createHash('sha256').update(JSON.stringify([payload.session_id, payload.agent_id])).digest('hex');
10
+ }
@@ -21,6 +21,7 @@
21
21
  * moved on and is reported as working. A `working` with a silent transcript for
22
22
  * five minutes is `unknown`, not working (agtx uses the same bound).
23
23
  */
24
+ import { isProjectState } from './great-cto-scope.mjs';
24
25
  import { mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, rmSync, renameSync } from 'node:fs';
25
26
  import { join } from 'node:path';
26
27
  import { observeWorkSession } from './work-tasks.mjs';
@@ -47,7 +48,9 @@ export function recordHookEvent(payload, { now = Date.now() } = {}) {
47
48
  if (event === 'SessionEnd') { rmSync(file, { force: true }); return { removed: true }; }
48
49
  const state = EVENT_STATE[event];
49
50
  if (!state) return null;
50
- try { statSync(join(cwd, '.great_cto')); } catch { return null; } // not a great_cto project
51
+ // A project, not merely a `.great_cto/` directory: an older SessionStart made
52
+ // one in any repository it ran in, and status then landed in someone else's tree.
53
+ if (!isProjectState(join(cwd, '.great_cto'))) return null;
51
54
  const rec = {
52
55
  session: id,
53
56
  state,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.63.0",
3
+ "version": "3.63.2",
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",