great-cto 3.30.0 → 3.32.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 +29 -2
- package/board/packages/board/lib/routes.mjs +10 -0
- package/board/packages/board/public/index.html +34 -0
- package/board/scripts/lib/agent-posture.mjs +3 -0
- package/board/scripts/lib/session-status.mjs +91 -0
- package/board/scripts/lib/verdict-record.mjs +6 -0
- 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.32.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
"type": "command",
|
|
64
|
-
"command": "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 | sort -V | tail -1 | 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; 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 | sort -V | tail -1 | xargs head -20 2>/dev/null; echo '=== LATEST CSO ==='; ls docs/security/CSO-*.md 2>/dev/null | sort -V | tail -1 | 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
|
+
"command": "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 | sort -V | tail -1 | 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 | sort -V | tail -1 | xargs head -20 2>/dev/null; echo '=== LATEST CSO ==='; ls docs/security/CSO-*.md 2>/dev/null | sort -V | tail -1 | 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",
|
|
65
65
|
"timeout": 30,
|
|
66
66
|
"statusMessage": "Loading Great CTO context..."
|
|
67
67
|
},
|
|
@@ -251,6 +251,11 @@
|
|
|
251
251
|
"command": "PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | sort -V | tail -1 | sed 's|/$||')}; node \"${PLUGIN_DIR}/scripts/hooks/session-end.mjs\" 2>/dev/null; true",
|
|
252
252
|
"timeout": 8,
|
|
253
253
|
"statusMessage": "Saving session summary..."
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"type": "command",
|
|
257
|
+
"command": "PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | sort -V | tail -1 | sed 's|/$||')}; node \"${PLUGIN_DIR}/scripts/hooks/session-status.mjs\" 2>/dev/null; true",
|
|
258
|
+
"timeout": 5
|
|
254
259
|
}
|
|
255
260
|
]
|
|
256
261
|
}
|
|
@@ -273,6 +278,11 @@
|
|
|
273
278
|
"type": "command",
|
|
274
279
|
"command": "[ -n \"$GREAT_CTO_CLASS_TELEMETRY\" ] && { PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | sort -V | tail -1 | sed 's|/$||')}; node \"${PLUGIN_DIR}/scripts/hooks/classify-telemetry.mjs\" 2>/dev/null; }; true",
|
|
275
280
|
"timeout": 3
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"type": "command",
|
|
284
|
+
"command": "PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | sort -V | tail -1 | sed 's|/$||')}; node \"${PLUGIN_DIR}/scripts/hooks/session-status.mjs\" 2>/dev/null; true",
|
|
285
|
+
"timeout": 5
|
|
276
286
|
}
|
|
277
287
|
]
|
|
278
288
|
}
|
|
@@ -309,6 +319,23 @@
|
|
|
309
319
|
"type": "command",
|
|
310
320
|
"async": true,
|
|
311
321
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/register-project.mjs\" 2>/dev/null || true"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"type": "command",
|
|
325
|
+
"command": "PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | sort -V | tail -1 | sed 's|/$||')}; node \"${PLUGIN_DIR}/scripts/hooks/session-status.mjs\" 2>/dev/null; true",
|
|
326
|
+
"timeout": 5
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
],
|
|
331
|
+
"Notification": [
|
|
332
|
+
{
|
|
333
|
+
"matcher": "",
|
|
334
|
+
"hooks": [
|
|
335
|
+
{
|
|
336
|
+
"type": "command",
|
|
337
|
+
"command": "PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | sort -V | tail -1 | sed 's|/$||')}; node \"${PLUGIN_DIR}/scripts/hooks/session-status.mjs\" 2>/dev/null; true",
|
|
338
|
+
"timeout": 5
|
|
312
339
|
}
|
|
313
340
|
]
|
|
314
341
|
}
|
|
@@ -14,6 +14,7 @@ import { autoRegisterProject, listProjects, resolveProjectCwd, resolveProjectInf
|
|
|
14
14
|
import { readVerdictsWithHealth } from './verdicts.mjs';
|
|
15
15
|
import { agentUsage, usageSnapshot } from '../../../scripts/lib/agent-usage.mjs';
|
|
16
16
|
import { reviewerStatus } from '../../../scripts/lib/required-reviewers.mjs';
|
|
17
|
+
import { readSessionStatus } from '../../../scripts/lib/session-status.mjs';
|
|
17
18
|
import { readScores, summarizeScores } from '../../../scripts/lib/scores.mjs';
|
|
18
19
|
import { status as routerKeyStatus, writeKey as writeRouterKey, verifyKey as verifyRouterKey } from '../../../scripts/lib/router-key.mjs';
|
|
19
20
|
|
|
@@ -1470,6 +1471,15 @@ async function dispatch(req, res, url, cwd) {
|
|
|
1470
1471
|
|
|
1471
1472
|
// Domain reviewers this project's PROJECT.md requires (archetype, packs,
|
|
1472
1473
|
// compliance), each with whether a verdict exists — the list gate:ship refuses on.
|
|
1474
|
+
// Sessions in this project that wait for a person — written by the
|
|
1475
|
+
// Notification / Stop / UserPromptSubmit hooks (scripts/lib/session-status.mjs).
|
|
1476
|
+
if (pathname === '/api/session-status') {
|
|
1477
|
+
const c = url.searchParams.get('project') ? resolveProjectCwd(url.searchParams.get('project')) : cwd;
|
|
1478
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
1479
|
+
res.end(JSON.stringify({ sessions: c ? readSessionStatus(c) : [] }));
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1473
1483
|
if (pathname === '/api/required-reviewers') {
|
|
1474
1484
|
const c = url.searchParams.get('project') ? resolveProjectCwd(url.searchParams.get('project')) : cwd;
|
|
1475
1485
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
@@ -1696,6 +1696,15 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
1696
1696
|
.pl-stage.pl-compact .pl-name { color: var(--text2); }
|
|
1697
1697
|
.pl-stage.active { flex: 2; }
|
|
1698
1698
|
/* Idle-focus all-clear card (shown when nothing needs a decision). */
|
|
1699
|
+
/* A session stopped on a permission prompt or an unanswered wait — the one piece
|
|
1700
|
+
of session status the operator can act on, so it is styled like a decision. */
|
|
1701
|
+
.session-blocked {
|
|
1702
|
+
padding: 6px 10px;
|
|
1703
|
+
margin-bottom: 4px;
|
|
1704
|
+
border-left: 3px solid var(--status-blocked);
|
|
1705
|
+
background: var(--bg-elevated);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1699
1708
|
.inbox-allclear {
|
|
1700
1709
|
display: flex; align-items: center; gap: 14px;
|
|
1701
1710
|
padding: 22px 22px; margin-top: 4px;
|
|
@@ -3399,6 +3408,10 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3399
3408
|
two speaking states render into #inbox-receipt-foot at the bottom of
|
|
3400
3409
|
this page (see refreshReceipt). -->
|
|
3401
3410
|
<div id="inbox-receipt" style="display:none;margin:6px 0 2px;font-size:var(--fs-small)"></div>
|
|
3411
|
+
<!-- Sessions stopped on the operator: a permission prompt, or idle waiting for
|
|
3412
|
+
an answer (Notification hook → .great_cto/status/). The one piece of
|
|
3413
|
+
status that IS a decision, so it sits above the status disclosure. -->
|
|
3414
|
+
<div id="inbox-sessions" style="display:none;margin:6px 0 10px;font-size:var(--fs-small)"></div>
|
|
3402
3415
|
<!-- Everything below is STATUS, and status is not a decision.
|
|
3403
3416
|
|
|
3404
3417
|
Six of the seven blocks on this screen were read-only: a pipeline rail,
|
|
@@ -4673,7 +4686,28 @@ function renderInboxList(rootId, items, countId, opts = {}) {
|
|
|
4673
4686
|
}).join('');
|
|
4674
4687
|
}
|
|
4675
4688
|
|
|
4689
|
+
// Sessions waiting on a person (scripts/lib/session-status.mjs). `blocked` is a
|
|
4690
|
+
// prompt or an unanswered wait and gets a row; `waiting` (turn ended) is what every
|
|
4691
|
+
// open session is between turns, so it is one muted count, not a row each.
|
|
4692
|
+
async function refreshSessionStatus() {
|
|
4693
|
+
const el = document.getElementById('inbox-sessions');
|
|
4694
|
+
if (!el) return;
|
|
4695
|
+
const r = await api(`/api/session-status${pqs()}`);
|
|
4696
|
+
const rows = (r && Array.isArray(r.sessions)) ? r.sessions : [];
|
|
4697
|
+
const blocked = rows.filter((x) => x.state === 'blocked');
|
|
4698
|
+
const waiting = rows.filter((x) => x.state === 'waiting').length;
|
|
4699
|
+
if (!blocked.length && !waiting) { el.style.display = 'none'; el.innerHTML = ''; return; }
|
|
4700
|
+
el.style.display = '';
|
|
4701
|
+
el.innerHTML = blocked.map((x) => `<div class="session-blocked">
|
|
4702
|
+
<strong>⏸ session ${esc(String(x.session).slice(0, 8))} waits for you</strong>
|
|
4703
|
+
<span class="muted">· ${esc(agoStr(x.since))}${x.kind ? ` · ${esc(x.kind)}` : ''}</span><br>
|
|
4704
|
+
<span>${esc(x.reason || 'waiting for input')}</span>
|
|
4705
|
+
</div>`).join('')
|
|
4706
|
+
+ (waiting ? `<div class="muted" title="these sessions finished their turn; the next message is yours">${waiting} session${waiting === 1 ? '' : 's'} finished a turn and wait for your next message</div>` : '');
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4676
4709
|
async function refreshInbox() {
|
|
4710
|
+
refreshSessionStatus();
|
|
4677
4711
|
// Tiers first: the gate rows are rendered from this same call, and a row drawn
|
|
4678
4712
|
// before the tiers arrive would silently claim every gate is waiting.
|
|
4679
4713
|
await refreshGateTiers();
|
|
@@ -134,6 +134,9 @@ const TOOL_POSTURES = Object.freeze({
|
|
|
134
134
|
WebSearch: ['network.fetch', 'communication.external.send'],
|
|
135
135
|
Agent: ['process.spawn'],
|
|
136
136
|
Task: ['process.spawn'],
|
|
137
|
+
// Loads a skill's text into the agent's context. It grants no tool the agent does
|
|
138
|
+
// not already hold: the skill runs with the agent's own grants.
|
|
139
|
+
Skill: ['code.read'],
|
|
137
140
|
});
|
|
138
141
|
|
|
139
142
|
/** MCP and beta tools, matched by prefix — the list of these grows monthly. */
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-status — which sessions in this project are waiting for a person.
|
|
3
|
+
*
|
|
4
|
+
* Nothing showed it. On the projects measured on 2026-09-21 the operator was the
|
|
5
|
+
* approval loop (1,181 "делай" in 5,126 messages), and the board could not say
|
|
6
|
+
* which session was sitting on a permission prompt or had finished its turn and
|
|
7
|
+
* was waiting for an answer. Idea from fynnfluegge/agtx (Apache-2.0), which drives
|
|
8
|
+
* its board from the same hook events; the code here is written for this project.
|
|
9
|
+
*
|
|
10
|
+
* Three hook events write one small file per session, `.great_cto/status/<id>.json`:
|
|
11
|
+
*
|
|
12
|
+
* Notification → blocked (a permission prompt, or idle waiting for input;
|
|
13
|
+
* the notification's own message is the reason)
|
|
14
|
+
* Stop → waiting (the turn ended; the next move is the operator's)
|
|
15
|
+
* UserPromptSubmit → working
|
|
16
|
+
* SessionEnd → the file is removed
|
|
17
|
+
*
|
|
18
|
+
* A hook only fires on the transitions it is told about, so a state can go stale:
|
|
19
|
+
* a permission granted fires no event. The reader therefore checks the session's
|
|
20
|
+
* transcript — if it was written after the blocked/waiting moment, the session has
|
|
21
|
+
* moved on and is reported as working. A `working` with a silent transcript for
|
|
22
|
+
* five minutes is `unknown`, not working (agtx uses the same bound).
|
|
23
|
+
*/
|
|
24
|
+
import { mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, rmSync, renameSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
|
|
27
|
+
export const STALE_WORKING_MS = 5 * 60 * 1000;
|
|
28
|
+
export const MOVED_ON_SLACK_MS = 2000;
|
|
29
|
+
export const DROP_AFTER_MS = 24 * 60 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
const EVENT_STATE = { Notification: 'blocked', Stop: 'waiting', UserPromptSubmit: 'working' };
|
|
32
|
+
|
|
33
|
+
const safeId = (id) => String(id || '').replace(/[^A-Za-z0-9_-]/g, '').slice(0, 64);
|
|
34
|
+
|
|
35
|
+
/** Apply one hook payload. Returns what was written, or null when nothing applies. */
|
|
36
|
+
export function recordHookEvent(payload, { now = Date.now() } = {}) {
|
|
37
|
+
const cwd = payload?.cwd || process.cwd();
|
|
38
|
+
const id = safeId(payload?.session_id);
|
|
39
|
+
if (!id) return null;
|
|
40
|
+
const dir = join(cwd, '.great_cto', 'status');
|
|
41
|
+
const file = join(dir, `${id}.json`);
|
|
42
|
+
const event = payload?.hook_event_name;
|
|
43
|
+
if (event === 'SessionEnd') { rmSync(file, { force: true }); return { removed: true }; }
|
|
44
|
+
const state = EVENT_STATE[event];
|
|
45
|
+
if (!state) return null;
|
|
46
|
+
try { statSync(join(cwd, '.great_cto')); } catch { return null; } // not a great_cto project
|
|
47
|
+
const rec = {
|
|
48
|
+
session: id,
|
|
49
|
+
state,
|
|
50
|
+
since: new Date(now).toISOString(),
|
|
51
|
+
transcript: payload.transcript_path || null,
|
|
52
|
+
};
|
|
53
|
+
if (state === 'blocked') {
|
|
54
|
+
rec.reason = String(payload.message || 'waiting for you').slice(0, 300);
|
|
55
|
+
if (payload.notification_type) rec.kind = String(payload.notification_type).slice(0, 40);
|
|
56
|
+
}
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
59
|
+
writeFileSync(tmp, JSON.stringify(rec));
|
|
60
|
+
renameSync(tmp, file);
|
|
61
|
+
return rec;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The effective state of every session with a status file.
|
|
66
|
+
* @returns {{session:string, state:'blocked'|'waiting'|'working'|'unknown', reason?:string, kind?:string, since:string}[]}
|
|
67
|
+
*/
|
|
68
|
+
export function readSessionStatus(projectDir, { now = Date.now() } = {}) {
|
|
69
|
+
const dir = join(projectDir, '.great_cto', 'status');
|
|
70
|
+
let files = [];
|
|
71
|
+
try { files = readdirSync(dir).filter((f) => f.endsWith('.json')); } catch { return []; }
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const f of files) {
|
|
74
|
+
let rec;
|
|
75
|
+
try { rec = JSON.parse(readFileSync(join(dir, f), 'utf8')); } catch { continue; }
|
|
76
|
+
const since = Date.parse(rec.since);
|
|
77
|
+
if (!Number.isFinite(since) || now - since > DROP_AFTER_MS) continue;
|
|
78
|
+
let moved = null;
|
|
79
|
+
if (rec.transcript) {
|
|
80
|
+
try { moved = statSync(rec.transcript).mtimeMs; } catch { moved = null; }
|
|
81
|
+
}
|
|
82
|
+
let state = rec.state;
|
|
83
|
+
if ((state === 'blocked' || state === 'waiting') && moved !== null && moved > since + MOVED_ON_SLACK_MS) state = 'working';
|
|
84
|
+
if (state === 'working' && now - Math.max(since, moved || 0) > STALE_WORKING_MS) state = 'unknown';
|
|
85
|
+
const row = { session: rec.session, state, since: rec.since };
|
|
86
|
+
if (state === 'blocked') { row.reason = rec.reason; if (rec.kind) row.kind = rec.kind; }
|
|
87
|
+
out.push(row);
|
|
88
|
+
}
|
|
89
|
+
const rank = { blocked: 0, waiting: 1, working: 2, unknown: 3 };
|
|
90
|
+
return out.sort((a, b) => rank[a.state] - rank[b.state] || (a.since < b.since ? -1 : 1));
|
|
91
|
+
}
|
|
@@ -202,6 +202,12 @@ export function parseVerdictLine(line) {
|
|
|
202
202
|
// is a verdict this system knows. Neither position matching leaves the
|
|
203
203
|
// original reading in place rather than guessing a different wrong one.
|
|
204
204
|
const parts = raw.split(/\s+/);
|
|
205
|
+
// `2026-07-12 15:47:00 PASS …` — a date and a time written with a space, as
|
|
206
|
+
// one project's QA log was. Read as two tokens, the time became the verdict
|
|
207
|
+
// and a passing QA read as no verdict at all.
|
|
208
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(parts[0] || '') && /^\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/.test(parts[1] || '')) {
|
|
209
|
+
parts.splice(0, 2, `${parts[0]}T${parts[1]}`);
|
|
210
|
+
}
|
|
205
211
|
ts = parts[0] || '';
|
|
206
212
|
const known = (t) => KNOWN_VERDICTS.includes(String(t || '').toUpperCase());
|
|
207
213
|
if (!known(parts[1]) && known(parts[2])) {
|
package/package.json
CHANGED