@hegemonart/get-design-done 1.57.1 → 1.57.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.claude-plugin/marketplace.json +26 -41
  2. package/.claude-plugin/plugin.json +23 -48
  3. package/CHANGELOG.md +91 -0
  4. package/README.md +166 -511
  5. package/SKILL.md +2 -0
  6. package/agents/README.md +33 -36
  7. package/agents/a11y-mapper.md +3 -3
  8. package/agents/component-benchmark-harvester.md +6 -6
  9. package/agents/component-benchmark-synthesizer.md +3 -3
  10. package/agents/compose-executor.md +3 -3
  11. package/agents/cost-forecaster.md +2 -2
  12. package/agents/design-auditor.md +7 -7
  13. package/agents/design-authority-watcher.md +15 -15
  14. package/agents/design-context-builder.md +4 -4
  15. package/agents/design-context-checker-gate.md +1 -1
  16. package/agents/design-discussant.md +2 -2
  17. package/agents/design-doc-writer.md +1 -1
  18. package/agents/design-executor.md +2 -2
  19. package/agents/design-figma-writer.md +2 -2
  20. package/agents/design-fixer.md +7 -7
  21. package/agents/design-integration-checker-gate.md +1 -1
  22. package/agents/design-integration-checker.md +1 -1
  23. package/agents/design-paper-writer.md +3 -3
  24. package/agents/design-pencil-writer.md +1 -1
  25. package/agents/design-planner.md +21 -0
  26. package/agents/design-reflector.md +39 -39
  27. package/agents/design-research-synthesizer.md +1 -0
  28. package/agents/design-start-writer.md +1 -1
  29. package/agents/design-update-checker.md +5 -5
  30. package/agents/design-verifier-gate.md +1 -1
  31. package/agents/design-verifier.md +52 -48
  32. package/agents/ds-generator.md +2 -2
  33. package/agents/ds-migration-planner.md +4 -4
  34. package/agents/email-executor.md +9 -9
  35. package/agents/experiment-result-ingester.md +3 -3
  36. package/agents/flutter-executor.md +5 -5
  37. package/agents/gdd-graph-refresh.md +3 -3
  38. package/agents/gdd-intel-updater.md +2 -2
  39. package/agents/motion-mapper.md +2 -2
  40. package/agents/motion-verifier.md +4 -4
  41. package/agents/pdf-executor.md +8 -8
  42. package/agents/perf-analyzer.md +17 -17
  43. package/agents/pr-commenter.md +9 -9
  44. package/agents/prototype-gate.md +2 -2
  45. package/agents/quality-gate-runner.md +1 -1
  46. package/agents/rollout-coordinator.md +3 -3
  47. package/agents/swift-executor.md +4 -4
  48. package/agents/ticket-sync-agent.md +6 -6
  49. package/agents/user-research-synthesizer.md +2 -2
  50. package/connections/connections.md +44 -45
  51. package/connections/cursor.md +73 -0
  52. package/connections/preview.md +3 -3
  53. package/dist/claude-code/.claude/skills/cache-manager/SKILL.md +3 -3
  54. package/dist/claude-code/.claude/skills/cache-manager/cache-policy.md +1 -1
  55. package/dist/claude-code/.claude/skills/design/SKILL.md +19 -0
  56. package/dist/claude-code/.claude/skills/explore/SKILL.md +11 -0
  57. package/dist/claude-code/.claude/skills/figma-write/SKILL.md +13 -2
  58. package/dist/claude-code/.claude/skills/paper-write/SKILL.md +54 -0
  59. package/dist/claude-code/.claude/skills/pencil-write/SKILL.md +54 -0
  60. package/dist/claude-code/.claude/skills/report-issue/SKILL.md +2 -2
  61. package/dist/claude-code/.claude/skills/router/SKILL.md +2 -2
  62. package/dist/claude-code/.claude/skills/verify/verify-procedure.md +10 -11
  63. package/dist/claude-code/.claude/skills/warm-cache/SKILL.md +1 -1
  64. package/hooks/first-run-nudge.cjs +171 -0
  65. package/hooks/gdd-intel-trigger.js +243 -0
  66. package/hooks/gdd-mcp-circuit-breaker.js +62 -7
  67. package/hooks/gdd-precompact-snapshot.js +50 -29
  68. package/hooks/gdd-protected-paths.js +150 -18
  69. package/hooks/gdd-risk-gate.js +93 -1
  70. package/hooks/gdd-sessionstart-recap.js +59 -24
  71. package/hooks/hooks.json +13 -4
  72. package/hooks/inject-using-gdd.cjs +188 -0
  73. package/hooks/update-check.cjs +511 -0
  74. package/package.json +9 -2
  75. package/reference/STATE-TEMPLATE.md +10 -13
  76. package/reference/audit-scoring.md +1 -1
  77. package/reference/cache-tier-doctrine.md +46 -0
  78. package/reference/config-schema.md +9 -9
  79. package/reference/i18n.md +1 -1
  80. package/reference/intel-schema.md +37 -2
  81. package/reference/meta-rules.md +4 -4
  82. package/reference/model-tiers.md +2 -2
  83. package/reference/registry.json +101 -94
  84. package/reference/runtime-models.md +11 -1
  85. package/reference/shared-preamble.md +13 -14
  86. package/reference/skill-graph.md +24 -1
  87. package/scripts/bootstrap.cjs +373 -0
  88. package/scripts/injection-patterns.cjs +58 -0
  89. package/scripts/lib/apply-reflections/incubator-proposals.cjs +57 -26
  90. package/scripts/lib/install/converters/codex-plugin.cjs +5 -2
  91. package/scripts/lib/install/converters/cursor.cjs +20 -0
  92. package/scripts/lib/issue-reporter/report-flow.cjs +1 -1
  93. package/scripts/lib/manifest/skills.json +80 -13
  94. package/scripts/lib/state/query-surface.cjs +67 -9
  95. package/scripts/lib/state/state-store.cjs +68 -26
  96. package/sdk/cli/commands/stage.ts +17 -0
  97. package/sdk/cli/index.js +14 -0
  98. package/skills/cache-manager/SKILL.md +3 -3
  99. package/skills/cache-manager/cache-policy.md +1 -1
  100. package/skills/design/SKILL.md +19 -0
  101. package/skills/explore/SKILL.md +11 -0
  102. package/skills/figma-write/SKILL.md +13 -2
  103. package/skills/paper-write/SKILL.md +54 -0
  104. package/skills/pencil-write/SKILL.md +54 -0
  105. package/skills/report-issue/SKILL.md +2 -2
  106. package/skills/router/SKILL.md +2 -2
  107. package/skills/verify/verify-procedure.md +10 -11
  108. package/skills/warm-cache/SKILL.md +1 -1
  109. package/hooks/first-run-nudge.sh +0 -82
  110. package/hooks/inject-using-gdd.sh +0 -72
  111. package/hooks/update-check.sh +0 -251
  112. package/scripts/lib/audit-aggregator/index.cjs +0 -219
  113. package/scripts/lib/hedge-ensemble.cjs +0 -217
@@ -1,82 +0,0 @@
1
- #!/usr/bin/env bash
2
- # get-design-done — first-run nudge (Phase 14.7)
3
- # SessionStart hook. Silent-on-failure by policy: exits 0 on every error path.
4
- # Prints exactly one restrained line pointing at /gdd:start when all gates pass,
5
- # and nothing otherwise.
6
-
7
- set -u # intentionally no -e: we want to fall through to exit 0
8
-
9
- # Silent logger — writes nothing by default. Set GDD_NUDGE_DEBUG=1 to enable stderr.
10
- log() {
11
- if [ "${GDD_NUDGE_DEBUG:-0}" = "1" ]; then
12
- printf '[gdd first-run-nudge] %s\n' "$*" >&2
13
- fi
14
- }
15
-
16
- DESIGN_DIR="$(pwd)/.design"
17
- STATE="${DESIGN_DIR}/STATE.md"
18
- CONFIG="${DESIGN_DIR}/config.json"
19
- DISMISS_FLAG="${HOME:-$USERPROFILE}/.claude/gdd-nudge-dismissed"
20
-
21
- # Gate 1 — repo already has GDD state, suppress.
22
- has_design_state() {
23
- [ -f "${CONFIG}" ] || [ -f "${STATE}" ]
24
- }
25
-
26
- # Gate 2 — per-install dismissal flag.
27
- is_dismissed() {
28
- [ -f "${DISMISS_FLAG}" ]
29
- }
30
-
31
- # Gate 3 — STATE.md stage belongs to an active pipeline window.
32
- # Inherits the shape used by Phase 13.3 update-check.sh.
33
- read_state_stage() {
34
- [ -f "${STATE}" ] || { printf ''; return; }
35
- grep -E '^stage:' "${STATE}" 2>/dev/null | head -n1 | \
36
- sed -E 's/^stage:[[:space:]]*"?([^"[:space:]]+)"?.*/\1/'
37
- }
38
-
39
- is_active_stage() {
40
- local s
41
- s="$(read_state_stage)"
42
- case "${s}" in
43
- plan|design|verify|executing|discussing) return 0 ;;
44
- *) return 1 ;;
45
- esac
46
- }
47
-
48
- # Gate 4 — recent session history has a gdd:* command. We cannot reliably read
49
- # session history from a hook in all runtimes; when the signal is unavailable,
50
- # treat it as "unknown → not suppressed". This preserves the nudge's
51
- # usefulness without creating false suppression.
52
- has_recent_gdd_command() {
53
- # Placeholder: no portable transcript path exposed to SessionStart hooks today.
54
- # Keep the function for future wiring; for now always returns non-zero (unknown).
55
- return 1
56
- }
57
-
58
- # MANDATORY sourcing guard: unit tests source this script to test the helper
59
- # functions without executing the main flow. Non-negotiable.
60
- if [ "${BASH_SOURCE[0]}" = "$0" ]; then
61
- if has_design_state; then
62
- log "design state present — suppress"
63
- exit 0
64
- fi
65
- if is_dismissed; then
66
- log "dismissal flag present — suppress"
67
- exit 0
68
- fi
69
- if is_active_stage; then
70
- log "active stage — suppress"
71
- exit 0
72
- fi
73
- if has_recent_gdd_command; then
74
- log "recent gdd:* command detected — suppress"
75
- exit 0
76
- fi
77
- # All gates passed — emit the locked one-line nudge.
78
- printf 'Tip: run /gdd:start to let GDD inspect this codebase and suggest one first fix.\n'
79
- exit 0
80
- fi
81
- # When sourced (BASH_SOURCE != $0), fall through with function definitions loaded
82
- # and without side effects.
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env bash
2
- # hooks/inject-using-gdd.sh — SessionStart per-harness context injector (D-07).
3
- #
4
- # The forcing function GDD lacked: on every session start / /clear / compact this
5
- # reads skills/using-gdd/SKILL.md (the bootstrap discipline contract) and emits it
6
- # as the host harness's SessionStart "additionalContext" shape so the agent is
7
- # primed with the 1%-rule + red-flags + skill-priority before it acts.
8
- #
9
- # Ported MECHANISM (not content) from obra/superpowers (MIT): one polyglot script,
10
- # env-var branch, pure-bash escape_for_json (no jq/python dependency). See NOTICE.
11
- #
12
- # Three emitted shapes (ONE JSON object on stdout, nothing else):
13
- # Cursor (CURSOR_PLUGIN_ROOT set) -> {"additional_context": "<escaped>"}
14
- # Claude Code (CLAUDE_PLUGIN_ROOT set, no Cursor)
15
- # -> {"hookSpecificOutput":
16
- # {"hookEventName":"SessionStart",
17
- # "additionalContext":"<escaped>"}}
18
- # SDK-standard (neither; e.g. COPILOT_CLI) -> {"additionalContext": "<escaped>"}
19
- #
20
- # Branch order: check Cursor BEFORE Claude Code — a Cursor session may also export
21
- # CLAUDE_PLUGIN_ROOT, and Cursor's own var must win.
22
- #
23
- # NO-CASCADE (D-06): this script is wired ONLY under the SessionStart hook event in
24
- # hooks/hooks.json. Subagent spawns do not fire SessionStart, so the inject cannot
25
- # cascade into a subagent's context. (Structural guarantee; behavioral proof = P33.)
26
-
27
- set -u
28
-
29
- # --- Resolve the plugin root so we can locate skills/using-gdd/SKILL.md ---------
30
- # Prefer the harness-provided roots; fall back to this script's parent dir so the
31
- # emitter is runnable straight from hooks/ in tests and in bare shells.
32
- SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
33
- ROOT="${CURSOR_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-${SELF_DIR}/..}}"
34
- ROOT="${ROOT//\\//}" # normalize Windows backslashes to forward slashes
35
- SKILL="${ROOT}/skills/using-gdd/SKILL.md"
36
-
37
- # Defensive: if the skill file is missing we must STILL emit a syntactically valid
38
- # JSON object (an empty additionalContext) so the SessionStart pipeline never
39
- # breaks on a partial install. Never crash the session start.
40
- if [[ -r "${SKILL}" ]]; then
41
- CONTENT="$(cat "${SKILL}")"
42
- else
43
- CONTENT=""
44
- fi
45
-
46
- # --- escape_for_json (superpowers pattern; pure bash param-substitution) --------
47
- # Order matters: backslash FIRST (so escapes we add next aren't re-escaped), then
48
- # double-quote, then the control chars newline / tab / carriage-return. Emits the
49
- # value WITH surrounding double-quotes so callers can splice it directly.
50
- escape_for_json() {
51
- local s="$1"
52
- s="${s//\\/\\\\}" # \ -> \\
53
- s="${s//\"/\\\"}" # " -> \"
54
- s="${s//$'\t'/\\t}" # tab -> \t
55
- s="${s//$'\r'/\\r}" # CR -> \r
56
- s="${s//$'\n'/\\n}" # LF -> \n (do last: newlines are the record separator)
57
- printf '"%s"' "$s"
58
- }
59
-
60
- ESCAPED="$(escape_for_json "${CONTENT}")"
61
-
62
- # --- Branch on harness env vars and emit the matching single JSON object --------
63
- if [[ -n "${CURSOR_PLUGIN_ROOT:-}" ]]; then
64
- # Cursor: top-level additional_context.
65
- printf '{"additional_context": %s}\n' "${ESCAPED}"
66
- elif [[ -n "${CLAUDE_PLUGIN_ROOT:-}" ]]; then
67
- # Claude Code: hookSpecificOutput envelope (mirrors hooks/gdd-decision-injector.js).
68
- printf '{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": %s}}\n' "${ESCAPED}"
69
- else
70
- # SDK-standard (COPILOT_CLI or none): top-level additionalContext.
71
- printf '{"additionalContext": %s}\n' "${ESCAPED}"
72
- fi
@@ -1,251 +0,0 @@
1
- #!/usr/bin/env bash
2
- # get-design-done — update check (Phase 13.3)
3
- # SessionStart hook. Silent-on-failure by policy (D-04): exits 0 on every error path.
4
- # 24h-cached unauthenticated GET of /releases/latest. Renders .design/update-available.md
5
- # only when a newer version exists AND it is not dismissed AND stage-guard allows.
6
-
7
- set -u # intentionally no -e: we want to fall through to exit 0
8
-
9
- PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
10
- PLUGIN_ROOT="${PLUGIN_ROOT//\\//}" # Windows → POSIX slashes
11
-
12
- DESIGN_DIR="$(pwd)/.design"
13
- CACHE="${DESIGN_DIR}/update-cache.json"
14
- BANNER="${DESIGN_DIR}/update-available.md"
15
- CONFIG="${DESIGN_DIR}/config.json"
16
- STATE="${DESIGN_DIR}/STATE.md"
17
- CACHE_TTL_SECONDS=86400 # 24h
18
-
19
- # Silent logger — writes nothing by default. Set GDD_UPDATE_DEBUG=1 to enable stderr.
20
- log() {
21
- if [ "${GDD_UPDATE_DEBUG:-0}" = "1" ]; then
22
- printf '[gdd update-check] %s\n' "$*" >&2
23
- fi
24
- }
25
-
26
- # Ensure .design/ exists (bootstrap normally creates it; belt+suspenders).
27
- mkdir -p "${DESIGN_DIR}" 2>/dev/null || exit 0
28
-
29
- # ---- Read current plugin version (no jq) ----
30
- PLUGIN_JSON="${PLUGIN_ROOT}/.claude-plugin/plugin.json"
31
-
32
- read_current_tag() {
33
- [ -f "${PLUGIN_JSON}" ] || return 1
34
- grep -E '^[[:space:]]*"version"[[:space:]]*:' "${PLUGIN_JSON}" | head -n1 | \
35
- sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/'
36
- }
37
-
38
- # ---- Semver normalizer: "v1.0.7" -> "1 0 7 0"; "v1.0.7.3" -> "1 0 7 3" ----
39
- normalize_semver() {
40
- local t="${1#v}"
41
- # strip any -pre/-beta suffix after first hyphen (unauth'd API rarely surfaces these, best-effort)
42
- t="${t%%-*}"
43
- # Replace dots with spaces; pad to 4 segments
44
- # shellcheck disable=SC2086
45
- set -- $(printf '%s' "${t}" | tr '.' ' ')
46
- local a="${1:-0}" b="${2:-0}" c="${3:-0}" d="${4:-0}"
47
- # Sanitize to digits only (POSIX: tr -cd 0-9 — BSD+GNU safe)
48
- a="$(printf '%s' "$a" | tr -cd '0-9')"; a="${a:-0}"
49
- b="$(printf '%s' "$b" | tr -cd '0-9')"; b="${b:-0}"
50
- c="$(printf '%s' "$c" | tr -cd '0-9')"; c="${c:-0}"
51
- d="$(printf '%s' "$d" | tr -cd '0-9')"; d="${d:-0}"
52
- printf '%s %s %s %s' "$a" "$b" "$c" "$d"
53
- }
54
-
55
- # ---- Classify delta: compare 4-segment tuples ----
56
- # Args: current_tag latest_tag
57
- # Prints: "newer|same|older|invalid" + "major|minor|patch|off-cadence|none"
58
- classify_delta() {
59
- local cur lat
60
- cur="$(normalize_semver "$1")" || { printf 'invalid none'; return; }
61
- lat="$(normalize_semver "$2")" || { printf 'invalid none'; return; }
62
- # shellcheck disable=SC2086
63
- set -- $cur; local ca="$1" cb="$2" cc="$3" cd="$4"
64
- # shellcheck disable=SC2086
65
- set -- $lat; local la="$1" lb="$2" lc="$3" ld="$4"
66
-
67
- # Per-segment integer compare (lexicographic per segment by numeric value)
68
- if [ "$la" -gt "$ca" ]; then printf 'newer major'; return
69
- elif [ "$la" -lt "$ca" ]; then printf 'older major'; return
70
- fi
71
- if [ "$lb" -gt "$cb" ]; then printf 'newer minor'; return
72
- elif [ "$lb" -lt "$cb" ]; then printf 'older minor'; return
73
- fi
74
- if [ "$lc" -gt "$cc" ]; then printf 'newer patch'; return
75
- elif [ "$lc" -lt "$cc" ]; then printf 'older patch'; return
76
- fi
77
- if [ "$ld" -gt "$cd" ]; then printf 'newer off-cadence'; return
78
- elif [ "$ld" -lt "$cd" ]; then printf 'older off-cadence'; return
79
- fi
80
- printf 'same none'
81
- }
82
-
83
- # ---- Cache freshness check: returns 0 if fresh (<24h old), 1 if stale or missing ----
84
- is_cache_fresh() {
85
- [ -f "${CACHE}" ] || return 1
86
- local now mtime age
87
- now="$(date +%s)"
88
- # BSD date -r on macOS; GNU stat -c on Linux; fall back to perl then python.
89
- if mtime="$(date -r "${CACHE}" +%s 2>/dev/null)"; then :
90
- elif mtime="$(stat -c %Y "${CACHE}" 2>/dev/null)"; then :
91
- elif mtime="$(perl -e 'print((stat shift)[9])' "${CACHE}" 2>/dev/null)"; then :
92
- else return 1
93
- fi
94
- [ -n "${mtime:-}" ] || return 1
95
- age=$((now - mtime))
96
- [ "${age}" -lt "${CACHE_TTL_SECONDS}" ]
97
- }
98
-
99
- # ---- Fetch latest release. Writes raw body to stdout on success, nothing on failure. ----
100
- fetch_latest() {
101
- command -v curl >/dev/null 2>&1 || { log "no curl"; return 1; }
102
- local url="https://api.github.com/repos/hegemonart/get-design-done/releases/latest"
103
- curl -sf --max-time 3 -H 'Accept: application/vnd.github+json' "${url}" 2>/dev/null || return 1
104
- }
105
-
106
- # ---- Extract fields from the release JSON (no jq). Robust to whitespace; fails soft. ----
107
- extract_tag() {
108
- grep -E '"tag_name"[[:space:]]*:' | head -n1 | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/'
109
- }
110
- # Body extraction: python3-only. If python3 is absent, we intentionally return empty
111
- # (D-04 silent-on-failure posture). No awk/sed fallback — JSON string decoding in pure
112
- # bash is fragile and untested; empty excerpt is the correct degraded state.
113
- extract_body() {
114
- command -v python3 >/dev/null 2>&1 || return 0
115
- python3 -c 'import json,sys
116
- try:
117
- d=json.load(sys.stdin)
118
- b=d.get("body","") or ""
119
- print(b[:500])
120
- except Exception:
121
- pass' 2>/dev/null
122
- }
123
-
124
- # ---- Read .design/STATE.md stage field. Returns "brief"|"explore"|"plan"|"design"|"verify"|"" ----
125
- # Schema source: reference/STATE-TEMPLATE.md — `stage:` lives in both the frontmatter
126
- # and the <position> block with identical values per the write contract. We take the
127
- # first occurrence (head -n1), which is the frontmatter line.
128
- read_state_stage() {
129
- [ -f "${STATE}" ] || { printf ''; return; }
130
- grep -E '^stage:' "${STATE}" 2>/dev/null | head -n1 | sed -E 's/^stage:[[:space:]]*"?([^"[:space:]]+)"?.*/\1/'
131
- }
132
-
133
- # ---- Read .design/config.json for update_dismissed. Returns tag or empty. ----
134
- read_dismissed() {
135
- [ -f "${CONFIG}" ] || { printf ''; return; }
136
- grep -E '"update_dismissed"[[:space:]]*:' "${CONFIG}" 2>/dev/null | head -n1 | \
137
- sed -E 's/.*"update_dismissed"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/'
138
- }
139
-
140
- # ---- Main control flow ----
141
- # MANDATORY sourcing guard: wrap the entire main flow so that `source update-check.sh`
142
- # (used by unit tests and interactive debugging) loads the function definitions without
143
- # executing steps 1-6 and exiting the sourcing shell. This is non-negotiable — the
144
- # semver self-test acceptance criterion sources this script.
145
- if [ "${BASH_SOURCE[0]}" = "$0" ]; then
146
-
147
- CURRENT_TAG="$(read_current_tag)" || { log "no plugin.json"; exit 0; }
148
- [ -n "${CURRENT_TAG:-}" ] || { log "no current version parsed"; exit 0; }
149
- # Normalize to "vX.Y.Z" shape for display (plugin.json stores bare "1.0.7")
150
- DISPLAY_CURRENT="v${CURRENT_TAG#v}"
151
-
152
- # Optional --refresh forces a fresh fetch (called by plan 13.3-04's /gdd:check-update --refresh).
153
- FORCE_REFRESH=0
154
- for arg in "$@"; do
155
- case "$arg" in
156
- --refresh) FORCE_REFRESH=1 ;;
157
- esac
158
- done
159
-
160
- # 1. Populate cache if missing/stale or forced.
161
- if [ "${FORCE_REFRESH}" -eq 1 ] || ! is_cache_fresh; then
162
- RAW="$(fetch_latest)" || RAW=""
163
- if [ -n "${RAW}" ]; then
164
- LATEST_TAG="$(printf '%s' "${RAW}" | extract_tag)"
165
- BODY_EXCERPT="$(printf '%s' "${RAW}" | extract_body)"
166
- # Strip control chars defensively (T-13.3-03)
167
- BODY_EXCERPT="$(printf '%s' "${BODY_EXCERPT}" | tr -d '\000-\010\013\014\016-\037')"
168
- # Strip double-quotes so the JSON round-trip sed read-back cannot be injected via a
169
- # crafted release body. Body is display-only — losing quotes is acceptable.
170
- BODY_EXCERPT="$(printf '%s' "${BODY_EXCERPT}" | tr -d '"')"
171
- # Validate LATEST_TAG is a safe semver string before trusting it (CR-02).
172
- if ! printf '%s' "${LATEST_TAG}" | grep -qE '^v?[0-9]+\.[0-9]+(\.[0-9]+)*$'; then
173
- log "LATEST_TAG '${LATEST_TAG}' failed semver safety check — aborting cache write"
174
- LATEST_TAG=""
175
- fi
176
- if [ -n "${LATEST_TAG}" ]; then
177
- read -r DELTA_STATE DELTA_KIND <<EOF
178
- $(classify_delta "${DISPLAY_CURRENT}" "${LATEST_TAG}")
179
- EOF
180
- IS_NEWER=false
181
- [ "${DELTA_STATE}" = "newer" ] && IS_NEWER=true
182
- CHECKED_AT="$(date +%s)"
183
- # Write cache atomically (write-to-tmp + rename) — T-13.3-04 mitigation
184
- TMP="${CACHE}.tmp.$$"
185
- {
186
- printf '{\n'
187
- printf ' "checked_at": %s,\n' "${CHECKED_AT}"
188
- printf ' "current_tag": "%s",\n' "${DISPLAY_CURRENT}"
189
- printf ' "latest_tag": "%s",\n' "${LATEST_TAG}"
190
- printf ' "delta": "%s",\n' "${DELTA_KIND}"
191
- printf ' "is_newer": %s,\n' "${IS_NEWER}"
192
- # Escape the body for JSON — backslashes first, then quotes, then newlines.
193
- ESC="$(printf '%s' "${BODY_EXCERPT}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | awk '{printf "%s\\n", $0}')"
194
- printf ' "changelog_excerpt": "%s"\n' "${ESC}"
195
- printf '}\n'
196
- } > "${TMP}" 2>/dev/null && mv "${TMP}" "${CACHE}" 2>/dev/null || rm -f "${TMP}" 2>/dev/null
197
- fi
198
- fi
199
- fi
200
-
201
- # 2. Read cache (whether freshly written or still valid).
202
- [ -f "${CACHE}" ] || exit 0 # no cache, nothing to do — silent exit
203
-
204
- C_LATEST="$(grep -E '"latest_tag"' "${CACHE}" 2>/dev/null | head -n1 | sed -E 's/.*"latest_tag"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"
205
- C_DELTA="$(grep -E '"delta"' "${CACHE}" 2>/dev/null | head -n1 | sed -E 's/.*"delta"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"
206
- # Allowlist-gate C_DELTA before it reaches any shell context (WR-04).
207
- case "${C_DELTA:-}" in
208
- major|minor|patch|off-cadence|none) : ;;
209
- *) C_DELTA="unknown" ;;
210
- esac
211
- C_NEWER="$(grep -E '"is_newer"' "${CACHE}" 2>/dev/null | head -n1 | sed -E 's/.*"is_newer"[[:space:]]*:[[:space:]]*(true|false).*/\1/')"
212
- C_BODY="$(grep -E '"changelog_excerpt"' "${CACHE}" 2>/dev/null | head -n1 | sed -E 's/.*"changelog_excerpt"[[:space:]]*:[[:space:]]*"(.*)".*/\1/' | sed -E 's/\\n/\n/g')"
213
-
214
- # 3. Gate: if cache says not newer, remove any stale banner and exit.
215
- if [ "${C_NEWER:-false}" != "true" ]; then
216
- rm -f "${BANNER}" 2>/dev/null
217
- exit 0
218
- fi
219
-
220
- # 4. Dismissal gate (D-13): if user already dismissed this exact tag, suppress.
221
- DISMISSED="$(read_dismissed)"
222
- if [ -n "${DISMISSED}" ] && [ "${DISMISSED}" = "${C_LATEST}" ]; then
223
- rm -f "${BANNER}" 2>/dev/null
224
- exit 0
225
- fi
226
-
227
- # 5. State-machine guard (D-11/D-12): suppress during plan|design|verify.
228
- STAGE="$(read_state_stage)"
229
- case "${STAGE}" in
230
- plan|design|verify)
231
- rm -f "${BANNER}" 2>/dev/null
232
- exit 0
233
- ;;
234
- esac
235
-
236
- # 6. All gates passed — render the banner atomically.
237
- TMP="${BANNER}.tmp.$$"
238
- {
239
- printf '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'
240
- printf ' 📦 Plugin update: %s → %s (%s)\n' "${DISPLAY_CURRENT}" "${C_LATEST}" "${C_DELTA}"
241
- if [ -n "${C_BODY}" ]; then
242
- printf '%s\n' "${C_BODY}"
243
- fi
244
- printf ' Install: /gdd:update Dismiss: /gdd:check-update --dismiss\n'
245
- printf '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'
246
- } > "${TMP}" 2>/dev/null && mv "${TMP}" "${BANNER}" 2>/dev/null || rm -f "${TMP}" 2>/dev/null
247
-
248
- exit 0
249
- fi
250
- # When sourced (BASH_SOURCE != $0), fall through with function definitions loaded
251
- # and without side effects. Sourcing callers must invoke functions explicitly.
@@ -1,219 +0,0 @@
1
- /**
2
- * audit-aggregator/index.cjs — dedup + score + rank findings from N
3
- * audit-agents (Plan 23-04).
4
- *
5
- * Replaces the prompt-only "trust the agent's score" pattern with a
6
- * deterministic scoring + dedup function that downstream tooling
7
- * (`/gdd:audit`, `/gdd:reflect`) can rely on.
8
- *
9
- * Dedup key: `${lowercased(normalizePath(file))}::${line ?? 0}::${rule_id}`.
10
- * Survivor selection on collision:
11
- * 1. higher confidence wins
12
- * 2. tie → higher severity (P0 > P1 > P2 > P3)
13
- * 3. tie → lexicographically earliest agent
14
- * 4. tie → first-seen
15
- *
16
- * Score = severityWeight(severity) * confidence.
17
- *
18
- * No external deps. CommonJS to match the rest of scripts/lib/.
19
- */
20
-
21
- 'use strict';
22
-
23
- const SEVERITY_RANK = { P0: 4, P1: 3, P2: 2, P3: 1 };
24
- const DEFAULT_WEIGHTS = Object.freeze({ P0: 8, P1: 4, P2: 2, P3: 1 });
25
-
26
- /**
27
- * @typedef {Object} Finding
28
- * @property {string} file
29
- * @property {number} [line]
30
- * @property {string} rule_id
31
- * @property {'P0'|'P1'|'P2'|'P3'} severity
32
- * @property {string} summary
33
- * @property {string} [evidence]
34
- * @property {string} [agent]
35
- * @property {number} [confidence]
36
- * @property {string[]} [merged_from]
37
- */
38
-
39
- /**
40
- * @typedef {Object} AggregateResult
41
- * @property {Finding[]} findings
42
- * @property {Object<string, number>} byRule
43
- * @property {Object<string, number>} bySeverity
44
- * @property {Object<string, number>} byFile
45
- * @property {number} total
46
- * @property {number} duplicates
47
- */
48
-
49
- /**
50
- * @typedef {Object} AggregateOptions
51
- * @property {number} [topN]
52
- * @property {Object<string, number>} [severityWeights]
53
- * @property {(a: Finding, b: Finding) => Finding} [merge]
54
- */
55
-
56
- function normalizePath(p) {
57
- return String(p).replace(/\\/g, '/').toLowerCase();
58
- }
59
-
60
- let _confidenceWarningEmitted = false;
61
-
62
- function clampConfidence(c) {
63
- if (c === undefined || c === null) return 1;
64
- if (typeof c !== 'number' || Number.isNaN(c)) return 1;
65
- if (c < 0) {
66
- if (!_confidenceWarningEmitted) {
67
- process.emitWarning('audit-aggregator: confidence < 0 clamped to 0', 'AuditAggregator');
68
- _confidenceWarningEmitted = true;
69
- }
70
- return 0;
71
- }
72
- if (c > 1) {
73
- if (!_confidenceWarningEmitted) {
74
- process.emitWarning('audit-aggregator: confidence > 1 clamped to 1', 'AuditAggregator');
75
- _confidenceWarningEmitted = true;
76
- }
77
- return 1;
78
- }
79
- return c;
80
- }
81
-
82
- /**
83
- * Compute score for a finding.
84
- *
85
- * @param {Finding} f
86
- * @param {Object<string, number>} weights
87
- * @returns {number}
88
- */
89
- function score(f, weights) {
90
- const w = (weights && weights[f.severity]) ?? DEFAULT_WEIGHTS[f.severity] ?? 0;
91
- return w * clampConfidence(f.confidence);
92
- }
93
-
94
- function validateFinding(f, idx) {
95
- if (!f || typeof f !== 'object') {
96
- throw new TypeError(`audit-aggregator: input[${idx}] is not an object`);
97
- }
98
- if (typeof f.file !== 'string' || f.file.length === 0) {
99
- throw new TypeError(`audit-aggregator: input[${idx}].file is required (non-empty string)`);
100
- }
101
- if (typeof f.rule_id !== 'string' || f.rule_id.length === 0) {
102
- throw new TypeError(`audit-aggregator: input[${idx}].rule_id is required (non-empty string)`);
103
- }
104
- if (!(f.severity in SEVERITY_RANK)) {
105
- throw new TypeError(
106
- `audit-aggregator: input[${idx}].severity must be P0|P1|P2|P3 (got ${JSON.stringify(f.severity)})`,
107
- );
108
- }
109
- }
110
-
111
- function dedupKey(f) {
112
- return `${normalizePath(f.file)}::${f.line ?? 0}::${f.rule_id}`;
113
- }
114
-
115
- function defaultMerge(a, b) {
116
- // Higher confidence wins.
117
- const ca = clampConfidence(a.confidence);
118
- const cb = clampConfidence(b.confidence);
119
- if (ca !== cb) return ca > cb ? a : b;
120
- // Higher severity wins.
121
- const ra = SEVERITY_RANK[a.severity];
122
- const rb = SEVERITY_RANK[b.severity];
123
- if (ra !== rb) return ra > rb ? a : b;
124
- // Lexicographic agent.
125
- const aa = a.agent ?? '';
126
- const ab = b.agent ?? '';
127
- if (aa !== ab) return aa < ab ? a : b;
128
- // First-seen wins (a is by convention the existing entry).
129
- return a;
130
- }
131
-
132
- /**
133
- * Aggregate findings.
134
- *
135
- * @param {Finding[]} input
136
- * @param {AggregateOptions} [opts]
137
- * @returns {AggregateResult}
138
- */
139
- function aggregate(input, opts = {}) {
140
- if (!Array.isArray(input)) {
141
- throw new TypeError('audit-aggregator: input must be an array');
142
- }
143
- // Reset the once-per-call warning flag so a second call can warn again.
144
- _confidenceWarningEmitted = false;
145
- const merge = typeof opts.merge === 'function' ? opts.merge : defaultMerge;
146
- const weights = { ...DEFAULT_WEIGHTS, ...(opts.severityWeights || {}) };
147
-
148
- /** @type {Map<string, Finding>} */
149
- const byKey = new Map();
150
- let duplicates = 0;
151
- for (let i = 0; i < input.length; i++) {
152
- validateFinding(input[i], i);
153
- const f = { ...input[i] };
154
- const key = dedupKey(f);
155
- if (byKey.has(key)) {
156
- duplicates += 1;
157
- const existing = byKey.get(key);
158
- const winner = merge(existing, f);
159
- const loser = winner === existing ? f : existing;
160
- const mergedFrom = new Set(winner.merged_from || []);
161
- if (existing.agent && existing !== winner) mergedFrom.add(existing.agent);
162
- if (loser.agent && loser !== winner) mergedFrom.add(loser.agent);
163
- // Combine prior merged_from too.
164
- for (const a of (loser.merged_from || [])) mergedFrom.add(a);
165
- winner.merged_from = Array.from(mergedFrom);
166
- byKey.set(key, winner);
167
- } else {
168
- byKey.set(key, f);
169
- }
170
- }
171
-
172
- const findings = Array.from(byKey.values()).map((f) => ({ ...f, _score: score(f, weights) }));
173
- findings.sort((a, b) => {
174
- if (a._score !== b._score) return b._score - a._score;
175
- const ra = SEVERITY_RANK[a.severity];
176
- const rb = SEVERITY_RANK[b.severity];
177
- if (ra !== rb) return rb - ra;
178
- if (a.file !== b.file) return a.file < b.file ? -1 : 1;
179
- return (a.line ?? 0) - (b.line ?? 0);
180
- });
181
- // Strip the internal _score field before returning.
182
- for (const f of findings) delete f._score;
183
-
184
- const truncated = typeof opts.topN === 'number' && opts.topN >= 0
185
- ? findings.slice(0, opts.topN)
186
- : findings;
187
-
188
- /** @type {Record<string, number>} */
189
- const byRule = {};
190
- /** @type {Record<string, number>} */
191
- const bySeverity = { P0: 0, P1: 0, P2: 0, P3: 0 };
192
- /** @type {Record<string, number>} */
193
- const byFile = {};
194
- for (const f of truncated) {
195
- byRule[f.rule_id] = (byRule[f.rule_id] ?? 0) + 1;
196
- bySeverity[f.severity] += 1;
197
- const k = normalizePath(f.file);
198
- byFile[k] = (byFile[k] ?? 0) + 1;
199
- }
200
-
201
- return {
202
- findings: truncated,
203
- byRule,
204
- bySeverity,
205
- byFile,
206
- total: truncated.length,
207
- duplicates,
208
- };
209
- }
210
-
211
- module.exports = {
212
- aggregate,
213
- score,
214
- normalizePath,
215
- dedupKey,
216
- defaultMerge,
217
- DEFAULT_WEIGHTS,
218
- SEVERITY_RANK,
219
- };