great-cto 2.82.2 → 2.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/board/.claude-plugin/plugin.json +2 -2
- package/board/packages/board/lib/alerts.mjs +7 -7
- package/board/packages/board/lib/config.mjs +1 -1
- package/board/packages/board/lib/notifications.mjs +10 -1
- package/board/packages/board/public/index.html +17 -20
- package/dist/update-check.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "great_cto",
|
|
3
3
|
"id": "great_cto",
|
|
4
4
|
"description": "Engineering process for solo founders and teams up to 50 engineers. Agents do architecture, code review, QA, and security. You make two decisions per feature.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.83.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Great CTO",
|
|
8
8
|
"url": "https://github.com/avelikiy/great_cto"
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"type": "command",
|
|
65
|
-
"command": "export PATH=\"/opt/homebrew/bin:$HOME/.local/bin:/usr/local/bin:$PATH\"; PLUGIN_DIR=$(ls -d ~/.claude/plugins/cache/local/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 ~/.claude/commands ~/.claude/agents ~/.great_cto .great_cto; for STALE in triage gates dora investigate threat-model sbom security-incident update status capture revisit board-report; do STALE_F=~/.claude/commands/${STALE}.md; [ -f \"$STALE_F\" ] && grep -q 'great_cto-managed' \"$STALE_F\" 2>/dev/null && rm -f \"$STALE_F\" 2>/dev/null; done; for CMD in start audit inbox board digest review recall ownership oncall rfc release doctor burn cost sec poc promote crystallize migrate resume save learn agent-review agent-retire help voice-compliance aedt-bias-audit api-contract-review discover gen-evals prd prompt-evolve skillify spec ccr exception trace coding-audit gov-metrics upl-check procurement-review close-review msp-review tax-review; do if cp \"${PLUGIN_DIR}/commands/${CMD}.md\" ~/.claude/commands/${CMD}.md 2>/dev/null; then grep -q 'great_cto-managed' ~/.claude/commands/${CMD}.md 2>/dev/null || echo '<!-- great_cto-managed -->' >> ~/.claude/commands/${CMD}.md; fi; done; for STALE_AGENT in tech-lead; do STALE_AF=~/.claude/agents/great_cto-${STALE_AGENT}.md; [ -f \"$STALE_AF\" ] && grep -q 'great_cto-managed' \"$STALE_AF\" 2>/dev/null && rm -f \"$STALE_AF\" 2>/dev/null; done; for AGENT in product-owner architect senior-dev code-reviewer qa-engineer e2e-test-engineer security-officer devops l3-support project-auditor ai-prompt-architect ai-eval-engineer ai-security-reviewer web-store-reviewer pci-reviewer oracle-reviewer firmware-reviewer regulated-reviewer performance-engineer db-migration-reviewer pm mobile-store-reviewer library-reviewer infra-reviewer cli-reviewer game-reviewer data-platform-reviewer devtools-reviewer enterprise-saas-reviewer mlops-reviewer streaming-reviewer marketplace-reviewer cms-reviewer continuous-learner edtech-reviewer gov-reviewer insurance-reviewer voice-ai-reviewer hr-ai-reviewer api-platform-reviewer gdpr-reviewer us-privacy-reviewer dpdpa-reviewer adtech-privacy-reviewer cmmc-reviewer us-ai-reviewer legal-reviewer rcm-reviewer procurement-reviewer accounting-reviewer msp-reviewer tax-reviewer coordinator; do if cp \"${PLUGIN_DIR}/agents/${AGENT}.md\" ~/.claude/agents/great_cto-${AGENT}.md 2>/dev/null; then grep -q 'great_cto-managed' ~/.claude/agents/great_cto-${AGENT}.md 2>/dev/null || echo '<!-- great_cto-managed -->' >> ~/.claude/agents/great_cto-${AGENT}.md; fi; done; 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 ==='; cat ~/.great_cto/preferences.md 2>/dev/null || 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; 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/local/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",
|
|
65
|
+
"command": "export PATH=\"/opt/homebrew/bin:$HOME/.local/bin:/usr/local/bin:$PATH\"; PLUGIN_DIR=$(ls -d ~/.claude/plugins/cache/local/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 ~/.claude/commands ~/.claude/agents ~/.great_cto .great_cto; for STALE in triage gates dora investigate threat-model sbom security-incident update status capture revisit board-report; do STALE_F=~/.claude/commands/${STALE}.md; [ -f \"$STALE_F\" ] && grep -q 'great_cto-managed' \"$STALE_F\" 2>/dev/null && rm -f \"$STALE_F\" 2>/dev/null; done; for CMD in start audit inbox board digest review recall ownership oncall rfc release doctor burn cost sec poc promote crystallize migrate resume save learn agent-review agent-retire help voice-compliance aedt-bias-audit api-contract-review discover gen-evals prd prompt-evolve skillify spec ccr exception trace coding-audit gov-metrics upl-check procurement-review close-review msp-review tax-review; do if cp \"${PLUGIN_DIR}/commands/${CMD}.md\" ~/.claude/commands/${CMD}.md 2>/dev/null; then grep -q 'great_cto-managed' ~/.claude/commands/${CMD}.md 2>/dev/null || echo '<!-- great_cto-managed -->' >> ~/.claude/commands/${CMD}.md; fi; done; for STALE_AGENT in tech-lead; do STALE_AF=~/.claude/agents/great_cto-${STALE_AGENT}.md; [ -f \"$STALE_AF\" ] && grep -q 'great_cto-managed' \"$STALE_AF\" 2>/dev/null && rm -f \"$STALE_AF\" 2>/dev/null; done; for AGENT in product-owner architect senior-dev code-reviewer qa-engineer e2e-test-engineer security-officer devops l3-support project-auditor ai-prompt-architect ai-eval-engineer ai-security-reviewer web-store-reviewer pci-reviewer oracle-reviewer firmware-reviewer regulated-reviewer performance-engineer db-migration-reviewer pm mobile-store-reviewer library-reviewer infra-reviewer cli-reviewer game-reviewer data-platform-reviewer devtools-reviewer enterprise-saas-reviewer mlops-reviewer streaming-reviewer marketplace-reviewer cms-reviewer continuous-learner edtech-reviewer gov-reviewer insurance-reviewer voice-ai-reviewer hr-ai-reviewer api-platform-reviewer gdpr-reviewer us-privacy-reviewer dpdpa-reviewer adtech-privacy-reviewer cmmc-reviewer us-ai-reviewer legal-reviewer rcm-reviewer procurement-reviewer accounting-reviewer msp-reviewer tax-reviewer coordinator; do if cp \"${PLUGIN_DIR}/agents/${AGENT}.md\" ~/.claude/agents/great_cto-${AGENT}.md 2>/dev/null; then grep -q 'great_cto-managed' ~/.claude/agents/great_cto-${AGENT}.md 2>/dev/null || echo '<!-- great_cto-managed -->' >> ~/.claude/agents/great_cto-${AGENT}.md; fi; done; 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 ==='; cat ~/.great_cto/preferences.md 2>/dev/null || 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/local/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",
|
|
66
66
|
"timeout": 30,
|
|
67
67
|
"statusMessage": "Loading Great CTO context..."
|
|
68
68
|
},
|
|
@@ -171,7 +171,7 @@ function startAlertCron() {
|
|
|
171
171
|
},
|
|
172
172
|
};
|
|
173
173
|
fireEmailAlert('incident.p0', dedupeKey, p0Payload);
|
|
174
|
-
addNotification('incident.p0', p0Payload);
|
|
174
|
+
addNotification('incident.p0', p0Payload, dedupeKey);
|
|
175
175
|
firePushAlert('incident.p0', dedupeKey, p0Payload);
|
|
176
176
|
}
|
|
177
177
|
}
|
|
@@ -207,7 +207,7 @@ function startAlertCron() {
|
|
|
207
207
|
},
|
|
208
208
|
};
|
|
209
209
|
fireEmailAlert('gate.blocked', dedupeKey, blockedPayload);
|
|
210
|
-
addNotification('gate.blocked', blockedPayload);
|
|
210
|
+
addNotification('gate.blocked', blockedPayload, dedupeKey);
|
|
211
211
|
firePushAlert('gate.blocked', dedupeKey, blockedPayload);
|
|
212
212
|
}
|
|
213
213
|
} catch (e) { log.warn('cron gate.blocked failed:', e.message); }
|
|
@@ -237,7 +237,7 @@ function startAlertCron() {
|
|
|
237
237
|
kv: { gate: g.id, agent: g.agent || 'unknown', age: `${ageHr.toFixed(1)}h` },
|
|
238
238
|
};
|
|
239
239
|
fireEmailAlert('gate.stale', dedupeKey, stalePayload);
|
|
240
|
-
addNotification('gate.stale', stalePayload);
|
|
240
|
+
addNotification('gate.stale', stalePayload, dedupeKey);
|
|
241
241
|
firePushAlert('gate.stale', dedupeKey, stalePayload);
|
|
242
242
|
}
|
|
243
243
|
}
|
|
@@ -276,7 +276,7 @@ function startAlertCron() {
|
|
|
276
276
|
},
|
|
277
277
|
};
|
|
278
278
|
fireEmailAlert('cost.threshold', dedupeKey, costPayload);
|
|
279
|
-
addNotification('cost.threshold', costPayload);
|
|
279
|
+
addNotification('cost.threshold', costPayload, dedupeKey);
|
|
280
280
|
firePushAlert('cost.threshold', dedupeKey, costPayload);
|
|
281
281
|
}
|
|
282
282
|
}
|
|
@@ -317,7 +317,7 @@ function startAlertCron() {
|
|
|
317
317
|
},
|
|
318
318
|
};
|
|
319
319
|
await fireEmailAlert('digest.weekly', dedupeKey, weeklyPayload);
|
|
320
|
-
addNotification('digest.weekly', weeklyPayload);
|
|
320
|
+
addNotification('digest.weekly', weeklyPayload, dedupeKey);
|
|
321
321
|
await firePushAlert('digest.weekly', dedupeKey, weeklyPayload);
|
|
322
322
|
}
|
|
323
323
|
} catch (e) { log.warn('cron digest.weekly failed:', e.message); }
|
|
@@ -384,7 +384,7 @@ function startAlertCron() {
|
|
|
384
384
|
kv: kvObj,
|
|
385
385
|
};
|
|
386
386
|
await fireEmailAlert('digest.daily', dedupeKey, dailyPayload);
|
|
387
|
-
addNotification('digest.daily', dailyPayload);
|
|
387
|
+
addNotification('digest.daily', dailyPayload, dedupeKey);
|
|
388
388
|
await firePushAlert('digest.daily', dedupeKey, dailyPayload);
|
|
389
389
|
}
|
|
390
390
|
} catch (e) { log.warn('cron digest.daily failed:', e.message); }
|
|
@@ -423,7 +423,7 @@ function startAlertCron() {
|
|
|
423
423
|
notify: (current, latest, dedupeKey) => {
|
|
424
424
|
const payload = buildUpdatePayload(current, latest);
|
|
425
425
|
fireEmailAlert('update.available', dedupeKey, payload);
|
|
426
|
-
addNotification('update.available', payload);
|
|
426
|
+
addNotification('update.available', payload, dedupeKey);
|
|
427
427
|
firePushAlert('update.available', dedupeKey, payload);
|
|
428
428
|
},
|
|
429
429
|
}).catch(e => log.warn('cron update.available failed:', e.message));
|
|
@@ -29,7 +29,7 @@ const PROJECTS_FILE = process.env.GREAT_CTO_PROJECTS_FILE || path.join(GREAT_CTO
|
|
|
29
29
|
const SHARE_ENDPOINT = 'https://greatcto.systems/r/';
|
|
30
30
|
const VAPID_KEYS_FILE = path.join(GREAT_CTO_DIR, 'vapid-keys.json');
|
|
31
31
|
const PUSH_SUBS_FILE = path.join(GREAT_CTO_DIR, 'push-subscriptions.json');
|
|
32
|
-
const NOTIF_HISTORY_FILE = path.join(GREAT_CTO_DIR, 'notif-history.json');
|
|
32
|
+
const NOTIF_HISTORY_FILE = process.env.GREAT_CTO_NOTIF_HISTORY_FILE || path.join(GREAT_CTO_DIR, 'notif-history.json');
|
|
33
33
|
const VAPID_SUBJECT = 'mailto:hi@updates.greatcto.systems';
|
|
34
34
|
|
|
35
35
|
export {
|
|
@@ -25,7 +25,15 @@ function saveNotifHistory() {
|
|
|
25
25
|
* Record a notification, broadcast via SSE, and persist.
|
|
26
26
|
* Called alongside fireEmailAlert / firePushAlert at every trigger point.
|
|
27
27
|
*/
|
|
28
|
-
function addNotification(event, payload) {
|
|
28
|
+
function addNotification(event, payload, dedupeKey = null) {
|
|
29
|
+
// Idempotent per dedupeKey: the alert crons tick every 5 min and a persistent
|
|
30
|
+
// condition (open P0, blocked/stale gate, cost over threshold, the daily
|
|
31
|
+
// digest during its whole UTC hour) would otherwise add a fresh in-app
|
|
32
|
+
// notification on every tick — spamming the bell. Email/push already dedupe
|
|
33
|
+
// via alerts-fired.json; this makes the in-app notification dedupe the same.
|
|
34
|
+
if (dedupeKey && notifHistory.some(n => n.dedupeKey === dedupeKey)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
29
37
|
const notif = {
|
|
30
38
|
id: crypto.randomUUID(),
|
|
31
39
|
event,
|
|
@@ -36,6 +44,7 @@ function addNotification(event, payload) {
|
|
|
36
44
|
project: payload.project || '',
|
|
37
45
|
ts: new Date().toISOString(),
|
|
38
46
|
read: false,
|
|
47
|
+
...(dedupeKey ? { dedupeKey } : {}),
|
|
39
48
|
};
|
|
40
49
|
notifHistory.unshift(notif);
|
|
41
50
|
if (notifHistory.length > MAX_NOTIF_HISTORY) notifHistory.length = MAX_NOTIF_HISTORY;
|
|
@@ -4039,14 +4039,18 @@ function renderDashboard(m) {
|
|
|
4039
4039
|
const avgMin = m.avg_completion_min ?? 0;
|
|
4040
4040
|
const cycleStat = m.cycle_time_stat === 'median_30d' ? 'Median cycle (30d)' : 'Avg cycle time';
|
|
4041
4041
|
|
|
4042
|
-
// Honest cost model
|
|
4043
|
-
//
|
|
4044
|
-
//
|
|
4045
|
-
//
|
|
4042
|
+
// Honest cost model — the backend is the source of truth:
|
|
4043
|
+
// cost.savings_x = MEASURED savings, only set when real verdict cost data
|
|
4044
|
+
// exists. NULL for the time-based (tasks) estimate, because
|
|
4045
|
+
// that ratio is just the hardcoded rate ratio, not a real
|
|
4046
|
+
// measurement — showing it would fabricate a number.
|
|
4047
|
+
// cost.human_usd = the backend's conservative human-cost estimate.
|
|
4048
|
+
// The frontend must NOT recompute its own human/savings figures (that produced
|
|
4049
|
+
// an inflated "×" that contradicted the API); it displays the backend fields.
|
|
4046
4050
|
const aiSpend = realLlmUsd > 0 ? realLlmUsd : llmUsd;
|
|
4047
|
-
const humanEst =
|
|
4048
|
-
const savingsX = (
|
|
4049
|
-
?
|
|
4051
|
+
const humanEst = m.cost?.human_usd ?? 0;
|
|
4052
|
+
const savingsX = (typeof planSavingsX === 'number' && planSavingsX > 1)
|
|
4053
|
+
? planSavingsX
|
|
4050
4054
|
: null;
|
|
4051
4055
|
|
|
4052
4056
|
const llmTrend = realLlmUsd > 0
|
|
@@ -4067,28 +4071,21 @@ function renderDashboard(m) {
|
|
|
4067
4071
|
v: savingsX.toLocaleString(),
|
|
4068
4072
|
sub: '×',
|
|
4069
4073
|
label: 'Cost savings vs FTE',
|
|
4070
|
-
trend: `human est. $${humanEst
|
|
4074
|
+
trend: humanEst > 0 ? `measured · human est. $${fmtMoney(humanEst)}` : 'measured savings',
|
|
4071
4075
|
cls: 'green',
|
|
4072
4076
|
}
|
|
4073
4077
|
: {
|
|
4078
|
+
// No MEASURED savings yet (no verdict cost data). Don't fabricate a
|
|
4079
|
+
// multiplier from the time-based estimate — show it's not measured.
|
|
4074
4080
|
v: '—',
|
|
4075
4081
|
sub: '',
|
|
4076
4082
|
label: 'Cost savings vs FTE',
|
|
4077
|
-
trend:
|
|
4083
|
+
trend: aiSpend > 0
|
|
4084
|
+
? 'estimate only · run pipelines for measured savings'
|
|
4085
|
+
: (done > 0 ? 'no spend recorded yet' : 'no tasks done yet'),
|
|
4078
4086
|
cls: '',
|
|
4079
4087
|
},
|
|
4080
4088
|
];
|
|
4081
|
-
|
|
4082
|
-
// Prefer planSavingsX when available (from PLAN-*.md) — explicit override
|
|
4083
|
-
if (planSavingsX != null && planSavingsX > 0) {
|
|
4084
|
-
hero[2] = {
|
|
4085
|
-
v: planSavingsX,
|
|
4086
|
-
sub: '×',
|
|
4087
|
-
label: 'Cost savings vs FTE',
|
|
4088
|
-
trend: `from PLAN-*.md figures`,
|
|
4089
|
-
cls: 'green',
|
|
4090
|
-
};
|
|
4091
|
-
}
|
|
4092
4089
|
document.getElementById('mp-hero').innerHTML = hero.map(s => `
|
|
4093
4090
|
<div class="metric-card ${s.cls || ''}">
|
|
4094
4091
|
<div class="metric-num">${s.v}${s.sub ? `<small>${s.sub}</small>` : ''}</div>
|
package/dist/update-check.js
CHANGED
|
@@ -134,7 +134,7 @@ export function isCacheFresh(cache, now = Date.now(), freshMs = CACHE_FRESH_MS)
|
|
|
134
134
|
const checkedAtMs = Date.parse(cache.checkedAt);
|
|
135
135
|
if (Number.isNaN(checkedAtMs))
|
|
136
136
|
return false;
|
|
137
|
-
return now - checkedAtMs
|
|
137
|
+
return now - checkedAtMs <= freshMs;
|
|
138
138
|
}
|
|
139
139
|
/** Pure function: does `latest` represent a newer version than `current`? */
|
|
140
140
|
export function isNewerVersion(current, latest) {
|
package/package.json
CHANGED