@hegemonart/get-design-done 1.0.7 → 1.14.1

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 (56) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +129 -2
  4. package/README.md +186 -53
  5. package/SKILL.md +6 -4
  6. package/agents/design-authority-watcher.md +208 -0
  7. package/agents/design-component-generator.md +221 -0
  8. package/agents/design-context-builder.md +83 -6
  9. package/agents/design-discussant.md +1 -1
  10. package/agents/design-figma-writer.md +8 -8
  11. package/agents/design-paper-writer.md +131 -0
  12. package/agents/design-pencil-writer.md +99 -0
  13. package/agents/design-research-synthesizer.md +13 -1
  14. package/agents/design-update-checker.md +117 -0
  15. package/agents/design-verifier.md +51 -0
  16. package/agents/token-mapper.md +1 -1
  17. package/connections/21st-dev.md +98 -0
  18. package/connections/claude-design.md +0 -1
  19. package/connections/connections.md +50 -33
  20. package/connections/figma.md +81 -39
  21. package/connections/magic-patterns.md +105 -0
  22. package/connections/paper-design.md +137 -0
  23. package/connections/pencil-dev.md +88 -0
  24. package/connections/pinterest.md +0 -1
  25. package/hooks/budget-enforcer.js +13 -2
  26. package/hooks/gdd-read-injection-scanner.js +4 -9
  27. package/hooks/hooks.json +8 -0
  28. package/hooks/update-check.sh +251 -0
  29. package/package.json +1 -1
  30. package/reference/ai-native-tool-interface.md +102 -0
  31. package/reference/authority-feeds.md +72 -0
  32. package/reference/schemas/authority-snapshot.schema.json +42 -0
  33. package/reference/schemas/config.schema.json +4 -0
  34. package/reference/schemas/marketplace.schema.json +2 -2
  35. package/reference/schemas/plugin.schema.json +1 -1
  36. package/scripts/aggregate-agent-metrics.js +20 -0
  37. package/scripts/build-intel.cjs +13 -5
  38. package/scripts/injection-patterns.cjs +17 -0
  39. package/scripts/run-injection-scanner-ci.cjs +1 -10
  40. package/scripts/tests/test-authority-rejected-kinds.sh +58 -0
  41. package/scripts/tests/test-authority-watcher-diff.sh +113 -0
  42. package/scripts/validate-schemas.cjs +18 -1
  43. package/skills/audit/SKILL.md +11 -1
  44. package/skills/check-update/SKILL.md +135 -0
  45. package/skills/complete-cycle/SKILL.md +10 -0
  46. package/skills/design/SKILL.md +5 -5
  47. package/skills/discover/SKILL.md +2 -2
  48. package/skills/explore/SKILL.md +55 -3
  49. package/skills/health/SKILL.md +10 -0
  50. package/skills/help/SKILL.md +10 -0
  51. package/skills/progress/SKILL.md +10 -0
  52. package/skills/reflect/SKILL.md +1 -0
  53. package/skills/scan/SKILL.md +9 -19
  54. package/skills/ship/SKILL.md +10 -0
  55. package/skills/watch-authorities/SKILL.md +82 -0
  56. package/connections/figma-writer.md +0 -139
@@ -36,6 +36,10 @@
36
36
  }
37
37
  }
38
38
  }
39
+ },
40
+ "update_dismissed": {
41
+ "type": "string",
42
+ "description": "Latest plugin tag (e.g. \"v1.0.7.3\") whose update nudge the user has dismissed. Set by /gdd:check-update --dismiss and by hooks/update-check.sh on the --dismiss code path. When a newer tag ships, the nudge reappears."
39
43
  }
40
44
  }
41
45
  }
@@ -25,7 +25,7 @@
25
25
  "required": ["description", "version"],
26
26
  "properties": {
27
27
  "description": { "type": "string", "minLength": 1 },
28
- "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" }
28
+ "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(\\.\\d+)?$" }
29
29
  }
30
30
  },
31
31
  "plugins": {
@@ -48,7 +48,7 @@
48
48
  "name": { "type": "string", "minLength": 1 },
49
49
  "source": { "type": "string", "minLength": 1 },
50
50
  "description": { "type": "string", "minLength": 1 },
51
- "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
51
+ "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(\\.\\d+)?$" },
52
52
  "author": {
53
53
  "type": "object",
54
54
  "additionalProperties": true,
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "version": {
30
30
  "type": "string",
31
- "pattern": "^\\d+\\.\\d+\\.\\d+$"
31
+ "pattern": "^\\d+\\.\\d+\\.\\d+(\\.\\d+)?$"
32
32
  },
33
33
  "description": {
34
34
  "type": "string",
@@ -25,6 +25,7 @@ const os = require('os');
25
25
  const CWD = process.cwd();
26
26
  const TELEMETRY_PATH = path.join(CWD, '.design', 'telemetry', 'costs.jsonl');
27
27
  const METRICS_PATH = path.join(CWD, '.design', 'agent-metrics.json');
28
+ const PHASE_TOTALS_PATH = path.join(CWD, '.design', 'telemetry', 'phase-totals.json');
28
29
  const AGENTS_DIR = path.join(CWD, 'agents');
29
30
 
30
31
  // ---- frontmatter reader (no YAML dep) ----
@@ -124,6 +125,18 @@ function writeAtomic(filePath, content) {
124
125
  fs.renameSync(tmp, filePath);
125
126
  }
126
127
 
128
+ // ---- phase totals aggregator (WR-02: avoids full JSONL replay in budget enforcer) ----
129
+ function aggregateByPhase(rows) {
130
+ const byPhase = {};
131
+ for (const r of rows) {
132
+ const phase = r.phase || 'unknown';
133
+ byPhase[phase] = (byPhase[phase] || 0) + Number(r.est_cost_usd || 0);
134
+ }
135
+ // Round to 6dp to match per-agent precision
136
+ for (const k of Object.keys(byPhase)) byPhase[k] = Number(byPhase[k].toFixed(6));
137
+ return byPhase;
138
+ }
139
+
127
140
  // ---- main ----
128
141
  function main() {
129
142
  const rows = readTelemetryRows();
@@ -133,6 +146,13 @@ function main() {
133
146
  agents,
134
147
  };
135
148
  writeAtomic(METRICS_PATH, JSON.stringify(payload, null, 2) + '\n');
149
+ // Write lightweight phase-totals.json so budget-enforcer can read phase spend
150
+ // in O(1) without replaying the full JSONL on every agent spawn (WR-02).
151
+ const phaseTotals = {
152
+ generated_at: new Date().toISOString(),
153
+ totals: aggregateByPhase(rows),
154
+ };
155
+ writeAtomic(PHASE_TOTALS_PATH, JSON.stringify(phaseTotals, null, 2) + '\n');
136
156
  }
137
157
 
138
158
  try {
@@ -17,7 +17,7 @@
17
17
 
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
- const { execSync } = require('child_process');
20
+ const { spawnSync } = require('child_process');
21
21
 
22
22
  const ROOT = process.cwd();
23
23
  const INTEL_DIR = path.join(ROOT, '.design', 'intel');
@@ -46,15 +46,23 @@ function writeSlice(name, data) {
46
46
 
47
47
  function gitHash(filePath) {
48
48
  try {
49
- return execSync(`git log -1 --format=%h -- "${filePath}"`, { stdio: ['pipe', 'pipe', 'ignore'] })
50
- .toString().trim() || 'untracked';
49
+ const r = spawnSync('git', ['log', '-1', '--format=%h', '--', filePath], {
50
+ stdio: ['pipe', 'pipe', 'ignore'],
51
+ encoding: 'utf8',
52
+ timeout: 5000,
53
+ });
54
+ return r.stdout.trim() || 'untracked';
51
55
  } catch { return 'untracked'; }
52
56
  }
53
57
 
54
58
  function headHash() {
55
59
  try {
56
- return execSync('git rev-parse --short HEAD', { stdio: ['pipe', 'pipe', 'ignore'] })
57
- .toString().trim();
60
+ const r = spawnSync('git', ['rev-parse', '--short', 'HEAD'], {
61
+ stdio: ['pipe', 'pipe', 'ignore'],
62
+ encoding: 'utf8',
63
+ timeout: 5000,
64
+ });
65
+ return r.stdout.trim() || 'unknown';
58
66
  } catch { return 'unknown'; }
59
67
  }
60
68
 
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+ // Shared prompt-injection patterns — single source of truth for both
3
+ // hooks/gdd-read-injection-scanner.js (runtime hook) and
4
+ // scripts/run-injection-scanner-ci.cjs (CI scanner).
5
+ // Add new patterns here; both consumers pick them up automatically.
6
+
7
+ const INJECTION_PATTERNS = [
8
+ { name: 'ignore previous', re: /ignore\s+(all\s+)?(previous|prior|above)\s+instructions?/i },
9
+ { name: 'disregard previous', re: /disregard\s+(all\s+)?(previous|prior|above)\s+instructions?/i },
10
+ { name: 'you are now a different', re: /you\s+are\s+now\s+a\s+different/i },
11
+ { name: 'system: you are', re: /system\s*:\s*you\s+are/i },
12
+ { name: 'role tag injection', re: /<\s*\/?\s*(system|assistant|human)\s*>/i },
13
+ { name: '[INST] fragment', re: /\[INST\]/i },
14
+ { name: '### instruction fragment',re: /###\s*instruction/i },
15
+ ];
16
+
17
+ module.exports = { INJECTION_PATTERNS };
@@ -13,16 +13,7 @@ const path = require('path');
13
13
 
14
14
  const REPO_ROOT = path.resolve(__dirname, '..');
15
15
 
16
- // Patterns mirror hooks/gdd-read-injection-scanner.js.
17
- const INJECTION_PATTERNS = [
18
- { name: 'ignore previous', re: /ignore\s+(all\s+)?(previous|prior|above)\s+instructions?/i },
19
- { name: 'disregard previous', re: /disregard\s+(all\s+)?(previous|prior|above)\s+instructions?/i },
20
- { name: 'you are now a different', re: /you\s+are\s+now\s+a\s+different/i },
21
- { name: 'system: you are', re: /system\s*:\s*you\s+are/i },
22
- { name: 'role tag injection', re: /<\s*\/?\s*(system|assistant|human)\s*>/i },
23
- { name: '[INST] fragment', re: /\[INST\]/i },
24
- { name: '### instruction fragment', re: /###\s*instruction/i },
25
- ];
16
+ const { INJECTION_PATTERNS } = require('./injection-patterns.cjs');
26
17
 
27
18
  function walkMd(dir, out) {
28
19
  if (!fs.existsSync(dir)) return;
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ # test-authority-rejected-kinds.sh
3
+ #
4
+ # Asserts that reference/authority-feeds.md does NOT contain trend-aggregator
5
+ # hosts OUTSIDE the explicit "## Rejected kinds" section. Enforces the anti-slop
6
+ # thesis structurally (CONTEXT.md D-08, D-28).
7
+ #
8
+ # Exit 0 = clean. Exit 1 = at least one rejected host appeared in the active
9
+ # whitelist, or the rejected-kinds section itself was removed.
10
+
11
+ set -euo pipefail
12
+
13
+ WHITELIST="${WHITELIST:-reference/authority-feeds.md}"
14
+
15
+ if [ ! -f "$WHITELIST" ]; then
16
+ echo "FAIL: $WHITELIST not found." >&2
17
+ exit 1
18
+ fi
19
+
20
+ # Split the file at the "## Rejected kinds" heading. Everything BEFORE it is
21
+ # the active whitelist; everything AFTER is the rejection manifest (which is
22
+ # allowed to mention these hosts — that's the whole point).
23
+ ACTIVE_SECTION="$(awk '/^## Rejected kinds/{exit} {print}' "$WHITELIST")"
24
+
25
+ REJECTED_PATTERNS=(
26
+ 'dribbble\.com'
27
+ 'behance\.net'
28
+ 'linkedin\.com'
29
+ 'medium\.com/topic'
30
+ 'producthunt\.com/posts'
31
+ 'top[[:space:]]*10[[:space:]]*ui'
32
+ 'trending-ui'
33
+ )
34
+
35
+ FAIL=0
36
+ for pat in "${REJECTED_PATTERNS[@]}"; do
37
+ if echo "$ACTIVE_SECTION" | grep -iEq "$pat"; then
38
+ echo "FAIL: rejected pattern '$pat' matched in active whitelist section of $WHITELIST." >&2
39
+ FAIL=1
40
+ fi
41
+ done
42
+
43
+ if [ "$FAIL" -ne 0 ]; then
44
+ echo "" >&2
45
+ echo "The whitelist must not contain trend-aggregator hosts outside the '## Rejected kinds' block." >&2
46
+ echo "See .planning/phases/13.2-external-authority-watcher/13.2-CONTEXT.md §D-08." >&2
47
+ exit 1
48
+ fi
49
+
50
+ # Also assert the rejected-kinds section itself is present — regression against
51
+ # someone deleting the section entirely.
52
+ if ! grep -q '^## Rejected kinds$' "$WHITELIST"; then
53
+ echo "FAIL: '## Rejected kinds' section is missing from $WHITELIST." >&2
54
+ exit 1
55
+ fi
56
+
57
+ echo "OK: $WHITELIST passes rejected-kinds check."
58
+ exit 0
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ # test-authority-watcher-diff.sh
3
+ #
4
+ # Structural-only v1: validates that the frozen authority-report baseline at
5
+ # test-fixture/baselines/phase-13.2/authority-report.expected.md preserves the
6
+ # shape the watcher-diff test depends on. Full end-to-end byte-diff against a
7
+ # live watcher run is deferred — CI cannot spawn Claude Code agents.
8
+ #
9
+ # What this test asserts (structural-only v1):
10
+ # 1. The fixture directory exists and is non-empty.
11
+ # 2. The baseline file exists, is non-empty, and starts with an Authority
12
+ # Report header.
13
+ # 3. The baseline contains the canonical classification section headings in
14
+ # the D-21 weighted order (spec > heuristic > pattern > craft).
15
+ # 4. The baseline declares a total-entries line consistent with the count of
16
+ # bulleted entries under its classification sections.
17
+ # 5. The baseline ends with a `**Skipped:**` footer.
18
+ #
19
+ # Exit 0 = structural shape preserved. Exit 1 = shape drift detected.
20
+ #
21
+ # Full end-to-end byte-diff (running the watcher against the fixtures and
22
+ # diffing stdout against the baseline) is a follow-up tracked in STATE.md
23
+ # "Open follow-ups". When the agent runtime becomes available in CI this
24
+ # script graduates from structural-only v1 to the full diff check.
25
+
26
+ set -euo pipefail
27
+
28
+ FIXTURE_DIR="test-fixture/authority-feeds"
29
+ BASELINE="test-fixture/baselines/phase-13.2/authority-report.expected.md"
30
+
31
+ if [ ! -d "$FIXTURE_DIR" ]; then
32
+ echo "FAIL: fixture dir $FIXTURE_DIR missing." >&2
33
+ exit 1
34
+ fi
35
+
36
+ # Count actual fixture files (should be 4 frozen feeds + 1 README; we only
37
+ # care that at least one XML/JSON fixture is present).
38
+ # Use null-delimited find to handle filenames with spaces/newlines (WR-05).
39
+ FIXTURE_COUNT=0
40
+ while IFS= read -r -d '' _f; do
41
+ FIXTURE_COUNT=$((FIXTURE_COUNT + 1))
42
+ done < <(find "$FIXTURE_DIR" -maxdepth 1 -type f \( -name '*.atom' -o -name '*.rss' -o -name '*.json' \) -print0)
43
+ if [ "$FIXTURE_COUNT" -lt 1 ]; then
44
+ echo "FAIL: $FIXTURE_DIR contains no feed fixtures (.atom/.rss/.json)." >&2
45
+ exit 1
46
+ fi
47
+
48
+ if [ ! -f "$BASELINE" ]; then
49
+ echo "FAIL: baseline $BASELINE missing." >&2
50
+ exit 1
51
+ fi
52
+
53
+ if [ ! -s "$BASELINE" ]; then
54
+ echo "FAIL: baseline $BASELINE is empty." >&2
55
+ exit 1
56
+ fi
57
+
58
+ # Header: must start with "# Authority Report"
59
+ if ! head -1 "$BASELINE" | grep -q '^# Authority Report'; then
60
+ echo "FAIL: baseline does not begin with '# Authority Report' header." >&2
61
+ exit 1
62
+ fi
63
+
64
+ # Totals line must declare a non-negative surfaced-entries count.
65
+ if ! grep -qE '^[0-9]+ entries surfaced across [0-9]+ feeds\. [0-9]+ skipped\.$' "$BASELINE"; then
66
+ echo "FAIL: baseline is missing the 'N entries surfaced across M feeds. K skipped.' totals line." >&2
67
+ exit 1
68
+ fi
69
+
70
+ # Classification sections — at least one of the four non-skip buckets must
71
+ # appear. (Empty buckets are omitted by D-21, but a well-shaped baseline
72
+ # always has at least one.)
73
+ SECTION_HITS=0
74
+ for heading in '^## spec-change' '^## heuristic-update' '^## pattern-guidance' '^## craft-tip'; do
75
+ if grep -qE "$heading" "$BASELINE"; then
76
+ SECTION_HITS=$((SECTION_HITS + 1))
77
+ fi
78
+ done
79
+
80
+ if [ "$SECTION_HITS" -lt 1 ]; then
81
+ echo "FAIL: baseline does not contain any classification heading (spec-change / heuristic-update / pattern-guidance / craft-tip)." >&2
82
+ exit 1
83
+ fi
84
+
85
+ # Count consistency check: the header's "N entries surfaced" must match the
86
+ # total bulleted entries across the four classification sections.
87
+ HEADER_COUNT=$(grep -oE '^[0-9]+ entries surfaced' "$BASELINE" | head -1 | grep -oE '^[0-9]+')
88
+
89
+ # Extract the body between the first "## spec-change|heuristic-update|pattern-guidance|craft-tip"
90
+ # heading and the closing "---" horizontal rule before the Skipped footer.
91
+ BULLET_COUNT=$(awk '
92
+ /^## (spec-change|heuristic-update|pattern-guidance|craft-tip)/ { in_section=1; next }
93
+ /^---$/ { in_section=0 }
94
+ in_section && /^- / { count++ }
95
+ END { print count+0 }
96
+ ' "$BASELINE")
97
+
98
+ if [ "$HEADER_COUNT" != "$BULLET_COUNT" ]; then
99
+ echo "FAIL: baseline header declares $HEADER_COUNT entries but the classification sections contain $BULLET_COUNT bullets." >&2
100
+ echo " Counts MUST match — regenerate the baseline or fix the header." >&2
101
+ exit 1
102
+ fi
103
+
104
+ # Footer: must have a Skipped line.
105
+ if ! grep -qE '^\*\*Skipped:\*\*' "$BASELINE"; then
106
+ echo "FAIL: baseline is missing the '**Skipped:**' footer line." >&2
107
+ exit 1
108
+ fi
109
+
110
+ echo "OK: authority-report baseline shape preserved (structural-only v1)."
111
+ echo " Fixtures: $FIXTURE_COUNT files under $FIXTURE_DIR"
112
+ echo " Entries: $HEADER_COUNT (header) = $BULLET_COUNT (bullets)"
113
+ exit 0
@@ -69,6 +69,14 @@ const PAIRS = [
69
69
  data: null,
70
70
  required: false,
71
71
  },
72
+ {
73
+ name: 'authority-snapshot',
74
+ schema: 'reference/schemas/authority-snapshot.schema.json',
75
+ // .design/authority-snapshot.json is runtime-only (gitignored via .design/).
76
+ // Only schema-compile it; Phase 13.2-02's watcher emits the data file at runtime.
77
+ data: null,
78
+ required: false,
79
+ },
72
80
  ];
73
81
 
74
82
  const USE_NPX = !process.argv.includes('--no-npx');
@@ -76,11 +84,20 @@ const USE_NPX = !process.argv.includes('--no-npx');
76
84
  /**
77
85
  * Try running ajv-cli via npx. Returns { ok, stdout, stderr, status, fetchFailed }.
78
86
  * fetchFailed=true if npx couldn't fetch the package (offline); caller should fall back.
87
+ *
88
+ * We pass `-c ajv-formats` so schemas declaring `format: "date-time"` (etc.) are
89
+ * validated against the standard JSON Schema formats plugin rather than being
90
+ * rejected as unknown formats under ajv's strict mode. Schemas that declare no
91
+ * formats (e.g., plugin, marketplace) are unaffected.
79
92
  */
80
93
  function runAjv(args) {
94
+ const injected = [...args];
95
+ // Inject `-c ajv-formats` after the sub-command (compile/validate) but before -s flags.
96
+ // Simpler: just append; ajv-cli accepts -c at any position.
97
+ injected.push('-c', 'ajv-formats');
81
98
  const result = spawnSync(
82
99
  'npx',
83
- ['--yes', 'ajv-cli@5', ...args],
100
+ ['--yes', '-p', 'ajv-cli@5', '-p', 'ajv-formats@3', 'ajv', ...injected],
84
101
  { encoding: 'utf8', cwd: REPO_ROOT }
85
102
  );
86
103
  const combined = (result.stdout || '') + (result.stderr || '');
@@ -2,7 +2,7 @@
2
2
  name: gdd-audit
3
3
  description: "Run design audit — wraps design-verifier + design-auditor + design-reflector. --retroactive audits the full cycle scope."
4
4
  argument-hint: "[--retroactive] [--quick] [--no-reflect]"
5
- tools: Read, Write, Task, Glob
5
+ tools: Read, Write, Task, Glob, Bash
6
6
  ---
7
7
 
8
8
  # /gdd:audit
@@ -51,4 +51,14 @@ Run only `design-auditor` (skip `design-integration-checker`). Faster health che
51
51
  - Do not modify source files.
52
52
  - Do not rerun stages; this is a read-only audit.
53
53
 
54
+ ## Step 7 — Update notice (post-closeout surface)
55
+
56
+ After the consolidated audit summary has been printed (and any reflection-proposal count appended), emit the plugin-update banner if one is present:
57
+
58
+ ```bash
59
+ [ -f .design/update-available.md ] && cat .design/update-available.md
60
+ ```
61
+
62
+ Written by `hooks/update-check.sh`; suppressed mid-pipeline and when the latest release is dismissed.
63
+
54
64
  ## AUDIT COMPLETE
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: gdd-check-update
3
+ description: "Manual plugin-update check. Shows cached state by default; --refresh bypasses the 24h TTL; --dismiss hides the nudge until a newer release ships; --prompt spawns design-update-checker for a richer summary."
4
+ argument-hint: "[--refresh] [--dismiss] [--prompt]"
5
+ tools: Read, Write, Bash, Task
6
+ ---
7
+
8
+ # /gdd:check-update
9
+
10
+ **Role:** Manual entry point for the plugin-update checker. The SessionStart hook (`hooks/update-check.sh`) already runs on its own 24h cadence and writes `.design/update-cache.json` + `.design/update-available.md`. This command lets the user inspect / force / dismiss / enrich that state on demand.
11
+
12
+ ## Flags
13
+
14
+ | Flag | Effect |
15
+ |---|---|
16
+ | *(none)* | Print cached state (latest_tag, delta, is_newer). If the cache is older than 24h, trigger `--refresh` implicitly. |
17
+ | `--refresh` | Invoke `hooks/update-check.sh --refresh` — bypasses the 24h TTL and re-fetches immediately. |
18
+ | `--dismiss` | Write `update_dismissed: "<latest_tag>"` to `.design/config.json` atomically and delete `.design/update-available.md`. Sticky until a newer release ships. |
19
+ | `--prompt` | Spawn `design-update-checker` agent (Haiku) to produce a 3–5-line "what this release changes for you" summary. Does not alter the banner or cache. |
20
+
21
+ Flags can be combined: `--refresh --prompt` is valid (re-fetch, then enrich). `--dismiss` is the only flag that mutates `.design/config.json`.
22
+
23
+ ## Steps
24
+
25
+ 1. **Parse flags.** Detect `--refresh`, `--dismiss`, `--prompt` in `$ARGUMENTS`. Any unknown flag: print `Unknown flag: <flag>` and exit.
26
+
27
+ 2. **--refresh path** (if `--refresh` in flags):
28
+ Run the hot-path hook with the refresh flag:
29
+ ```bash
30
+ bash "${CLAUDE_PLUGIN_ROOT:-$(pwd)}/hooks/update-check.sh" --refresh
31
+ ```
32
+ This re-fetches `/releases/latest`, rewrites `.design/update-cache.json`, and re-renders `.design/update-available.md` subject to the same state/dismissal gates.
33
+
34
+ 3. **Read cache.** After any optional refresh, read `.design/update-cache.json`. If it does not exist:
35
+ - Print: `No cache. Network may be unreachable or the hook has not run yet. Try /gdd:check-update --refresh.`
36
+ - Exit.
37
+
38
+ <!-- markdownlint-disable MD025 -->
39
+
40
+ 4. **Dismiss path** (if `--dismiss` in flags):
41
+ Compute new config contents and write atomically. The python heredoc receives CONFIG_PATH and LATEST_TAG via the ENVIRONMENT (env-prefix form — `KEY=VALUE python3 <<PY`), NOT via trailing argv. Passing `python3 -c '...' KEY=VALUE` makes Python treat the assignments as `sys.argv`, which the old draft did incorrectly; env-prefix form is the portable fix.
42
+
43
+ ```bash
44
+ CONFIG_PATH=".design/config.json"
45
+ LATEST_TAG="$(grep -E '"latest_tag"' .design/update-cache.json | head -n1 | sed -E 's/.*"latest_tag"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"
46
+ [ -n "$LATEST_TAG" ] || { echo 'No latest_tag in cache — nothing to dismiss.'; exit 0; }
47
+
48
+ mkdir -p .design
49
+
50
+ # Env-prefix form: CONFIG_PATH and LATEST_TAG are placed into the child process env,
51
+ # then python3 reads them via os.environ. Heredoc body is flat at column 0 (required —
52
+ # any leading indentation breaks Python parsing in a heredoc).
53
+ CONFIG_PATH="$CONFIG_PATH" LATEST_TAG="$LATEST_TAG" python3 <<'PY'
54
+ import json, os, sys, tempfile
55
+
56
+ config_path = os.environ['CONFIG_PATH']
57
+ latest_tag = os.environ['LATEST_TAG']
58
+
59
+ # Load existing config if present; otherwise start fresh. Preserve every unknown key.
60
+ try:
61
+ with open(config_path, 'r', encoding='utf-8') as f:
62
+ data = json.load(f)
63
+ if not isinstance(data, dict):
64
+ data = {}
65
+ except (FileNotFoundError, json.JSONDecodeError):
66
+ data = {}
67
+
68
+ # Set ONLY the dismissal key — every other pre-existing key is preserved verbatim.
69
+ data['update_dismissed'] = latest_tag
70
+
71
+ # Atomic write: write to tmp on the SAME filesystem, then os.replace() (POSIX rename(2)).
72
+ target_dir = os.path.dirname(config_path) or '.'
73
+ fd, tmp_path = tempfile.mkstemp(prefix='config.', suffix='.tmp', dir=target_dir)
74
+ try:
75
+ with os.fdopen(fd, 'w', encoding='utf-8') as f:
76
+ json.dump(data, f, indent=2)
77
+ f.write('\n')
78
+ os.replace(tmp_path, config_path) # atomic on same filesystem
79
+ except Exception as exc:
80
+ try:
81
+ os.unlink(tmp_path)
82
+ except OSError:
83
+ pass
84
+ sys.exit(1)
85
+ PY
86
+
87
+ # Remove the rendered banner (user dismissed — stop showing it until a newer release ships).
88
+ rm -f .design/update-available.md
89
+ echo "Dismissed $LATEST_TAG. The nudge will return when a newer release ships."
90
+ ```
91
+
92
+ D-14 atomic-write invariant: `os.replace()` (POSIX `rename(2)`) is atomic on the same filesystem — an interrupted write leaves the original config intact. The `json.load → set single key → json.dump` round-trip guarantees every unknown top-level key (e.g. `model_profile`, `parallelism`) is preserved verbatim.
93
+
94
+ 5. **Print default state** (always, unless the skill exited early):
95
+ ```
96
+ ━━━ /gdd:check-update ━━━
97
+ Current: v<X.Y.Z>
98
+ Latest: v<A.B.C> (delta: <major|minor|patch|off-cadence|none>)
99
+ Newer: <true|false>
100
+ Checked: <ISO time of checked_at>
101
+ Dismissed: <tag or "no">
102
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━
103
+ ```
104
+ Parse these fields from `.design/update-cache.json` using `grep + sed` (no jq dependency). Read dismissal from `.design/config.json` via the same pattern as hooks/update-check.sh:
105
+ ```bash
106
+ [ -f .design/config.json ] && grep -E '"update_dismissed"' .design/config.json | sed -E 's/.*"update_dismissed"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/'
107
+ ```
108
+
109
+ 6. **--prompt path** (if `--prompt` in flags):
110
+ Spawn the `design-update-checker` agent via the Task tool. Pass as context:
111
+ - `current_tag` — from `.design/update-cache.json`
112
+ - `latest_tag` — from `.design/update-cache.json`
113
+ - `delta` — from `.design/update-cache.json`
114
+ - `release_body` — the `changelog_excerpt` from the cache (may be pre-truncated to 500 chars; that's fine — the agent knows)
115
+
116
+ Display the agent's inline response verbatim below the state banner. The agent ends with `## UPDATE-CHECKER COMPLETE` — the skill's own completion marker (below) is the final line.
117
+
118
+ ## Defaults
119
+
120
+ - No flags → behave as: `--refresh (only if cache >24h old) → print state`. Silent no-op if cache is fresh and there is no newer version.
121
+
122
+ ## Do Not
123
+
124
+ - Do not fetch from GitHub in this skill directly — always go through `hooks/update-check.sh --refresh` so the caching + state-guard + dismissal logic stays in one place.
125
+ - Do not modify `.design/update-available.md` except to delete it on `--dismiss`. The hot-path hook is the only writer of that banner (D-06).
126
+ - Do not rewrite `.design/config.json` wholesale — the atomic python rewrite preserves every unknown key (D-14).
127
+ - Do not pass variables to the python heredoc via trailing `KEY=VALUE` argv — that assigns to `sys.argv`, not `os.environ`. Use env-prefix form only.
128
+
129
+ ## Completion marker
130
+
131
+ Last line of output:
132
+
133
+ ```
134
+ ## CHECK-UPDATE COMPLETE
135
+ ```
@@ -30,4 +30,14 @@ Closes the current cycle: marks CYCLES.md entry complete, archives pipeline arti
30
30
  - Do not delete source files in `src/` — only archive `.design/` artifacts.
31
31
  - Do not auto-start a new cycle — user invokes `/gdd:new-cycle` explicitly.
32
32
 
33
+ ## Step 6 — Update notice (post-closeout surface)
34
+
35
+ After the archive has been written and STATE.md has been cleared for the next cycle, emit the plugin-update banner if one is present:
36
+
37
+ ```bash
38
+ [ -f .design/update-available.md ] && cat .design/update-available.md
39
+ ```
40
+
41
+ Written by `hooks/update-check.sh`; suppressed mid-pipeline and when the latest release is dismissed.
42
+
33
43
  ## COMPLETE-CYCLE COMPLETE
@@ -41,7 +41,7 @@ Skip if `auto_mode=true`.
41
41
 
42
42
  ## Pre-execution — Project-local conventions
43
43
 
44
- When spawning the executor, include any `./.claude/skills/design-*-conventions.md` files in `<required_reading>` so the executor sees project-local design conventions (typography, color, layout, motion, component, interaction decisions codified from prior sketch wrap-ups). Also include any `~/.claude/gdd/global-skills/*.md` files if the directory exists � global skills are cross-project conventions that inform but do not override project-local D-XX decisions.
44
+ When spawning the executor, include any `./.claude/skills/design-*-conventions.md` files in `<required_reading>` so the executor sees project-local design conventions (typography, color, layout, motion, component, interaction decisions codified from prior sketch wrap-ups). Also include any `~/.claude/gdd/global-skills/*.md` files if the directory exists � global skills are cross-project conventions that inform but do not override project-local D-XX decisions.
45
45
 
46
46
  ---
47
47
 
@@ -256,13 +256,13 @@ Next: /get-design-done:verify
256
256
 
257
257
  After design-executor has finished and DESIGN-PLAN.md tasks are complete:
258
258
 
259
- 1. Read `figma_writer:` status from `.design/STATE.md` `<connections>`:
260
- - If `figma_writer: not_configured` or absent → skip this block entirely (no prompt, no output)
261
- - If `figma_writer: available` → proceed to step 2
259
+ 1. Read `figma:` status from `.design/STATE.md` `<connections>` (the unified remote MCP covers both reads and writes as of v1.0.7.1):
260
+ - If `figma: not_configured` or `figma: unavailable` or absent → skip this block entirely (no prompt, no output)
261
+ - If `figma: available` → proceed to step 2
262
262
 
263
263
  2. Offer the user a prompt:
264
264
  ```
265
- figma-writer is available — propagate design decisions back to Figma?
265
+ figma write-back is available — propagate design decisions back to Figma?
266
266
  Modes: annotate (layer comments) | tokenize (variable bindings) | mappings (Code Connect)
267
267
  Run figma-write? (y/N):
268
268
  ```
@@ -22,9 +22,9 @@ user-invocable: true
22
22
  **A — Figma probe:**
23
23
 
24
24
  ```
25
- A1. ToolSearch({ query: "select:mcp__figma-desktop__get_metadata", max_results: 1 })
25
+ A1. ToolSearch({ query: "select:mcp__figma__get_metadata", max_results: 1 })
26
26
  A2. Empty result → figma: not_configured (skip all Figma paths)
27
- Non-empty result → call mcp__figma-desktop__get_metadata
27
+ Non-empty result → call mcp__figma__get_metadata
28
28
  Success → figma: available
29
29
  Error → figma: unavailable
30
30
  ```
@@ -19,9 +19,9 @@ Probe connection availability and update `.design/STATE.md` `<connections>`:
19
19
 
20
20
  **A — Figma probe:**
21
21
  ```
22
- ToolSearch({ query: "select:mcp__figma-desktop__get_metadata", max_results: 1 })
22
+ ToolSearch({ query: "select:mcp__figma__get_metadata", max_results: 1 })
23
23
  Empty → figma: not_configured
24
- Non-empty → call mcp__figma-desktop__get_metadata; success → available; error → unavailable
24
+ Non-empty → call mcp__figma__get_metadata; success → available; error → unavailable
25
25
  ```
26
26
 
27
27
  **B — Refero probe:**
@@ -31,7 +31,59 @@ Empty → refero: not_configured
31
31
  Non-empty → refero: available
32
32
  ```
33
33
 
34
- Write results to STATE.md `<connections>`.
34
+ **C 21st.dev probe:**
35
+ ```
36
+ ToolSearch({ query: "mcp__21st", max_results: 5 })
37
+ Empty → 21st-dev: not_configured
38
+ Non-empty → 21st-dev: available
39
+ ```
40
+
41
+ **D — Magic Patterns probe:**
42
+ ```
43
+ ToolSearch({ query: "mcp__magic_patterns", max_results: 5 })
44
+ Empty → magic-patterns: not_configured
45
+ Non-empty → magic-patterns: available
46
+ ```
47
+
48
+ **E — paper.design probe:**
49
+ ```
50
+ ToolSearch({ query: "mcp__paper", max_results: 5 })
51
+ Empty → paper-design: not_configured
52
+ Non-empty → call mcp__paper-design__get_selection; success → available; error → unavailable
53
+ ```
54
+
55
+ **F — pencil.dev probe (file-based):**
56
+ ```bash
57
+ find . -name "*.pen" -not -path "*/node_modules/*" 2>/dev/null | head -1
58
+ Empty → pencil-dev: not_configured
59
+ Found → pencil-dev: available
60
+ ```
61
+
62
+ Write all results to STATE.md `<connections>`.
63
+
64
+ ## Step 1.5 — 21st.dev Prior-Art Check (when 21st-dev: available)
65
+
66
+ If `21st-dev: not_configured` in STATE.md: skip this step entirely.
67
+
68
+ When the explore stage identifies any greenfield component in scope (component name from BRIEF.md or user request that does not yet have an implementation file):
69
+
70
+ 1. `21st_magic_component_search(component_name, limit: 3)`
71
+ 2. Evaluate top result:
72
+ - **fit ≥ 80%**: add `<prior-art>` block to DESIGN.md:
73
+ ```xml
74
+ <prior-art source="21st.dev" component="<name>" fit="<score>%" id="<component_id>">
75
+ Recommendation: adopt — do not build custom. Confirm with design-executor.
76
+ </prior-art>
77
+ ```
78
+ - **fit < 80%**: note top candidate in DESIGN.md as a reference, proceed with custom build:
79
+ ```xml
80
+ <prior-art source="21st.dev" component="<name>" fit="<score>%" id="<component_id>">
81
+ Low fit — noted for reference. Building custom component.
82
+ </prior-art>
83
+ ```
84
+ 3. If `svgl_get_brand_logo` is available and explore scope includes brand logo assets: call `svgl_get_brand_logo(brand_name)` for each required brand asset; add SVG results to `.design/assets/` and note in DESIGN.md.
85
+
86
+ If no greenfield components in scope: skip this step.
35
87
 
36
88
  ## Step 2 — Inventory scan (unless `--skip-scan`)
37
89