great-cto 3.29.1 → 3.31.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.29.1",
4
+ "version": "3.31.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 v2.5.8: keep only 3 most recent cached versions to prevent disk bloat. ls -d \"$HOME\"/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | sort -V | awk -v k=3 '{a[NR]=$0} END {for (i=1; i<=NR-k; i++) print a[i]}' | xargs -I{} rm -rf {} 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
  },
@@ -315,5 +315,77 @@
315
315
  ]
316
316
  },
317
317
  "homepage": "https://greatcto.systems",
318
- "repository": "https://github.com/avelikiy/great_cto"
318
+ "repository": "https://github.com/avelikiy/great_cto",
319
+ "agents": [
320
+ "./agents-full/accounting-reviewer.md",
321
+ "./agents-full/adtech-privacy-reviewer.md",
322
+ "./agents-full/ai-eval-engineer.md",
323
+ "./agents-full/ai-prompt-architect.md",
324
+ "./agents-full/ai-security-reviewer.md",
325
+ "./agents-full/api-platform-reviewer.md",
326
+ "./agents-full/app-scaffolder.md",
327
+ "./agents-full/architect.md",
328
+ "./agents-full/auth-engineer.md",
329
+ "./agents-full/cli-reviewer.md",
330
+ "./agents-full/cmmc-reviewer.md",
331
+ "./agents-full/cms-reviewer.md",
332
+ "./agents-full/code-reviewer.md",
333
+ "./agents-full/connector-builder.md",
334
+ "./agents-full/continuous-learner.md",
335
+ "./agents-full/coordinator.md",
336
+ "./agents-full/data-platform-reviewer.md",
337
+ "./agents-full/db-migration-reviewer.md",
338
+ "./agents-full/decision-scorer.md",
339
+ "./agents-full/design-advisor.md",
340
+ "./agents-full/devops.md",
341
+ "./agents-full/devtools-reviewer.md",
342
+ "./agents-full/dpdpa-reviewer.md",
343
+ "./agents-full/e2e-test-engineer.md",
344
+ "./agents-full/edtech-reviewer.md",
345
+ "./agents-full/enterprise-saas-reviewer.md",
346
+ "./agents-full/firmware-reviewer.md",
347
+ "./agents-full/game-reviewer.md",
348
+ "./agents-full/gdpr-reviewer.md",
349
+ "./agents-full/geo-routing-engineer.md",
350
+ "./agents-full/gov-reviewer.md",
351
+ "./agents-full/growth-engineer.md",
352
+ "./agents-full/healthcare-reviewer.md",
353
+ "./agents-full/hr-ai-reviewer.md",
354
+ "./agents-full/infra-provisioner.md",
355
+ "./agents-full/infra-reviewer.md",
356
+ "./agents-full/insurance-reviewer.md",
357
+ "./agents-full/integrations-engineer.md",
358
+ "./agents-full/knowledge-extractor.md",
359
+ "./agents-full/l3-support.md",
360
+ "./agents-full/legal-reviewer.md",
361
+ "./agents-full/library-reviewer.md",
362
+ "./agents-full/marketplace-reviewer.md",
363
+ "./agents-full/mcp-server-reviewer.md",
364
+ "./agents-full/media-pipeline-engineer.md",
365
+ "./agents-full/migration-import-engineer.md",
366
+ "./agents-full/mlops-reviewer.md",
367
+ "./agents-full/mobile-app-builder.md",
368
+ "./agents-full/mobile-store-reviewer.md",
369
+ "./agents-full/msp-reviewer.md",
370
+ "./agents-full/oracle-reviewer.md",
371
+ "./agents-full/pci-reviewer.md",
372
+ "./agents-full/performance-engineer.md",
373
+ "./agents-full/pm.md",
374
+ "./agents-full/procurement-reviewer.md",
375
+ "./agents-full/product-owner.md",
376
+ "./agents-full/project-auditor.md",
377
+ "./agents-full/qa-engineer.md",
378
+ "./agents-full/quant-researcher.md",
379
+ "./agents-full/rcm-reviewer.md",
380
+ "./agents-full/regulated-reviewer.md",
381
+ "./agents-full/security-officer.md",
382
+ "./agents-full/senior-dev.md",
383
+ "./agents-full/streaming-reviewer.md",
384
+ "./agents-full/subscription-billing-engineer.md",
385
+ "./agents-full/tax-reviewer.md",
386
+ "./agents-full/us-ai-reviewer.md",
387
+ "./agents-full/us-privacy-reviewer.md",
388
+ "./agents-full/voice-ai-reviewer.md",
389
+ "./agents-full/web-store-reviewer.md"
390
+ ]
319
391
  }
@@ -12,6 +12,8 @@ import { eventSurface, readFileSafe, originAllowed } from './util.mjs';
12
12
  import { sseClients, notifHistory } from './state.mjs';
13
13
  import { autoRegisterProject, listProjects, resolveProjectCwd, resolveProjectInfo, getChangeTier, readProjectsRegistry, getRegistryDegradation } from './projects.mjs';
14
14
  import { readVerdictsWithHealth } from './verdicts.mjs';
15
+ import { agentUsage, usageSnapshot } from '../../../scripts/lib/agent-usage.mjs';
16
+ import { reviewerStatus } from '../../../scripts/lib/required-reviewers.mjs';
15
17
  import { readScores, summarizeScores } from '../../../scripts/lib/scores.mjs';
16
18
  import { status as routerKeyStatus, writeKey as writeRouterKey, verifyKey as verifyRouterKey } from '../../../scripts/lib/router-key.mjs';
17
19
 
@@ -1455,6 +1457,26 @@ async function dispatch(req, res, url, cwd) {
1455
1457
  return true;
1456
1458
  }
1457
1459
 
1460
+ // Real dispatches per agent, read from Claude Code's session logs on this
1461
+ // machine (scripts/lib/agent-usage.mjs). Machine-wide, not per project: the logs
1462
+ // are one history. The first pass over a large history takes about a minute, so
1463
+ // the snapshot answers at once — `computing`, then the last result — and never
1464
+ // makes a request wait for the scan.
1465
+ if (pathname === '/api/agent-usage') {
1466
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
1467
+ res.end(JSON.stringify(boardUsage().get()));
1468
+ return true;
1469
+ }
1470
+
1471
+ // Domain reviewers this project's PROJECT.md requires (archetype, packs,
1472
+ // compliance), each with whether a verdict exists — the list gate:ship refuses on.
1473
+ if (pathname === '/api/required-reviewers') {
1474
+ const c = url.searchParams.get('project') ? resolveProjectCwd(url.searchParams.get('project')) : cwd;
1475
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
1476
+ res.end(JSON.stringify(c ? reviewerStatus(c) : { state: 'no-project', reviewers: [] }));
1477
+ return true;
1478
+ }
1479
+
1458
1480
  // Installed agents — fleet view (DESIGN-agents-fleet-view §3.1).
1459
1481
  //
1460
1482
  // Extended in 2026-05-15 from a flat list to a faceted-fleet payload:
@@ -2084,4 +2106,16 @@ async function dispatch(req, res, url, cwd) {
2084
2106
  * the inbox.
2085
2107
  */
2086
2108
 
2109
+
2110
+ let _usageSnap = null;
2111
+ function boardUsage() {
2112
+ if (!_usageSnap) {
2113
+ const agentsDir = path.join(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'agents');
2114
+ let agents = [];
2115
+ try { agents = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3)); } catch { /* none: every count is unavailable-by-omission */ }
2116
+ _usageSnap = usageSnapshot({ compute: () => agentUsage({ agents }) });
2117
+ }
2118
+ return _usageSnap;
2119
+ }
2120
+
2087
2121
  export { dispatch, secondOpinionForTree };
@@ -8668,9 +8668,42 @@ const fleetState = {
8668
8668
  search: '',
8669
8669
  sort: 'runs_30d',
8670
8670
  drawerSlug: null,
8671
+ // Real dispatches from the session logs (/api/agent-usage). Machine-wide, and
8672
+ // null until the first answer; `computing` while the first scan runs.
8673
+ usage: null,
8674
+ // Reviewers this project requires (/api/required-reviewers) — gate:ship refuses
8675
+ // while one has no verdict.
8676
+ required: null,
8671
8677
  };
8678
+ function requiredOf(slug) {
8679
+ const r = fleetState.required;
8680
+ return r && r.state === 'read' ? (r.reviewers || []).find((x) => x.agent === slug) || null : null;
8681
+ }
8682
+ async function refreshRequiredReviewers() {
8683
+ const r = await api(`/api/required-reviewers${pqs()}`);
8684
+ if (!r) return;
8685
+ fleetState.required = r;
8686
+ renderFleetList();
8687
+ }
8688
+
8689
+ // Dispatches for one agent, or null when the logs were not counted. Null is not
8690
+ // zero: a zero means the logs were read and never named this agent.
8691
+ function usageOf(slug) {
8692
+ const u = fleetState.usage;
8693
+ return u && u.state === 'counted' && u.agents ? (u.agents[slug] || { dispatches: 0, lastRun: null, projects: 0 }) : null;
8694
+ }
8695
+ async function refreshAgentUsage() {
8696
+ const u = await api('/api/agent-usage');
8697
+ if (!u) return;
8698
+ fleetState.usage = u;
8699
+ renderFleetList();
8700
+ // The first scan of a large history takes about a minute; ask again until it lands.
8701
+ if (u.state === 'computing' || u.refreshing) setTimeout(refreshAgentUsage, 15000);
8702
+ }
8672
8703
 
8673
8704
  async function refreshAgentsInstalled() {
8705
+ refreshAgentUsage();
8706
+ refreshRequiredReviewers();
8674
8707
  const data = await api(`/api/agents-installed${pqs()}`);
8675
8708
  if (!data) return;
8676
8709
  fleetState.agents = data.agents || [];
@@ -8703,7 +8736,8 @@ const FLEET_VIEWS = [
8703
8736
  // brief refuses an inferred idle/retired status in v1.
8704
8737
  function agentNeedsAttention(a) {
8705
8738
  const p = a.posture || {};
8706
- return a.health === 'failing'
8739
+ const req = requiredOf(a.slug);
8740
+ return (req && !req.verdict) || a.health === 'failing'
8707
8741
  || p.state === 'unclassified' || p.state === 'unreadable'
8708
8742
  || (Array.isArray(p.scopedInNameOnly) && p.scopedInNameOnly.length > 0)
8709
8743
  || String(a.budget?.state || '').startsWith('over');
@@ -8713,7 +8747,9 @@ function inFleetView(a, view) {
8713
8747
  if (a.retired) return false;
8714
8748
  if (view === 'needs') return agentNeedsAttention(a);
8715
8749
  if (view === 'expensive') return a.posture?.state === 'expensive';
8716
- if (view === 'never') return !(a.runs_total > 0);
8750
+ // Never observed: no verdict line, and — when the logs were counted — no
8751
+ // dispatch either. An agent that ran without reporting is not "never".
8752
+ if (view === 'never') return !(a.runs_total > 0) && !((usageOf(a.slug)?.dispatches || 0) > 0);
8717
8753
  return true;
8718
8754
  }
8719
8755
  function setFleetView(view) {
@@ -8809,7 +8845,12 @@ function renderFleetList() {
8809
8845
  const order = [...DOMAINS, ...Object.keys(byDomain).filter((d) => !DOMAINS.includes(d))];
8810
8846
  const expanded = order.filter((d) => (byDomain[d] || []).length);
8811
8847
  const collapsed = order.filter((d) => !(byDomain[d] || []).length && totals[d]);
8812
- const head = `<li class="fleet-head" aria-hidden="true"><span></span><span>Agent</span><span>Posture</span><span>Last verdict</span><span class="tab">Runs</span><span class="tab">Pass</span><span class="tab">30d $</span><span></span></li>`;
8848
+ const uu = fleetState.usage;
8849
+ const usageLine = !uu ? ''
8850
+ : uu.state === 'counted'
8851
+ ? `<li class="fleet-group-collapsed" title="read from Claude Code session logs on this machine; they keep about 30 days"><span>dispatched in session logs: ${Object.values(uu.agents || {}).filter((x) => x.dispatches > 0).length} of ${Object.keys(uu.agents || {}).length} agents · ${esc((uu.window?.from || '?').slice(0, 10))} → ${esc((uu.window?.to || '?').slice(0, 10))}</span></li>`
8852
+ : `<li class="fleet-group-collapsed"><span class="abs">${esc(uu.state === 'computing' ? 'counting dispatches in the session logs…' : (uu.why || 'dispatches not counted'))}</span></li>`;
8853
+ const head = usageLine + `<li class="fleet-head" aria-hidden="true"><span></span><span>Agent</span><span>Posture</span><span>Last verdict</span><span class="tab">Runs</span><span class="tab">Pass</span><span class="tab">30d $</span><span></span></li>`;
8813
8854
  const rows = head + expanded.map((d) => `
8814
8855
  <li class="fleet-group-head"><span>${esc(d)} · ${byDomain[d].length} of ${totals[d] || 0}</span></li>
8815
8856
  ${byDomain[d].map(agentRowHtml).join('')}`).join('');
@@ -8843,13 +8884,27 @@ function postureChips(a) {
8843
8884
  }
8844
8885
  return out.join('');
8845
8886
  }
8887
+ // Required by this project's PROJECT.md: the chip says why, and whether it has run.
8888
+ function requiredChip(a) {
8889
+ const r = requiredOf(a.slug);
8890
+ if (!r) return '';
8891
+ return r.verdict
8892
+ ? `<span class="pos rou" title="required by ${esc(r.why)} — has a verdict in this project">✓ required</span>`
8893
+ : `<span class="pos warn" title="required by ${esc(r.why)} — no verdict in this project yet; gate:ship refuses until it runs or a signed exception names it">⚠ required · not run</span>`;
8894
+ }
8846
8895
  function agentRowHtml(a) {
8847
8896
  const rate = a.success_rate;
8848
8897
  const runs = a.runs_30d ?? 0;
8849
8898
  const observed = a.runs_total > 0 || !!a.last_run;
8899
+ const u = usageOf(a.slug);
8900
+ const dispatchNote = u ? `dispatched ${u.dispatches}× in the session logs${u.lastRun ? `, last ${agoStr(u.lastRun)}` : ''}${u.projects ? `, ${u.projects} project${u.projects === 1 ? '' : 's'}` : ''}` : 'dispatches not counted';
8850
8901
  const last = observed
8851
- ? (a.health === 'failing' ? `<span style="color:var(--status-blocked)">failing</span> ${esc(agoStr(a.last_run))}` : `<span style="color:var(--status-done)">ok</span> ${esc(agoStr(a.last_run))}`)
8852
- : '<span class="abs" title="no verdict line names this agent — absence of a log is not absence of a run">never observed</span>';
8902
+ ? `<span title="${esc(dispatchNote)}">${a.health === 'failing' ? `<span style="color:var(--status-blocked)">failing</span>` : `<span style="color:var(--status-done)">ok</span>`} ${esc(agoStr(a.last_run))}</span>`
8903
+ : u && u.dispatches > 0
8904
+ ? `<span class="abs" title="${esc(dispatchNote)} — but no verdict line names it: it ran and did not report">dispatched ${u.dispatches}× · no verdict</span>`
8905
+ : u
8906
+ ? '<span class="abs" title="no verdict line names this agent, and the session logs on this machine never dispatched it">never dispatched</span>'
8907
+ : '<span class="abs" title="no verdict line names this agent — absence of a log is not absence of a run">never observed</span>';
8853
8908
  const pass = !observed ? '<span class="abs" title="never observed">—</span>'
8854
8909
  : runs === 0 ? '<span class="abs" title="no run in this window — measured, none">—</span>'
8855
8910
  : rate != null ? `${rate}%` : '<span class="abs" title="runs exist but no verdict carried a result yet">·</span>';
@@ -8867,7 +8922,7 @@ function agentRowHtml(a) {
8867
8922
  onkeydown="if(event.key==='Enter'){event.preventDefault();openAgentDrawer('${esc(a.slug)}')}">
8868
8923
  <span class="ar-status" aria-hidden="true"></span>
8869
8924
  <span class="ar-name">${esc(a.slug)} <span class="mono cap muted" style="font-weight:400">${model}</span>${a.retired ? ' <span class="cap" style="color:var(--agent-retired)">retired</span>' : ''}</span>
8870
- <span class="ar-posture">${postureChips(a)}</span>
8925
+ <span class="ar-posture">${requiredChip(a)}${postureChips(a)}</span>
8871
8926
  <span class="mono cap ar-last">${last}</span>
8872
8927
  <span class="mono cap tab" title="runs in the last 30 days">${runs}</span>
8873
8928
  <span class="mono cap tab" title="pass rate over those runs">${pass}</span>
@@ -0,0 +1,195 @@
1
+ /**
2
+ * agent-usage — how often each great_cto agent is really dispatched, read from
3
+ * Claude Code's own session logs (`~/.claude/projects/**\/*.jsonl`).
4
+ *
5
+ * The board's Fleet screen said "never observed" when no verdict line named an
6
+ * agent. A verdict line is written at the end of a run, so a run that wrote none
7
+ * was invisible, and nothing said how often an agent is dispatched at all. On
8
+ * 2026-09-21 the logs of one machine showed 26 of 70 agents dispatched at least
9
+ * once in the retained window, and 44 never. Idea from migsilva89/loadout (MIT),
10
+ * which reads assistant session logs for the same reason.
11
+ *
12
+ * Rules:
13
+ * - Read-only on the logs. They belong to Claude Code; the index this keeps lives
14
+ * in ~/.great_cto, never next to them.
15
+ * - `unavailable` when the logs directory cannot be read — never a zero, which
16
+ * would read as disuse. A zero is reported only for an agent the logs were read
17
+ * for and did not name.
18
+ * - Paths never leave this module. Projects are counted as distinct `cwd` values,
19
+ * kept in the index only as short hashes.
20
+ * - The logs are gigabytes; an unchanged file (same size and mtime) is taken from
21
+ * the index, and only lines that mention `subagent_type` are parsed.
22
+ */
23
+ import fs from 'node:fs';
24
+ import os from 'node:os';
25
+ import path from 'node:path';
26
+ import readline from 'node:readline';
27
+ import { createHash } from 'node:crypto';
28
+
29
+ const INDEX_VERSION = 1;
30
+
31
+ export const DEFAULT_LOGS_DIR = path.join(os.homedir(), '.claude', 'projects');
32
+ export const DEFAULT_CACHE_FILE = path.join(os.homedir(), '.great_cto', 'usage-index.json');
33
+
34
+ const short = (s) => createHash('sha1').update(String(s)).digest('hex').slice(0, 12);
35
+
36
+ function listTranscripts(dir) {
37
+ const out = [];
38
+ const walk = (d, depth) => {
39
+ let entries;
40
+ try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
41
+ for (const e of entries) {
42
+ const p = path.join(d, e.name);
43
+ if (e.isDirectory()) { if (depth < 5) walk(p, depth + 1); }
44
+ else if (e.isFile() && e.name.endsWith('.jsonl')) out.push(p);
45
+ }
46
+ };
47
+ walk(dir, 0);
48
+ return out;
49
+ }
50
+
51
+ /** Dispatches in one transcript: { [rawName]: { n, last, cwds:[hash] } } plus its time span. */
52
+ async function readTranscript(file) {
53
+ const perName = {};
54
+ let from = null;
55
+ let to = null;
56
+ const stream = fs.createReadStream(file, { encoding: 'utf8' });
57
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
58
+ for await (const line of rl) {
59
+ if (!line.includes('"subagent_type"')) continue;
60
+ let d;
61
+ try { d = JSON.parse(line); } catch { continue; }
62
+ const content = d?.message?.content;
63
+ if (!Array.isArray(content)) continue;
64
+ for (const c of content) {
65
+ if (c?.type !== 'tool_use' || (c?.name !== 'Agent' && c?.name !== 'Task')) continue;
66
+ const raw = c?.input?.subagent_type;
67
+ if (typeof raw !== 'string' || !raw) continue;
68
+ const ts = typeof d.timestamp === 'string' ? d.timestamp : null;
69
+ const slot = perName[raw] || (perName[raw] = { n: 0, last: null, cwds: [] });
70
+ slot.n++;
71
+ if (ts && (!slot.last || ts > slot.last)) slot.last = ts;
72
+ const h = d.cwd ? short(d.cwd) : null;
73
+ if (h && !slot.cwds.includes(h)) slot.cwds.push(h);
74
+ if (ts) { if (!from || ts < from) from = ts; if (!to || ts > to) to = ts; }
75
+ }
76
+ }
77
+ return { perName, from, to };
78
+ }
79
+
80
+ function loadIndex(cacheFile) {
81
+ try {
82
+ const x = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
83
+ if (x && x.version === INDEX_VERSION && x.files && typeof x.files === 'object') return x;
84
+ } catch { /* missing or corrupt: rebuilt below, never trusted */ }
85
+ return { version: INDEX_VERSION, files: {} };
86
+ }
87
+
88
+ function saveIndex(cacheFile, index) {
89
+ try {
90
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
91
+ const tmp = `${cacheFile}.${process.pid}.tmp`;
92
+ fs.writeFileSync(tmp, JSON.stringify(index));
93
+ fs.renameSync(tmp, cacheFile);
94
+ } catch { /* an index that cannot be saved costs a re-read next time, nothing else */ }
95
+ }
96
+
97
+ /** `great-cto:senior-dev` and `senior-dev` are the same agent. */
98
+ const agentOf = (raw, known) => {
99
+ const name = raw.includes(':') ? raw.slice(raw.lastIndexOf(':') + 1) : raw;
100
+ return known.has(name) ? name : null;
101
+ };
102
+
103
+ /**
104
+ * @param {{logsDir?:string, cacheFile?:string, agents:string[]}} opts
105
+ * @returns {Promise<{state:'counted'|'unavailable', why?:string, transcripts:number, read:number,
106
+ * window:{from:string|null,to:string|null}, agents:Record<string,{dispatches:number,lastRun:string|null,projects:number}>}>}
107
+ */
108
+ export async function agentUsage({ logsDir = DEFAULT_LOGS_DIR, cacheFile = DEFAULT_CACHE_FILE, agents = [] } = {}) {
109
+ let isDir = false;
110
+ try { isDir = fs.statSync(logsDir).isDirectory(); } catch { /* absent */ }
111
+ if (!isDir) {
112
+ return { state: 'unavailable', why: 'no Claude Code session logs on this machine', transcripts: 0, read: 0, window: { from: null, to: null }, agents: {} };
113
+ }
114
+
115
+ const known = new Set(agents);
116
+ const index = loadIndex(cacheFile);
117
+ const next = { version: INDEX_VERSION, files: {} };
118
+ let read = 0;
119
+
120
+ for (const file of listTranscripts(logsDir)) {
121
+ let st;
122
+ try { st = fs.statSync(file); } catch { continue; }
123
+ const key = short(file);
124
+ const prev = index.files[key];
125
+ if (prev && prev.size === st.size && prev.mtimeMs === st.mtimeMs) { next.files[key] = prev; continue; }
126
+ try {
127
+ const r = await readTranscript(file);
128
+ next.files[key] = { size: st.size, mtimeMs: st.mtimeMs, ...r };
129
+ read++;
130
+ } catch { /* an unreadable transcript is skipped, not counted as empty */ }
131
+ }
132
+ saveIndex(cacheFile, next);
133
+
134
+ const out = {};
135
+ for (const a of agents) out[a] = { dispatches: 0, lastRun: null, projects: 0 };
136
+ const cwds = {};
137
+ let from = null;
138
+ let to = null;
139
+ for (const f of Object.values(next.files)) {
140
+ if (f.from && (!from || f.from < from)) from = f.from;
141
+ if (f.to && (!to || f.to > to)) to = f.to;
142
+ for (const [raw, slot] of Object.entries(f.perName || {})) {
143
+ const a = agentOf(raw, known);
144
+ if (!a) continue;
145
+ out[a].dispatches += slot.n;
146
+ if (slot.last && (!out[a].lastRun || slot.last > out[a].lastRun)) out[a].lastRun = slot.last;
147
+ const seen = cwds[a] || (cwds[a] = new Set());
148
+ for (const h of slot.cwds) seen.add(h);
149
+ }
150
+ }
151
+ for (const [a, s] of Object.entries(cwds)) out[a].projects = s.size;
152
+
153
+ return { state: 'counted', transcripts: Object.keys(next.files).length, read, window: { from, to }, agents: out };
154
+ }
155
+
156
+ // CLI: node scripts/lib/agent-usage.mjs [--json]
157
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
158
+ if (isMain) {
159
+ const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
160
+ const agents = fs.readdirSync(path.join(root, 'agents')).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3));
161
+ const u = await agentUsage({ agents });
162
+ if (process.argv.includes('--json')) { console.log(JSON.stringify(u, null, 2)); process.exit(0); }
163
+ if (u.state !== 'counted') { console.log(`agent-usage: ${u.why}`); process.exit(0); }
164
+ const rows = Object.entries(u.agents).sort((x, y) => y[1].dispatches - x[1].dispatches);
165
+ const used = rows.filter(([, v]) => v.dispatches > 0).length;
166
+ console.log(`agent-usage: ${used} of ${rows.length} agents dispatched · ${u.transcripts} transcripts · window ${u.window.from?.slice(0, 10) ?? '?'} → ${u.window.to?.slice(0, 10) ?? '?'}`);
167
+ for (const [a, v] of rows) console.log(` ${String(v.dispatches).padStart(5)} ${a.padEnd(30)} ${v.lastRun ? v.lastRun.slice(0, 10) : 'never'} projects=${v.projects}`);
168
+ }
169
+
170
+ /**
171
+ * A request-safe view of agentUsage: `get()` answers at once — `computing` before
172
+ * the first pass finishes, then the last result — and starts at most one refresh
173
+ * in the background when the result is older than `ttlMs`. The first pass over
174
+ * real logs took 47 s; a board request cannot wait for that.
175
+ */
176
+ export function usageSnapshot({ compute, ttlMs = 10 * 60 * 1000, now = () => Date.now() }) {
177
+ let last = null;
178
+ let at = 0;
179
+ let running = null;
180
+ const start = () => {
181
+ if (running) return;
182
+ let pass;
183
+ try { pass = Promise.resolve(compute()); } catch (e) { pass = Promise.reject(e); }
184
+ running = pass
185
+ .then((r) => { last = r; }, (e) => { last = { state: 'unavailable', why: `usage could not be read: ${e?.message || e}` }; })
186
+ .finally(() => { at = now(); running = null; });
187
+ };
188
+ return {
189
+ get() {
190
+ if (!last) { start(); return { state: 'computing', why: 'counting agent dispatches in the session logs — the first pass over a large history takes a minute' }; }
191
+ if (now() - at > ttlMs) { start(); return { ...last, refreshing: true }; }
192
+ return last;
193
+ },
194
+ };
195
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * required-reviewers — which domain reviewers a project must have run, read from its
3
+ * `.great_cto/PROJECT.md`.
4
+ *
5
+ * The product says it wires the right reviewers for a project's archetype and
6
+ * compliance. Against this machine's session logs (30.06–21.09) and 22 registered
7
+ * projects, it did not: pci-reviewer was needed by 7 projects and dispatched once;
8
+ * enterprise-saas, us-privacy and voice-ai reviewers were needed by 3–5 and never ran.
9
+ * Nothing required them — `pipeline-state` made only QA and security mandatory, and
10
+ * the auto-attach hook matched file paths, never the archetype.
11
+ *
12
+ * The registry below is the CLI's (packages/cli/src/archetypes.ts REVIEWERS_BY_ARCHETYPE,
13
+ * packs.ts PACK_REVIEWERS), which plugin scripts cannot import from TypeScript;
14
+ * tests/lib/required-reviewers.test.mjs fails when the two drift.
15
+ */
16
+ import { readFileSync, existsSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+
19
+ export const REVIEWERS_BY_ARCHETYPE = {
20
+ "web-service": ["security-officer"],
21
+ "mobile-app": ["mobile-store-reviewer", "security-officer"],
22
+ "ai-system": ["ai-security-reviewer", "ai-prompt-architect", "ai-eval-engineer"],
23
+ "agent-product": ["ai-security-reviewer", "ai-prompt-architect", "ai-eval-engineer"],
24
+ "mlops": ["mlops-reviewer", "ai-security-reviewer"],
25
+ "data-platform": ["data-platform-reviewer"],
26
+ "streaming": ["streaming-reviewer"],
27
+ "infra": ["infra-reviewer"],
28
+ "library": ["library-reviewer"],
29
+ "cli-tool": ["cli-reviewer"],
30
+ "commerce": ["pci-reviewer", "security-officer"],
31
+ "marketplace": ["marketplace-reviewer", "pci-reviewer"],
32
+ "fintech": ["pci-reviewer", "regulated-reviewer"],
33
+ "healthcare": ["healthcare-reviewer", "security-officer"],
34
+ "web3": ["oracle-reviewer"],
35
+ "iot-embedded": ["firmware-reviewer"],
36
+ "regulated": ["regulated-reviewer"],
37
+ "devtools": ["devtools-reviewer"],
38
+ "browser-extension": ["web-store-reviewer"],
39
+ "game": ["game-reviewer"],
40
+ "cms": ["cms-reviewer"],
41
+ "enterprise-saas": ["enterprise-saas-reviewer"],
42
+ "edtech": ["edtech-reviewer"],
43
+ "gov-public": ["gov-reviewer", "security-officer"],
44
+ "insurance": ["insurance-reviewer", "regulated-reviewer"],
45
+ "legal": ["legal-reviewer", "security-officer"],
46
+ "defense-govcon": ["cmmc-reviewer", "gov-reviewer", "security-officer"],
47
+ "vertical-saas": ["security-officer"],
48
+ "booking": ["pci-reviewer", "security-officer"],
49
+ "crm": ["security-officer"],
50
+ "dashboard": ["security-officer"],
51
+ "content-platform": ["pci-reviewer", "security-officer"],
52
+ "marketplace-lite": ["pci-reviewer", "security-officer"],
53
+ "greenfield": [],
54
+ };
55
+
56
+ export const PACK_REVIEWERS = {
57
+ "voice-pack": [
58
+ "voice-ai-reviewer"
59
+ ],
60
+ "hr-ai-pack": [
61
+ "hr-ai-reviewer"
62
+ ],
63
+ "api-platform-pack": [
64
+ "api-platform-reviewer"
65
+ ],
66
+ "adtech-privacy-pack": [
67
+ "adtech-privacy-reviewer",
68
+ "us-privacy-reviewer"
69
+ ],
70
+ "us-ai-pack": [
71
+ "us-ai-reviewer"
72
+ ]
73
+ };
74
+
75
+ /** A compliance token in PROJECT.md → the reviewer that owns it. Matched as whole tokens. */
76
+ export const COMPLIANCE_REVIEWERS = [
77
+ { token: /\bpci(?:-dss)?\b/i, reviewer: 'pci-reviewer' },
78
+ { token: /\bhipaa\b/i, reviewer: 'healthcare-reviewer' },
79
+ { token: /\btcpa\b/i, reviewer: 'voice-ai-reviewer' },
80
+ { token: /\bgdpr\b/i, reviewer: 'gdpr-reviewer' },
81
+ { token: /\b(?:ccpa|cpra|us-privacy)\b/i, reviewer: 'us-privacy-reviewer' },
82
+ { token: /\bdpdpa\b/i, reviewer: 'dpdpa-reviewer' },
83
+ { token: /\bcmmc\b/i, reviewer: 'cmmc-reviewer' },
84
+ ];
85
+
86
+ // QA and security are already mandatory for every build (pipeline-state MANDATORY);
87
+ // this module is about the domain reviewers the project's own declaration implies.
88
+ const ALWAYS_MANDATORY = new Set(['security-officer', 'qa-engineer']);
89
+
90
+ const field = (text, key) => {
91
+ const m = String(text).match(new RegExp(`^${key}:\\s*(.*)$`, 'm'));
92
+ return m ? m[1].trim() : null;
93
+ };
94
+ const listOf = (v) => (v ? v.replace(/^\[|\]$/g, '').split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean) : []);
95
+
96
+ /**
97
+ * @param {string} projectMd the text of .great_cto/PROJECT.md
98
+ * @returns {{agent:string, why:string}[]} each required reviewer once, with every reason
99
+ */
100
+ export function requiredReviewers(projectMd) {
101
+ const out = new Map();
102
+ const add = (agent, why) => {
103
+ if (ALWAYS_MANDATORY.has(agent)) return;
104
+ const prev = out.get(agent);
105
+ out.set(agent, prev ? `${prev}; ${why}` : why);
106
+ };
107
+ // `primary:` is sometimes a display label (`developer-tools`) beside a registry key in
108
+ // `archetype:` (`devtools`); the first value the registry knows decides.
109
+ const archetypeName = ['primary', 'archetype']
110
+ .map((k) => (field(projectMd, k) || '').split(/\s|#/)[0])
111
+ .find((v) => v && REVIEWERS_BY_ARCHETYPE[v]) || null;
112
+ for (const r of REVIEWERS_BY_ARCHETYPE[archetypeName] || []) add(r, `archetype ${archetypeName}`);
113
+ for (const p of listOf(field(projectMd, 'packs'))) for (const r of PACK_REVIEWERS[p] || []) add(r, `pack ${p}`);
114
+ const compliance = field(projectMd, 'compliance') || '';
115
+ for (const { token, reviewer } of COMPLIANCE_REVIEWERS) {
116
+ const m = compliance.match(token);
117
+ if (m) add(reviewer, `compliance ${m[0]}`);
118
+ }
119
+ return [...out].map(([agent, why]) => ({ agent, why }));
120
+ }
121
+
122
+ /**
123
+ * Required reviewers for a project directory, each with whether a verdict exists.
124
+ * A verdict is a non-empty line in .great_cto/verdicts/<agent>.log — evidence the
125
+ * reviewer ran for this project, not for which feature.
126
+ * @returns {{state:'read'|'no-project', reviewers:{agent:string, why:string, verdict:boolean}[]}}
127
+ */
128
+ export function reviewerStatus(projectDir) {
129
+ const pm = join(projectDir, '.great_cto', 'PROJECT.md');
130
+ if (!existsSync(pm)) return { state: 'no-project', reviewers: [] };
131
+ const reviewers = requiredReviewers(readFileSync(pm, 'utf8')).map((r) => {
132
+ let verdict = false;
133
+ try { verdict = /\S/.test(readFileSync(join(projectDir, '.great_cto', 'verdicts', `${r.agent}.log`), 'utf8')); } catch { /* no log: never ran here */ }
134
+ return { ...r, verdict };
135
+ });
136
+ return { state: 'read', reviewers };
137
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.29.1",
3
+ "version": "3.31.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",