@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.
- package/.claude-plugin/marketplace.json +26 -41
- package/.claude-plugin/plugin.json +23 -48
- package/CHANGELOG.md +91 -0
- package/README.md +166 -511
- package/SKILL.md +2 -0
- package/agents/README.md +33 -36
- package/agents/a11y-mapper.md +3 -3
- package/agents/component-benchmark-harvester.md +6 -6
- package/agents/component-benchmark-synthesizer.md +3 -3
- package/agents/compose-executor.md +3 -3
- package/agents/cost-forecaster.md +2 -2
- package/agents/design-auditor.md +7 -7
- package/agents/design-authority-watcher.md +15 -15
- package/agents/design-context-builder.md +4 -4
- package/agents/design-context-checker-gate.md +1 -1
- package/agents/design-discussant.md +2 -2
- package/agents/design-doc-writer.md +1 -1
- package/agents/design-executor.md +2 -2
- package/agents/design-figma-writer.md +2 -2
- package/agents/design-fixer.md +7 -7
- package/agents/design-integration-checker-gate.md +1 -1
- package/agents/design-integration-checker.md +1 -1
- package/agents/design-paper-writer.md +3 -3
- package/agents/design-pencil-writer.md +1 -1
- package/agents/design-planner.md +21 -0
- package/agents/design-reflector.md +39 -39
- package/agents/design-research-synthesizer.md +1 -0
- package/agents/design-start-writer.md +1 -1
- package/agents/design-update-checker.md +5 -5
- package/agents/design-verifier-gate.md +1 -1
- package/agents/design-verifier.md +52 -48
- package/agents/ds-generator.md +2 -2
- package/agents/ds-migration-planner.md +4 -4
- package/agents/email-executor.md +9 -9
- package/agents/experiment-result-ingester.md +3 -3
- package/agents/flutter-executor.md +5 -5
- package/agents/gdd-graph-refresh.md +3 -3
- package/agents/gdd-intel-updater.md +2 -2
- package/agents/motion-mapper.md +2 -2
- package/agents/motion-verifier.md +4 -4
- package/agents/pdf-executor.md +8 -8
- package/agents/perf-analyzer.md +17 -17
- package/agents/pr-commenter.md +9 -9
- package/agents/prototype-gate.md +2 -2
- package/agents/quality-gate-runner.md +1 -1
- package/agents/rollout-coordinator.md +3 -3
- package/agents/swift-executor.md +4 -4
- package/agents/ticket-sync-agent.md +6 -6
- package/agents/user-research-synthesizer.md +2 -2
- package/connections/connections.md +44 -45
- package/connections/cursor.md +73 -0
- package/connections/preview.md +3 -3
- package/dist/claude-code/.claude/skills/cache-manager/SKILL.md +3 -3
- package/dist/claude-code/.claude/skills/cache-manager/cache-policy.md +1 -1
- package/dist/claude-code/.claude/skills/design/SKILL.md +19 -0
- package/dist/claude-code/.claude/skills/explore/SKILL.md +11 -0
- package/dist/claude-code/.claude/skills/figma-write/SKILL.md +13 -2
- package/dist/claude-code/.claude/skills/paper-write/SKILL.md +54 -0
- package/dist/claude-code/.claude/skills/pencil-write/SKILL.md +54 -0
- package/dist/claude-code/.claude/skills/report-issue/SKILL.md +2 -2
- package/dist/claude-code/.claude/skills/router/SKILL.md +2 -2
- package/dist/claude-code/.claude/skills/verify/verify-procedure.md +10 -11
- package/dist/claude-code/.claude/skills/warm-cache/SKILL.md +1 -1
- package/hooks/first-run-nudge.cjs +171 -0
- package/hooks/gdd-intel-trigger.js +243 -0
- package/hooks/gdd-mcp-circuit-breaker.js +62 -7
- package/hooks/gdd-precompact-snapshot.js +50 -29
- package/hooks/gdd-protected-paths.js +150 -18
- package/hooks/gdd-risk-gate.js +93 -1
- package/hooks/gdd-sessionstart-recap.js +59 -24
- package/hooks/hooks.json +13 -4
- package/hooks/inject-using-gdd.cjs +188 -0
- package/hooks/update-check.cjs +511 -0
- package/package.json +9 -2
- package/reference/STATE-TEMPLATE.md +10 -13
- package/reference/audit-scoring.md +1 -1
- package/reference/cache-tier-doctrine.md +46 -0
- package/reference/config-schema.md +9 -9
- package/reference/i18n.md +1 -1
- package/reference/intel-schema.md +37 -2
- package/reference/meta-rules.md +4 -4
- package/reference/model-tiers.md +2 -2
- package/reference/registry.json +101 -94
- package/reference/runtime-models.md +11 -1
- package/reference/shared-preamble.md +13 -14
- package/reference/skill-graph.md +24 -1
- package/scripts/bootstrap.cjs +373 -0
- package/scripts/injection-patterns.cjs +58 -0
- package/scripts/lib/apply-reflections/incubator-proposals.cjs +57 -26
- package/scripts/lib/install/converters/codex-plugin.cjs +5 -2
- package/scripts/lib/install/converters/cursor.cjs +20 -0
- package/scripts/lib/issue-reporter/report-flow.cjs +1 -1
- package/scripts/lib/manifest/skills.json +80 -13
- package/scripts/lib/state/query-surface.cjs +67 -9
- package/scripts/lib/state/state-store.cjs +68 -26
- package/sdk/cli/commands/stage.ts +17 -0
- package/sdk/cli/index.js +14 -0
- package/skills/cache-manager/SKILL.md +3 -3
- package/skills/cache-manager/cache-policy.md +1 -1
- package/skills/design/SKILL.md +19 -0
- package/skills/explore/SKILL.md +11 -0
- package/skills/figma-write/SKILL.md +13 -2
- package/skills/paper-write/SKILL.md +54 -0
- package/skills/pencil-write/SKILL.md +54 -0
- package/skills/report-issue/SKILL.md +2 -2
- package/skills/router/SKILL.md +2 -2
- package/skills/verify/verify-procedure.md +10 -11
- package/skills/warm-cache/SKILL.md +1 -1
- package/hooks/first-run-nudge.sh +0 -82
- package/hooks/inject-using-gdd.sh +0 -72
- package/hooks/update-check.sh +0 -251
- package/scripts/lib/audit-aggregator/index.cjs +0 -219
- package/scripts/lib/hedge-ensemble.cjs +0 -217
package/hooks/first-run-nudge.sh
DELETED
|
@@ -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
|
package/hooks/update-check.sh
DELETED
|
@@ -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
|
-
};
|