@hegemonart/get-design-done 1.19.6 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +60 -0
  4. package/README.md +12 -0
  5. package/agents/design-reflector.md +13 -0
  6. package/connections/connections.md +3 -0
  7. package/connections/figma.md +2 -0
  8. package/connections/gdd-state.md +186 -0
  9. package/hooks/budget-enforcer.ts +716 -0
  10. package/hooks/context-exhaustion.ts +251 -0
  11. package/hooks/gdd-read-injection-scanner.ts +172 -0
  12. package/hooks/hooks.json +3 -3
  13. package/package.json +19 -6
  14. package/reference/config-schema.md +2 -2
  15. package/reference/error-recovery.md +58 -0
  16. package/reference/registry.json +7 -0
  17. package/reference/schemas/budget.schema.json +42 -0
  18. package/reference/schemas/events.schema.json +55 -0
  19. package/reference/schemas/generated.d.ts +419 -0
  20. package/reference/schemas/iteration-budget.schema.json +36 -0
  21. package/reference/schemas/mcp-gdd-state-tools.schema.json +89 -0
  22. package/reference/schemas/rate-limits.schema.json +31 -0
  23. package/scripts/aggregate-agent-metrics.ts +282 -0
  24. package/scripts/codegen-schema-types.ts +149 -0
  25. package/scripts/lib/error-classifier.cjs +232 -0
  26. package/scripts/lib/error-classifier.d.cts +44 -0
  27. package/scripts/lib/event-stream/emitter.ts +88 -0
  28. package/scripts/lib/event-stream/index.ts +154 -0
  29. package/scripts/lib/event-stream/types.ts +127 -0
  30. package/scripts/lib/event-stream/writer.ts +154 -0
  31. package/scripts/lib/gdd-errors/classification.ts +124 -0
  32. package/scripts/lib/gdd-errors/index.ts +218 -0
  33. package/scripts/lib/gdd-state/gates.ts +216 -0
  34. package/scripts/lib/gdd-state/index.ts +167 -0
  35. package/scripts/lib/gdd-state/lockfile.ts +232 -0
  36. package/scripts/lib/gdd-state/mutator.ts +574 -0
  37. package/scripts/lib/gdd-state/parser.ts +523 -0
  38. package/scripts/lib/gdd-state/types.ts +179 -0
  39. package/scripts/lib/iteration-budget.cjs +205 -0
  40. package/scripts/lib/iteration-budget.d.cts +32 -0
  41. package/scripts/lib/jittered-backoff.cjs +112 -0
  42. package/scripts/lib/jittered-backoff.d.cts +38 -0
  43. package/scripts/lib/lockfile.cjs +177 -0
  44. package/scripts/lib/lockfile.d.cts +21 -0
  45. package/scripts/lib/prompt-sanitizer/index.ts +435 -0
  46. package/scripts/lib/prompt-sanitizer/patterns.ts +173 -0
  47. package/scripts/lib/rate-guard.cjs +365 -0
  48. package/scripts/lib/rate-guard.d.cts +38 -0
  49. package/scripts/mcp-servers/gdd-state/schemas/add_blocker.schema.json +67 -0
  50. package/scripts/mcp-servers/gdd-state/schemas/add_decision.schema.json +68 -0
  51. package/scripts/mcp-servers/gdd-state/schemas/add_must_have.schema.json +68 -0
  52. package/scripts/mcp-servers/gdd-state/schemas/checkpoint.schema.json +51 -0
  53. package/scripts/mcp-servers/gdd-state/schemas/frontmatter_update.schema.json +62 -0
  54. package/scripts/mcp-servers/gdd-state/schemas/get.schema.json +51 -0
  55. package/scripts/mcp-servers/gdd-state/schemas/probe_connections.schema.json +75 -0
  56. package/scripts/mcp-servers/gdd-state/schemas/resolve_blocker.schema.json +66 -0
  57. package/scripts/mcp-servers/gdd-state/schemas/set_status.schema.json +47 -0
  58. package/scripts/mcp-servers/gdd-state/schemas/transition_stage.schema.json +70 -0
  59. package/scripts/mcp-servers/gdd-state/schemas/update_progress.schema.json +58 -0
  60. package/scripts/mcp-servers/gdd-state/server.ts +288 -0
  61. package/scripts/mcp-servers/gdd-state/tools/add_blocker.ts +72 -0
  62. package/scripts/mcp-servers/gdd-state/tools/add_decision.ts +89 -0
  63. package/scripts/mcp-servers/gdd-state/tools/add_must_have.ts +113 -0
  64. package/scripts/mcp-servers/gdd-state/tools/checkpoint.ts +60 -0
  65. package/scripts/mcp-servers/gdd-state/tools/frontmatter_update.ts +91 -0
  66. package/scripts/mcp-servers/gdd-state/tools/get.ts +51 -0
  67. package/scripts/mcp-servers/gdd-state/tools/index.ts +51 -0
  68. package/scripts/mcp-servers/gdd-state/tools/probe_connections.ts +73 -0
  69. package/scripts/mcp-servers/gdd-state/tools/resolve_blocker.ts +84 -0
  70. package/scripts/mcp-servers/gdd-state/tools/set_status.ts +54 -0
  71. package/scripts/mcp-servers/gdd-state/tools/shared.ts +194 -0
  72. package/scripts/mcp-servers/gdd-state/tools/transition_stage.ts +80 -0
  73. package/scripts/mcp-servers/gdd-state/tools/update_progress.ts +81 -0
  74. package/scripts/validate-frontmatter.ts +114 -0
  75. package/scripts/validate-schemas.ts +401 -0
  76. package/skills/brief/SKILL.md +15 -6
  77. package/skills/design/SKILL.md +31 -13
  78. package/skills/explore/SKILL.md +41 -17
  79. package/skills/health/SKILL.md +15 -4
  80. package/skills/optimize/SKILL.md +3 -3
  81. package/skills/pause/SKILL.md +16 -10
  82. package/skills/plan/SKILL.md +33 -17
  83. package/skills/progress/SKILL.md +15 -11
  84. package/skills/resume/SKILL.md +19 -10
  85. package/skills/settings/SKILL.md +11 -3
  86. package/skills/todo/SKILL.md +12 -3
  87. package/skills/verify/SKILL.md +65 -29
  88. package/hooks/budget-enforcer.js +0 -329
  89. package/hooks/context-exhaustion.js +0 -127
  90. package/hooks/gdd-read-injection-scanner.js +0 -39
  91. package/scripts/aggregate-agent-metrics.js +0 -173
  92. package/scripts/validate-frontmatter.cjs +0 -68
  93. package/scripts/validate-schemas.cjs +0 -242
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * context-exhaustion.ts — PostToolUse hook
4
+ *
5
+ * Phase 20 Plan 20-13 rewrite of the original context-exhaustion.js.
6
+ * Behavior is byte-equivalent: when tool_response reports context
7
+ * consumption at or above THRESHOLD (default 0.85), the hook writes a
8
+ * <paused> resumption block into .design/STATE.md so the next session
9
+ * can resume with full context. Only writes once per session — if a
10
+ * <paused> block from the same trigger already exists, the hook exits
11
+ * silently.
12
+ *
13
+ * Phase 20 addition: every decision (ok / warn) fires a hook.fired
14
+ * event to .design/telemetry/events.jsonl via appendEvent() (Plan 20-06).
15
+ *
16
+ * Hook type: PostToolUse (any tool)
17
+ * Input: JSON on stdin { tool_name, tool_input, tool_response }
18
+ * Output: JSON on stdout { continue, suppressOutput, message } or nothing
19
+ */
20
+
21
+ import {
22
+ existsSync,
23
+ mkdirSync,
24
+ readFileSync,
25
+ writeFileSync,
26
+ appendFileSync,
27
+ } from 'node:fs';
28
+ import { dirname, join } from 'node:path';
29
+ import { createInterface } from 'node:readline';
30
+
31
+ import { appendEvent } from '../scripts/lib/event-stream/index.ts';
32
+ import type { HookFiredEvent } from '../scripts/lib/event-stream/index.ts';
33
+
34
+ // ── Types ───────────────────────────────────────────────────────────────────
35
+
36
+ interface ToolResponseMeta {
37
+ context_usage?: number | string;
38
+ contextUsage?: number | string;
39
+ }
40
+ interface ToolResponse {
41
+ context_usage?: number | string;
42
+ contextUsage?: number | string;
43
+ metadata?: ToolResponseMeta;
44
+ meta?: ToolResponseMeta;
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ interface HookStdin {
49
+ tool_name?: string;
50
+ tool_input?: Record<string, unknown>;
51
+ tool_response?: ToolResponse;
52
+ [key: string]: unknown;
53
+ }
54
+
55
+ interface HookOutput {
56
+ continue: boolean;
57
+ suppressOutput?: boolean;
58
+ message?: string;
59
+ }
60
+
61
+ /** Hook decision emitted on the event stream. */
62
+ export type HookDecision = 'ok' | 'warn';
63
+
64
+ // ── Constants ───────────────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Context-usage fraction above which the hook paints a <paused> block.
68
+ * Override via GDD_CONTEXT_THRESHOLD env var (float in [0,1]).
69
+ */
70
+ export const THRESHOLD: number = (() => {
71
+ const raw = process.env['GDD_CONTEXT_THRESHOLD'];
72
+ const parsed = raw !== undefined ? Number.parseFloat(raw) : Number.NaN;
73
+ return Number.isFinite(parsed) ? parsed : 0.85;
74
+ })();
75
+
76
+ const STATE_PATH = join(process.cwd(), '.design', 'STATE.md');
77
+
78
+ // ── helpers ─────────────────────────────────────────────────────────────────
79
+
80
+ function now(): string {
81
+ return new Date().toISOString();
82
+ }
83
+
84
+ /**
85
+ * Claude Code injects context usage in several shapes across versions.
86
+ * Try direct fields, then metadata.meta alias, then string forms
87
+ * (fraction or percentage). Returns null when no usage data is present.
88
+ */
89
+ export function extractContextUsage(
90
+ toolResponse: ToolResponse | null | undefined,
91
+ ): number | null {
92
+ if (typeof toolResponse !== 'object' || toolResponse === null) return null;
93
+
94
+ if (typeof toolResponse.context_usage === 'number') return toolResponse.context_usage;
95
+ if (typeof toolResponse.contextUsage === 'number') return toolResponse.contextUsage;
96
+
97
+ const meta: ToolResponseMeta =
98
+ toolResponse.metadata ?? toolResponse.meta ?? {};
99
+ if (typeof meta.context_usage === 'number') return meta.context_usage;
100
+ if (typeof meta.contextUsage === 'number') return meta.contextUsage;
101
+
102
+ const raw =
103
+ toolResponse.context_usage ??
104
+ toolResponse.contextUsage ??
105
+ meta.context_usage ??
106
+ meta.contextUsage;
107
+ if (typeof raw === 'string') {
108
+ if (raw.endsWith('%')) return Number.parseFloat(raw) / 100;
109
+ const n = Number.parseFloat(raw);
110
+ if (Number.isFinite(n)) return n > 1 ? n / 100 : n;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ export function buildPausedBlock(toolName: string, usage: number): string {
116
+ const pct = Math.round(usage * 100);
117
+ const thresholdPct = Math.round(THRESHOLD * 100);
118
+ return `
119
+ <paused>
120
+ recorded: ${now()}
121
+ trigger: context-exhaustion-hook
122
+ context_usage: ${pct}%
123
+ last_tool: ${toolName}
124
+
125
+ ## Resumption instructions
126
+
127
+ Context reached ${pct}% during the previous session (threshold: ${thresholdPct}%).
128
+ The session was auto-paused to preserve quality.
129
+
130
+ To resume:
131
+ 1. Run \`/gdd:resume\` — it will read this block and restore working context
132
+ 2. If mid-plan: check .design/STATE.md for the last completed task
133
+ 3. Re-read the active PLAN.md to orient before continuing
134
+
135
+ Intel store status at pause time:
136
+ ls .design/intel/files.json 2>/dev/null && echo "present" || echo "missing"
137
+ </paused>
138
+ `;
139
+ }
140
+
141
+ export function stateFileHasPausedBlock(): boolean {
142
+ if (!existsSync(STATE_PATH)) return false;
143
+ const content = readFileSync(STATE_PATH, 'utf8');
144
+ return (
145
+ content.includes('<paused>') &&
146
+ content.includes('context-exhaustion-hook')
147
+ );
148
+ }
149
+
150
+ function appendPausedBlock(block: string): void {
151
+ if (!existsSync(dirname(STATE_PATH))) {
152
+ mkdirSync(dirname(STATE_PATH), { recursive: true });
153
+ }
154
+ if (!existsSync(STATE_PATH)) {
155
+ writeFileSync(STATE_PATH, '# Design State\n\n', 'utf8');
156
+ }
157
+ appendFileSync(STATE_PATH, block, 'utf8');
158
+ }
159
+
160
+ // ── event-stream emitter ────────────────────────────────────────────────────
161
+
162
+ let CACHED_SESSION_ID: string | null = null;
163
+ function getSessionId(): string {
164
+ if (CACHED_SESSION_ID === null) {
165
+ const iso = new Date().toISOString().replace(/[:.]/g, '-');
166
+ CACHED_SESSION_ID = `gdd-hook-${iso}-${process.pid}`;
167
+ }
168
+ return CACHED_SESSION_ID;
169
+ }
170
+
171
+ function emitHookFired(decision: HookDecision): void {
172
+ const ev: HookFiredEvent = {
173
+ type: 'hook.fired',
174
+ timestamp: new Date().toISOString(),
175
+ sessionId: getSessionId(),
176
+ payload: { hook: 'context-exhaustion', decision },
177
+ };
178
+ try {
179
+ appendEvent(ev);
180
+ } catch {
181
+ // Fail open — event-stream errors must never block the hook.
182
+ }
183
+ }
184
+
185
+ // ── main ────────────────────────────────────────────────────────────────────
186
+
187
+ async function readStdin(): Promise<string> {
188
+ const rl = createInterface({ input: process.stdin });
189
+ let data = '';
190
+ for await (const line of rl) data += line + '\n';
191
+ return data;
192
+ }
193
+
194
+ export async function main(): Promise<void> {
195
+ const inputData = await readStdin();
196
+
197
+ let parsed: HookStdin;
198
+ try {
199
+ parsed = JSON.parse(inputData) as HookStdin;
200
+ } catch {
201
+ process.exit(0);
202
+ }
203
+
204
+ const toolName =
205
+ typeof parsed.tool_name === 'string' && parsed.tool_name.length > 0
206
+ ? parsed.tool_name
207
+ : 'unknown';
208
+ const toolResponse: ToolResponse = parsed.tool_response ?? {};
209
+
210
+ const usage = extractContextUsage(toolResponse);
211
+
212
+ // No usage data — cannot act. Do not emit a hook.fired event; this
213
+ // is a non-decision, not an "ok" outcome.
214
+ if (usage === null) process.exit(0);
215
+
216
+ // Below threshold — explicit "ok" decision.
217
+ if (usage < THRESHOLD) {
218
+ emitHookFired('ok');
219
+ process.exit(0);
220
+ }
221
+
222
+ // At or above threshold but block already present — emit ok (we did
223
+ // the right thing earlier) and bail.
224
+ if (stateFileHasPausedBlock()) {
225
+ emitHookFired('ok');
226
+ process.exit(0);
227
+ }
228
+
229
+ const block = buildPausedBlock(toolName, usage);
230
+ appendPausedBlock(block);
231
+ emitHookFired('warn');
232
+
233
+ const response: HookOutput = {
234
+ continue: true,
235
+ suppressOutput: false,
236
+ message: `gdd-context-exhaustion: Context at ${Math.round(usage * 100)}% — auto-recorded <paused> block in .design/STATE.md. Run /gdd:resume in the next session to continue.`,
237
+ };
238
+ process.stdout.write(JSON.stringify(response));
239
+ }
240
+
241
+ const isDirectInvocation =
242
+ process.argv[1] !== undefined &&
243
+ /context-exhaustion\.ts$/.test(process.argv[1]);
244
+
245
+ if (isDirectInvocation) {
246
+ main().catch((err: unknown) => {
247
+ const msg = err instanceof Error ? err.message : String(err);
248
+ process.stderr.write(`context-exhaustion hook error: ${msg}\n`);
249
+ process.exit(0);
250
+ });
251
+ }
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gdd-read-injection-scanner.ts — PostToolUse hook (matcher: Read)
4
+ *
5
+ * Phase 20 Plan 20-13 rewrite of the original
6
+ * gdd-read-injection-scanner.js. Scans Read tool output for common
7
+ * prompt-injection patterns and warns (does not block) when suspicious
8
+ * content is found in a read file. Advisory-only — output is a JSON
9
+ * response containing a `message` field the user / agent can act on.
10
+ *
11
+ * Injection patterns come from scripts/injection-patterns.cjs (Tier-2;
12
+ * not TypeScript-converted per Plan 20-00's policy). We require-load
13
+ * them through Node's CJS interop using `createRequire()` — this works
14
+ * under --experimental-strip-types without `package.json "type":"module"`
15
+ * changes and keeps the .cjs module shared with
16
+ * scripts/run-injection-scanner-ci.cjs unchanged.
17
+ *
18
+ * Phase 20 addition: every decision (block / allow) fires a hook.fired
19
+ * event via appendEvent() (Plan 20-06). "block" is used for the
20
+ * warning-emission path even though the hook itself never hard-blocks
21
+ * — it signals the advisory decision to downstream consumers.
22
+ *
23
+ * Hook type: PostToolUse (matcher: Read)
24
+ * Input: JSON on stdin { tool_name, tool_input, tool_response }
25
+ * Output: JSON on stdout { continue, suppressOutput, message } or nothing
26
+ */
27
+
28
+ import { createRequire } from 'node:module';
29
+ import { createInterface } from 'node:readline';
30
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
31
+
32
+ import { appendEvent } from '../scripts/lib/event-stream/index.ts';
33
+ import type { HookFiredEvent } from '../scripts/lib/event-stream/index.ts';
34
+
35
+ // ── require-bridge to the shared .cjs pattern file ──────────────────────────
36
+
37
+ /**
38
+ * Load injection-patterns.cjs through Node's CJS require even though
39
+ * this TS file runs under --experimental-strip-types (which auto-detects
40
+ * ES-module mode). createRequire() can be anchored on any absolute
41
+ * filesystem path (or a `file://` URL string) — we deliberately avoid
42
+ * `import.meta.url` so this module stays compatible with the `Node16`
43
+ * tsconfig module setting without forcing `"type":"module"` in
44
+ * package.json (which would break the Tier-2 .cjs tests per Plan 20-00).
45
+ *
46
+ * Path resolution: when Claude Code invokes the hook, it passes the
47
+ * absolute path as argv[1]. We anchor against that (so the .cjs
48
+ * resolves relative to this file's own directory). Falls back to
49
+ * process.cwd() — scripts/injection-patterns.cjs lives under the
50
+ * package root, which is cwd in both CI and npm script contexts.
51
+ */
52
+ function loadPatterns(): readonly RegExp[] {
53
+ const hookPath =
54
+ typeof process.argv[1] === 'string' && process.argv[1].length > 0
55
+ ? isAbsolute(process.argv[1])
56
+ ? process.argv[1]
57
+ : resolve(process.argv[1])
58
+ : resolve('hooks/gdd-read-injection-scanner.ts');
59
+ const require = createRequire(hookPath);
60
+ const candidatePaths: string[] = [
61
+ join(dirname(hookPath), '..', 'scripts', 'injection-patterns.cjs'),
62
+ join(process.cwd(), 'scripts', 'injection-patterns.cjs'),
63
+ ];
64
+ let lastErr: unknown = null;
65
+ for (const p of candidatePaths) {
66
+ try {
67
+ const mod = require(p) as {
68
+ INJECTION_PATTERNS: Array<{ name: string; re: RegExp }>;
69
+ };
70
+ return mod.INJECTION_PATTERNS.map((entry) => entry.re);
71
+ } catch (err) {
72
+ lastErr = err;
73
+ }
74
+ }
75
+ const msg =
76
+ lastErr instanceof Error ? lastErr.message : String(lastErr);
77
+ throw new Error(
78
+ `gdd-read-injection-scanner: failed to load injection-patterns.cjs (${msg})`,
79
+ );
80
+ }
81
+
82
+ const INJECTION_PATTERNS: readonly RegExp[] = loadPatterns();
83
+
84
+ // ── Types ───────────────────────────────────────────────────────────────────
85
+
86
+ interface HookStdin {
87
+ tool_name?: string;
88
+ tool_input?: { file_path?: string };
89
+ tool_response?: { content?: string };
90
+ [key: string]: unknown;
91
+ }
92
+
93
+ interface HookOutput {
94
+ continue: boolean;
95
+ suppressOutput?: boolean;
96
+ message?: string;
97
+ }
98
+
99
+ /** Hook decision tag for the event stream. */
100
+ export type HookDecision = 'block' | 'allow';
101
+
102
+ // ── event-stream emitter ────────────────────────────────────────────────────
103
+
104
+ let CACHED_SESSION_ID: string | null = null;
105
+ function getSessionId(): string {
106
+ if (CACHED_SESSION_ID === null) {
107
+ const iso = new Date().toISOString().replace(/[:.]/g, '-');
108
+ CACHED_SESSION_ID = `gdd-hook-${iso}-${process.pid}`;
109
+ }
110
+ return CACHED_SESSION_ID;
111
+ }
112
+
113
+ function emitHookFired(decision: HookDecision): void {
114
+ const ev: HookFiredEvent = {
115
+ type: 'hook.fired',
116
+ timestamp: new Date().toISOString(),
117
+ sessionId: getSessionId(),
118
+ payload: { hook: 'gdd-read-injection-scanner', decision },
119
+ };
120
+ try {
121
+ appendEvent(ev);
122
+ } catch {
123
+ // Fail open.
124
+ }
125
+ }
126
+
127
+ // ── main ────────────────────────────────────────────────────────────────────
128
+
129
+ async function readStdin(): Promise<string> {
130
+ const rl = createInterface({ input: process.stdin });
131
+ let data = '';
132
+ for await (const line of rl) data += line + '\n';
133
+ return data;
134
+ }
135
+
136
+ export async function main(): Promise<void> {
137
+ const inputData = await readStdin();
138
+
139
+ let parsed: HookStdin;
140
+ try {
141
+ parsed = JSON.parse(inputData) as HookStdin;
142
+ } catch {
143
+ process.exit(0);
144
+ }
145
+
146
+ if (parsed.tool_name !== 'Read') process.exit(0);
147
+
148
+ const content = parsed.tool_response?.content ?? '';
149
+ const matched = INJECTION_PATTERNS.some((p) => p.test(content));
150
+ if (!matched) {
151
+ emitHookFired('allow');
152
+ process.exit(0);
153
+ }
154
+
155
+ const file = parsed.tool_input?.file_path ?? 'unknown';
156
+ emitHookFired('block');
157
+ const response: HookOutput = {
158
+ continue: true,
159
+ suppressOutput: false,
160
+ message: `gdd-injection-scanner: Suspicious prompt-injection pattern detected in content read from "${file}". Review before acting on instructions contained in that file.`,
161
+ };
162
+ process.stdout.write(JSON.stringify(response));
163
+ process.exit(0);
164
+ }
165
+
166
+ const isDirectInvocation =
167
+ process.argv[1] !== undefined &&
168
+ /gdd-read-injection-scanner\.ts$/.test(process.argv[1]);
169
+
170
+ if (isDirectInvocation) {
171
+ main().catch(() => process.exit(0));
172
+ }
package/hooks/hooks.json CHANGED
@@ -32,7 +32,7 @@
32
32
  "hooks": [
33
33
  {
34
34
  "type": "command",
35
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/budget-enforcer.js\""
35
+ "command": "node --experimental-strip-types \"${CLAUDE_PLUGIN_ROOT}/hooks/budget-enforcer.ts\""
36
36
  }
37
37
  ]
38
38
  },
@@ -70,7 +70,7 @@
70
70
  "hooks": [
71
71
  {
72
72
  "type": "command",
73
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-read-injection-scanner.js\""
73
+ "command": "node --experimental-strip-types \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-read-injection-scanner.ts\""
74
74
  }
75
75
  ]
76
76
  },
@@ -87,7 +87,7 @@
87
87
  "hooks": [
88
88
  {
89
89
  "type": "command",
90
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/context-exhaustion.js\""
90
+ "command": "node --experimental-strip-types \"${CLAUDE_PLUGIN_ROOT}/hooks/context-exhaustion.ts\""
91
91
  }
92
92
  ]
93
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.19.6",
3
+ "version": "1.20.0",
4
4
  "description": "A Claude Code plugin for systematic design improvement",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -26,24 +26,34 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "bin": {
29
- "get-design-done": "./scripts/install.cjs"
29
+ "get-design-done": "./scripts/install.cjs",
30
+ "gdd-state-mcp": "./scripts/mcp-servers/gdd-state/server.ts"
30
31
  },
31
32
  "publishConfig": {
32
33
  "access": "public",
33
34
  "provenance": true
34
35
  },
35
36
  "scripts": {
36
- "test": "node --test \"tests/**/*.cjs\"",
37
+ "test": "node --test --experimental-strip-types \"tests/**/*.cjs\" \"tests/**/*.ts\"",
38
+ "typecheck": "tsc --noEmit",
39
+ "codegen:schemas": "node --experimental-strip-types scripts/codegen-schema-types.ts",
37
40
  "lint:md": "npx --yes markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.planning\" \"#.claude\" \"#test-fixture/baselines\"",
38
41
  "lint:links": "npx --yes lychee --no-progress --accept 200,206,403,429 \"**/*.md\" || true",
39
- "validate:schemas": "node scripts/validate-schemas.cjs",
40
- "validate:frontmatter": "node scripts/validate-frontmatter.cjs agents/",
42
+ "validate:schemas": "node --experimental-strip-types scripts/validate-schemas.ts",
43
+ "validate:frontmatter": "node --experimental-strip-types scripts/validate-frontmatter.ts agents/",
41
44
  "detect:stale-refs": "node scripts/detect-stale-refs.cjs",
42
45
  "scan:injection": "node scripts/run-injection-scanner-ci.cjs",
43
46
  "test:size-budget": "node --test tests/agent-size-budget.test.cjs",
44
47
  "release:extract-changelog": "node scripts/extract-changelog-section.cjs",
45
48
  "verify:version-sync": "node scripts/verify-version-sync.cjs"
46
49
  },
50
+ "devDependencies": {
51
+ "@types/node": "^22.0.0",
52
+ "ajv-cli": "^5.0.0",
53
+ "ajv-formats": "^3.0.1",
54
+ "json-schema-to-typescript": "^15.0.0",
55
+ "typescript": "^5.5.0"
56
+ },
47
57
  "keywords": [
48
58
  "claude",
49
59
  "claude-code",
@@ -103,5 +113,8 @@
103
113
  "skills": [
104
114
  "SKILL.md"
105
115
  ],
106
- "hooks": "hooks/hooks.json"
116
+ "hooks": "hooks/hooks.json",
117
+ "dependencies": {
118
+ "@modelcontextprotocol/sdk": "^1.0.0"
119
+ }
107
120
  }
@@ -185,7 +185,7 @@ If `.design/budget.json` is missing when any `/gdd:*` command runs, `scripts/boo
185
185
 
186
186
  ## .design/telemetry/costs.jsonl + .design/agent-metrics.json (Phase 10.1)
187
187
 
188
- Phase 10.1 introduces two measurement artifacts written by `hooks/budget-enforcer.js` (PreToolUse on `Agent` spawns) and `scripts/aggregate-agent-metrics.js` (detached child of the hook + refresh step of `/gdd:optimize`). Both files live under the gitignored `.design/` directory — they are local session state, not committed.
188
+ Phase 10.1 introduces two measurement artifacts written by `hooks/budget-enforcer.js` (PreToolUse on `Agent` spawns) and `scripts/aggregate-agent-metrics.ts` (detached child of the hook + refresh step of `/gdd:optimize`). Both files live under the gitignored `.design/` directory — they are local session state, not committed.
189
189
 
190
190
  ### .design/telemetry/costs.jsonl
191
191
 
@@ -223,7 +223,7 @@ Append-only ledger. One JSON object per line. Written by `hooks/budget-enforcer.
223
223
 
224
224
  ### .design/agent-metrics.json
225
225
 
226
- Per-agent aggregate derived from `costs.jsonl` by `scripts/aggregate-agent-metrics.js`. Written atomically via tmp-file + rename. Overwritten in full on every refresh — not append-only. Consumers should treat it as a snapshot.
226
+ Per-agent aggregate derived from `costs.jsonl` by `scripts/aggregate-agent-metrics.ts`. Written atomically via tmp-file + rename. Overwritten in full on every refresh — not append-only. Consumers should treat it as a snapshot.
227
227
 
228
228
  **Schema:**
229
229
 
@@ -0,0 +1,58 @@
1
+ # Error recovery
2
+
3
+ This is the recovery-action protocol for low-level errors inside the GDD pipeline. It sits on top of `scripts/lib/error-classifier.cjs` (Plan 20-14) and references the rate-guard, jittered-backoff, and iteration-budget primitives.
4
+
5
+ ## Recovery protocol
6
+
7
+ On `status=413` or `context_overflow`, re-emit with compressed context (drop oldest non-system turns, target 50% reduction, retry once).
8
+
9
+ On `status=429`, consult `scripts/lib/rate-guard.cjs` → `blockUntilReady(provider)` before retry.
10
+
11
+ On network-transient (5xx, ECONNRESET), use jittered backoff (`scripts/lib/jittered-backoff.cjs`); max 3 retries.
12
+
13
+ On auth-error, surface to user — do not retry.
14
+
15
+ ## Recovery-action table
16
+
17
+ The `FailoverReason` enum in `scripts/lib/error-classifier.cjs` has eight values. Each row below is the canonical recovery action for one of those values. The classifier's `suggestedAction` field returns a one-liner drawn from this table; this doc is the authoritative long form.
18
+
19
+ | FailoverReason | Retryable | Action |
20
+ | ------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `rate_limited` | yes | Call `rate-guard.ingestHeaders(provider, response.headers)` to record the rate-limit signal, then `rate-guard.blockUntilReady(provider)` before retrying. The blocker waits until `resetAt` on disk — synchronized siblings (watch-authorities + update-check) therefore share the backoff boundary. After the block returns, retry with jittered backoff at attempt 0. |
22
+ | `context_overflow` | yes | Compress context — drop the oldest non-system turns (or the oldest attachments) targeting roughly 50 % token reduction. Retry **once** with the compressed payload. If the retry also raises `context_overflow`, escalate to the user as an unrecoverable block — further compression destroys information. |
23
+ | `auth_error` | no | Surface the error to the user with actionable text: which credential, which provider, and the renewal path (OAuth re-auth URL, API-key environment variable, etc.). Do not retry automatically — a loop would just multiply the failure. |
24
+ | `network_transient` | yes | Retry with `scripts/lib/jittered-backoff.cjs` — `await sleep(attempt)` inside a bounded loop. Cap at 3 attempts before giving up. When retries exhaust, reclassify as `network_permanent` and surface to the user. |
25
+ | `network_permanent` | no | Surface to user. The endpoint is wrong, DNS is broken, or the resource was removed. A retry without operator action will just re-fail. |
26
+ | `tool_not_found` | no | Surface to user. Either the tool name drifted (common for MCP servers whose prefixes change across sessions) or the MCP is not registered. Reprobe via the connection's probe sequence before retrying anything. |
27
+ | `validation` | no | Surface the validation detail to the caller. Do not retry the same input — 4xx is the server saying "your payload is wrong". Fixing the payload is caller work. |
28
+ | `unknown` | no | Surface the raw error to the user. Do not retry — we can't tell whether it's safe. Add a telemetry row so we can tighten the classifier over time. |
29
+
30
+ ## Integration points
31
+
32
+ | Caller | When to classify | What to do with `reason` |
33
+ | ------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
34
+ | `hooks/budget-enforcer.ts` | pre-spawn rate-guard check (Plan 20-14) | If upstream state already shows `rate_limited`, emit `decision: 'rate-limited'` and short-circuit before any spawn. |
35
+ | Figma MCP probe | live `get_metadata` call errors | `network_transient` → jittered-backoff retry. `auth_error` → STOP with a reauth note. `rate_limited` → block then retry. |
36
+ | Watch-authorities fetcher | per-feed HTTP fetch | Same policy as Figma probe; `validation` also possible on ETag stalemate (304). |
37
+ | Update-check HTTP curl | GitHub `releases/latest` fetch | Silent failure by D-04 of Plan 13.3 — classify but don't surface; log and exit 0. |
38
+ | MCP transport | tool-call errors (gdd-state, figma, 21st-dev)| Map `tool_not_found` to a probe-reissue; map `auth_error` to STOP; retry transient classes via the caller's own loop. |
39
+
40
+ ## Fix-loop iteration interaction
41
+
42
+ Retries consume iteration budget when paired with the Layer-B cache:
43
+
44
+ 1. On cache hit, `iteration-budget.refund(1)` preserves the iteration that would otherwise have been spent.
45
+ 2. On each actual retry that does real work (no cache hit), the caller `iteration-budget.consume(1)` before the spawn.
46
+ 3. When the budget's `remaining === 0`, further retries throw `IterationBudgetExhaustedError` and the caller must surface to user — a retry cycle has become pathological.
47
+
48
+ This protects the "infinite fix loop" case — a blocker that regenerates after every fix — from burning unbounded context.
49
+
50
+ ## Telemetry
51
+
52
+ Every classification result that leads to a retry or a surfaced error should append an event to `.design/telemetry/events.jsonl`:
53
+
54
+ ```json
55
+ { "type": "error.classified", "timestamp": "…", "sessionId": "…", "payload": { "reason": "rate_limited", "retryable": true, "caller": "figma-probe" } }
56
+ ```
57
+
58
+ The event subtype is defined in `scripts/lib/event-stream/types.ts`. Consumers (`gdd-reflector`, dashboard) aggregate by `reason` to detect classifier drift — if `unknown` spikes, the classifier needs tightening.
@@ -3,6 +3,13 @@
3
3
  "version": 1,
4
4
  "generated_at": "2026-04-24T00:00:00.000Z",
5
5
  "entries": [
6
+ {
7
+ "name": "error-recovery",
8
+ "path": "reference/error-recovery.md",
9
+ "type": "meta-rules",
10
+ "phase": 20,
11
+ "description": "Phase 20 resilience recovery protocol — rate-limit + 429 + context-overflow retry guidance for the SDK runner (jittered-backoff / rate-guard / error-classifier / iteration-budget integration)"
12
+ },
6
13
  {
7
14
  "name": "component-authoring",
8
15
  "path": "reference/component-authoring.md",
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://get-design-done.example/schemas/budget.schema.json",
4
+ "title": ".design/budget.json",
5
+ "description": "Shape of .design/budget.json — the Phase 10.1 optimization-layer budget governance file. Consumed by hooks/budget-enforcer.ts on every PreToolUse:Agent spawn. Bootstrap writes the Default Config from reference/config-schema.md if the file is missing.",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "properties": {
9
+ "per_task_cap_usd": {
10
+ "type": "number",
11
+ "minimum": 0,
12
+ "description": "Hard ceiling per agent spawn (USD). Breach under enforcement_mode=enforce triggers D-02 block."
13
+ },
14
+ "per_phase_cap_usd": {
15
+ "type": "number",
16
+ "minimum": 0,
17
+ "description": "Cumulative ceiling across all spawns within the current phase (USD). Read from .design/STATE.md frontmatter `phase:` field."
18
+ },
19
+ "tier_overrides": {
20
+ "type": "object",
21
+ "additionalProperties": {
22
+ "type": "string",
23
+ "enum": ["haiku", "sonnet", "opus"]
24
+ },
25
+ "description": "Per-agent tier override map (agent-name -> tier). Wins over agent frontmatter default-tier per D-04."
26
+ },
27
+ "auto_downgrade_on_cap": {
28
+ "type": "boolean",
29
+ "description": "When true, hook silently rewrites tier -> haiku at 80% of per_task_cap_usd per D-03; logged as tier_downgraded: true in telemetry."
30
+ },
31
+ "cache_ttl_seconds": {
32
+ "type": "integer",
33
+ "minimum": 0,
34
+ "description": "TTL (seconds) driving .design/cache-manifest.json entry expiry per D-08 Layer B. Default 3600."
35
+ },
36
+ "enforcement_mode": {
37
+ "type": "string",
38
+ "enum": ["enforce", "warn", "log"],
39
+ "description": "D-11 enforcement policy. enforce = block + auto-downgrade; warn = print warnings but allow spawn; log = advisory-only telemetry without gating."
40
+ }
41
+ }
42
+ }