@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,365 @@
1
+ // scripts/lib/rate-guard.cjs
2
+ //
3
+ // Plan 20-14 — per-provider rate-limit bookkeeping.
4
+ //
5
+ // Parses the standard rate-limit header families and persists per-provider
6
+ // state to `.design/rate-limits/<provider>.json` under atomic-write +
7
+ // file-lock. Consumed by `hooks/budget-enforcer.ts` to short-circuit agent
8
+ // spawns when the provider is rate-limited.
9
+ //
10
+ // Header families supported (case-insensitive keys):
11
+ // - retry-after: seconds (numeric) or HTTP-date (per RFC 7231 §7.1.3)
12
+ // - x-ratelimit-remaining-requests / -tokens: integer
13
+ // - x-ratelimit-reset-requests / -tokens: Unix seconds
14
+ // - anthropic-ratelimit-requests-remaining: integer
15
+ // - anthropic-ratelimit-requests-reset: ISO-8601 timestamp
16
+ //
17
+ // Most-restrictive precedence: when a single response carries multiple
18
+ // limit-ish headers (e.g. x-ratelimit-remaining-requests AND
19
+ // x-ratelimit-remaining-tokens) the lower `remaining` wins and the later
20
+ // `resetAt` wins. This matches the D-01 rule in the plan.
21
+ //
22
+ // State-file writes are atomic (temp + rename) and coordinated by
23
+ // scripts/lib/lockfile.cjs so two child processes hitting `ingestHeaders`
24
+ // concurrently can never corrupt the file. The lock is scoped to the
25
+ // state file, not to the provider directory, so different providers can
26
+ // update in parallel.
27
+
28
+ 'use strict';
29
+
30
+ const fs = require('node:fs');
31
+ const path = require('node:path');
32
+
33
+ const { acquire } = require('./lockfile.cjs');
34
+
35
+ const STATE_DIR_REL = path.join('.design', 'rate-limits');
36
+ const LOCK_MAX_WAIT_MS = 3_000;
37
+
38
+ /**
39
+ * Resolve the state-file path for a given provider. `provider` is
40
+ * normalized to lowercase with basic sanitization so callers can't
41
+ * escape the rate-limits directory via "../" or similar.
42
+ */
43
+ function stateFileFor(provider) {
44
+ if (typeof provider !== 'string' || provider.length === 0) {
45
+ throw new Error('rate-guard: provider must be a non-empty string');
46
+ }
47
+ // Reject path separators and control characters; keep alnum + hyphen +
48
+ // underscore + dot only. No .. allowed.
49
+ const normalized = provider.toLowerCase().trim();
50
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) {
51
+ throw new Error(`rate-guard: provider name '${provider}' contains illegal characters`);
52
+ }
53
+ return path.join(process.cwd(), STATE_DIR_REL, `${normalized}.json`);
54
+ }
55
+
56
+ /**
57
+ * Normalize a headers record to case-insensitive lookup. Accepts plain
58
+ * objects, Map, and the native Fetch Headers interface.
59
+ */
60
+ function getHeader(headers, name) {
61
+ if (headers === null || headers === undefined) return undefined;
62
+ const lowerName = name.toLowerCase();
63
+
64
+ // Native Headers
65
+ if (typeof headers.get === 'function') {
66
+ const v = headers.get(lowerName);
67
+ return v === null ? undefined : v;
68
+ }
69
+
70
+ // Map
71
+ if (headers instanceof Map) {
72
+ for (const [k, v] of headers.entries()) {
73
+ if (typeof k === 'string' && k.toLowerCase() === lowerName) return v;
74
+ }
75
+ return undefined;
76
+ }
77
+
78
+ // Plain object — walk keys case-insensitively.
79
+ if (typeof headers === 'object') {
80
+ for (const k of Object.keys(headers)) {
81
+ if (k.toLowerCase() === lowerName) return headers[k];
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+
87
+ /**
88
+ * Parse a `retry-after` value into an ISO resetAt.
89
+ * Accepts:
90
+ * - integer seconds ("30")
91
+ * - HTTP-date ("Mon, 24 Apr 2026 00:00:00 GMT")
92
+ * Returns null if the value can't be interpreted.
93
+ */
94
+ function parseRetryAfter(raw) {
95
+ if (raw === undefined || raw === null) return null;
96
+ const v = String(raw).trim();
97
+ if (v === '') return null;
98
+
99
+ // Pure integer → seconds.
100
+ if (/^\d+$/.test(v)) {
101
+ const secs = Number(v);
102
+ if (!Number.isFinite(secs)) return null;
103
+ return new Date(Date.now() + secs * 1000).toISOString();
104
+ }
105
+
106
+ // HTTP-date (RFC 7231). Date.parse handles RFC 1123 / RFC 850 / asctime.
107
+ const t = Date.parse(v);
108
+ if (!Number.isFinite(t)) return null;
109
+ return new Date(t).toISOString();
110
+ }
111
+
112
+ /** Parse Unix-seconds integer to ISO. */
113
+ function parseUnixSeconds(raw) {
114
+ if (raw === undefined || raw === null) return null;
115
+ const v = String(raw).trim();
116
+ if (!/^\d+$/.test(v)) return null;
117
+ const secs = Number(v);
118
+ if (!Number.isFinite(secs) || secs <= 0) return null;
119
+ return new Date(secs * 1000).toISOString();
120
+ }
121
+
122
+ /** Parse ISO-8601 to ISO (normalized). Returns null on garbage. */
123
+ function parseIso(raw) {
124
+ if (raw === undefined || raw === null) return null;
125
+ const v = String(raw).trim();
126
+ const t = Date.parse(v);
127
+ if (!Number.isFinite(t)) return null;
128
+ return new Date(t).toISOString();
129
+ }
130
+
131
+ /** Parse an integer remaining-count. Returns null on non-numeric. */
132
+ function parseInt32(raw) {
133
+ if (raw === undefined || raw === null) return null;
134
+ const v = String(raw).trim();
135
+ if (!/^-?\d+$/.test(v)) return null;
136
+ const n = Number(v);
137
+ if (!Number.isFinite(n)) return null;
138
+ return Math.max(0, Math.floor(n));
139
+ }
140
+
141
+ /**
142
+ * Distill a headers object into a `{ remaining, resetAt }` tuple using
143
+ * most-restrictive precedence. Either field can end up null if no
144
+ * relevant header is present; caller decides what that means.
145
+ */
146
+ function distill(headers) {
147
+ const remainings = [];
148
+ const resets = [];
149
+
150
+ // OpenAI-style / de-facto standard
151
+ const remReq = parseInt32(getHeader(headers, 'x-ratelimit-remaining-requests'));
152
+ const remTok = parseInt32(getHeader(headers, 'x-ratelimit-remaining-tokens'));
153
+ const resReq = parseUnixSeconds(getHeader(headers, 'x-ratelimit-reset-requests'));
154
+ const resTok = parseUnixSeconds(getHeader(headers, 'x-ratelimit-reset-tokens'));
155
+
156
+ if (remReq !== null) remainings.push(remReq);
157
+ if (remTok !== null) remainings.push(remTok);
158
+ if (resReq !== null) resets.push(resReq);
159
+ if (resTok !== null) resets.push(resTok);
160
+
161
+ // Anthropic-style
162
+ const anRem = parseInt32(getHeader(headers, 'anthropic-ratelimit-requests-remaining'));
163
+ const anReset = parseIso(getHeader(headers, 'anthropic-ratelimit-requests-reset'));
164
+ if (anRem !== null) remainings.push(anRem);
165
+ if (anReset !== null) resets.push(anReset);
166
+
167
+ // Retry-After (fallback/coarsest)
168
+ const retryAfter = parseRetryAfter(getHeader(headers, 'retry-after'));
169
+ if (retryAfter !== null) {
170
+ // retry-after implies the remaining budget is effectively zero right
171
+ // now — otherwise the server wouldn't be asking us to back off.
172
+ remainings.push(0);
173
+ resets.push(retryAfter);
174
+ }
175
+
176
+ const remaining = remainings.length === 0
177
+ ? null
178
+ : remainings.reduce((a, b) => (b < a ? b : a), Number.POSITIVE_INFINITY);
179
+ // Most-restrictive = LATEST reset (we wait longer), not earliest.
180
+ const resetAt = resets.length === 0
181
+ ? null
182
+ : resets
183
+ .map((s) => Date.parse(s))
184
+ .filter(Number.isFinite)
185
+ .reduce((a, b) => (b > a ? b : a), 0);
186
+
187
+ return {
188
+ remaining: remaining === null ? null : Number(remaining),
189
+ resetAt: resetAt === null || resetAt === 0 ? null : new Date(resetAt).toISOString(),
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Atomically write `state` to the provider state file under the
195
+ * advisory lock. The `.tmp` file is created in the same directory as
196
+ * the target so the rename is atomic on every POSIX filesystem and on
197
+ * NTFS.
198
+ */
199
+ async function atomicWriteState(absPath, state) {
200
+ const dir = path.dirname(absPath);
201
+ fs.mkdirSync(dir, { recursive: true });
202
+ const release = await acquire(absPath, { maxWaitMs: LOCK_MAX_WAIT_MS });
203
+ try {
204
+ const tmp = `${absPath}.tmp.${process.pid}.${Date.now()}`;
205
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8');
206
+ fs.renameSync(tmp, absPath);
207
+ } finally {
208
+ await release();
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Merge the incoming distilled values with any existing state on disk
214
+ * using most-restrictive precedence across calls, not just within a
215
+ * single headers object. This matters when responses arrive
216
+ * out-of-order and an older, less restrictive header shouldn't clobber
217
+ * a newer, more restrictive one.
218
+ */
219
+ function mergeState(existing, provider, incoming) {
220
+ const now = new Date().toISOString();
221
+
222
+ // Start from the existing state if it's shaped correctly.
223
+ const hasExisting = existing &&
224
+ typeof existing === 'object' &&
225
+ existing.provider === provider &&
226
+ Number.isFinite(existing.remaining) &&
227
+ typeof existing.resetAt === 'string';
228
+
229
+ let remaining;
230
+ if (incoming.remaining !== null && hasExisting) {
231
+ remaining = Math.min(existing.remaining, incoming.remaining);
232
+ } else if (incoming.remaining !== null) {
233
+ remaining = incoming.remaining;
234
+ } else if (hasExisting) {
235
+ remaining = existing.remaining;
236
+ } else {
237
+ // No incoming signal, no prior state — defaulting to 1 would be wrong
238
+ // (implies "plenty of budget"), defaulting to 0 would be wrong (blocks
239
+ // the next caller). We leave the provider unset in that case by
240
+ // returning null — callers treat "no state" and "remaining > 0" the
241
+ // same way.
242
+ return null;
243
+ }
244
+
245
+ let resetAt;
246
+ if (incoming.resetAt !== null && hasExisting) {
247
+ // Pick the latest reset — "wait longer" is the conservative choice.
248
+ const a = Date.parse(existing.resetAt);
249
+ const b = Date.parse(incoming.resetAt);
250
+ resetAt = new Date(Math.max(a || 0, b || 0)).toISOString();
251
+ } else if (incoming.resetAt !== null) {
252
+ resetAt = incoming.resetAt;
253
+ } else if (hasExisting) {
254
+ resetAt = existing.resetAt;
255
+ } else {
256
+ // remaining is set but resetAt is unknown — default to now + 60s so
257
+ // callers that wake on resetAt eventually get out of the block.
258
+ resetAt = new Date(Date.now() + 60_000).toISOString();
259
+ }
260
+
261
+ return {
262
+ provider,
263
+ remaining,
264
+ resetAt,
265
+ updatedAt: now,
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Ingest rate-limit headers for a provider. Parses the known header
271
+ * families, merges with any existing state, and persists atomically.
272
+ *
273
+ * @param {string} provider provider key (normalized to lowercase)
274
+ * @param {object} headers response headers (plain object, Map, or Fetch Headers)
275
+ * @returns {Promise<object|null>} the persisted state, or null when no
276
+ * rate-limit signal was found in the headers.
277
+ */
278
+ async function ingestHeaders(provider, headers) {
279
+ const absPath = stateFileFor(provider);
280
+ const incoming = distill(headers);
281
+
282
+ // If nothing relevant was found, no state change.
283
+ if (incoming.remaining === null && incoming.resetAt === null) {
284
+ return null;
285
+ }
286
+
287
+ // Read existing (best-effort; tolerate missing/corrupt).
288
+ let existing = null;
289
+ try {
290
+ if (fs.existsSync(absPath)) {
291
+ existing = JSON.parse(fs.readFileSync(absPath, 'utf8'));
292
+ }
293
+ } catch { /* corrupt → treat as no state */ }
294
+
295
+ const merged = mergeState(existing, provider.toLowerCase().trim(), incoming);
296
+ if (merged === null) return null;
297
+
298
+ await atomicWriteState(absPath, merged);
299
+ return merged;
300
+ }
301
+
302
+ /**
303
+ * Return current state for a provider, or null on miss / parse failure.
304
+ * Callers treat null identically to "no constraint — proceed".
305
+ *
306
+ * @param {string} provider
307
+ * @returns {{provider: string, remaining: number, resetAt: string, updatedAt: string}|null}
308
+ */
309
+ function remaining(provider) {
310
+ const absPath = stateFileFor(provider);
311
+ try {
312
+ if (!fs.existsSync(absPath)) return null;
313
+ const raw = fs.readFileSync(absPath, 'utf8');
314
+ const parsed = JSON.parse(raw);
315
+ if (
316
+ parsed &&
317
+ typeof parsed === 'object' &&
318
+ typeof parsed.provider === 'string' &&
319
+ Number.isFinite(parsed.remaining) &&
320
+ typeof parsed.resetAt === 'string' &&
321
+ typeof parsed.updatedAt === 'string'
322
+ ) {
323
+ // If the reset window has already passed, treat as expired so
324
+ // callers get a clean "no constraint" signal without us having to
325
+ // rewrite the file.
326
+ const resetMs = Date.parse(parsed.resetAt);
327
+ if (Number.isFinite(resetMs) && resetMs <= Date.now()) return null;
328
+ return parsed;
329
+ }
330
+ return null;
331
+ } catch {
332
+ return null;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * If `remaining <= 0`, wait until `resetAt` before resolving. Returns
338
+ * the number of ms actually waited (0 when there was no constraint or
339
+ * the window was already expired).
340
+ *
341
+ * @param {string} provider
342
+ * @returns {Promise<number>}
343
+ */
344
+ async function blockUntilReady(provider) {
345
+ const state = remaining(provider);
346
+ if (state === null) return 0;
347
+ if (state.remaining > 0) return 0;
348
+
349
+ const resetMs = Date.parse(state.resetAt);
350
+ if (!Number.isFinite(resetMs)) return 0;
351
+
352
+ const waitMs = resetMs - Date.now();
353
+ if (waitMs <= 0) return 0;
354
+
355
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
356
+ return waitMs;
357
+ }
358
+
359
+ module.exports = {
360
+ ingestHeaders,
361
+ remaining,
362
+ blockUntilReady,
363
+ // internal helpers surfaced for tests only — stable API = first three.
364
+ _internal: { distill, parseRetryAfter, parseUnixSeconds, parseIso, stateFileFor },
365
+ };
@@ -0,0 +1,38 @@
1
+ // scripts/lib/rate-guard.d.cts — types for rate-guard.cjs.
2
+
3
+ export interface ProviderState {
4
+ provider: string;
5
+ remaining: number;
6
+ resetAt: string; // ISO-8601
7
+ updatedAt: string; // ISO-8601
8
+ }
9
+
10
+ /** Headers shape accepted by {@link ingestHeaders}. */
11
+ export type HeadersLike =
12
+ | Record<string, string | number | undefined>
13
+ | Map<string, string>
14
+ | Headers;
15
+
16
+ /**
17
+ * Parse rate-limit headers for a provider, merge with any prior state
18
+ * (most-restrictive precedence), and persist atomically to
19
+ * `.design/rate-limits/<provider>.json`.
20
+ *
21
+ * Returns the persisted state, or `null` if no rate-limit signal was
22
+ * present in the headers.
23
+ */
24
+ export function ingestHeaders(provider: string, headers: HeadersLike): Promise<ProviderState | null>;
25
+
26
+ /**
27
+ * Return the current state for a provider, or `null` when no state
28
+ * exists, the file is corrupt, or the rate-limit window has already
29
+ * reset. Callers treat `null` as "no constraint".
30
+ */
31
+ export function remaining(provider: string): ProviderState | null;
32
+
33
+ /**
34
+ * If the provider's current `remaining <= 0`, wait until `resetAt`
35
+ * before resolving. Returns the number of ms actually waited (0 when
36
+ * there was no constraint or the window was already expired).
37
+ */
38
+ export function blockUntilReady(provider: string): Promise<number>;
@@ -0,0 +1,67 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/hegemonart/get-design-done/main/scripts/mcp-servers/gdd-state/schemas/add_blocker.schema.json",
4
+ "title": "GddStateAddBlocker",
5
+ "description": "MCP tool gdd_state__add_blocker — append one entry to <blockers>. Emits state.mutation.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["input", "output"],
9
+ "properties": {
10
+ "input": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["text"],
14
+ "properties": {
15
+ "text": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "description": "Human-readable blocker description."
19
+ },
20
+ "stage": {
21
+ "type": "string",
22
+ "description": "Optional. Defaults to <position>.stage."
23
+ },
24
+ "date": {
25
+ "type": "string",
26
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
27
+ "description": "Optional ISO date (YYYY-MM-DD). Defaults to today (UTC)."
28
+ }
29
+ }
30
+ },
31
+ "output": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "required": ["success"],
35
+ "properties": {
36
+ "success": { "type": "boolean" },
37
+ "data": {
38
+ "type": "object",
39
+ "additionalProperties": true,
40
+ "properties": {
41
+ "blocker": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": ["stage", "date", "text"],
45
+ "properties": {
46
+ "stage": { "type": "string" },
47
+ "date": { "type": "string" },
48
+ "text": { "type": "string" }
49
+ }
50
+ },
51
+ "count": { "type": "integer", "minimum": 1 }
52
+ }
53
+ },
54
+ "error": {
55
+ "type": "object",
56
+ "additionalProperties": true,
57
+ "required": ["code", "message", "kind"],
58
+ "properties": {
59
+ "code": { "type": "string" },
60
+ "message": { "type": "string" },
61
+ "kind": { "type": "string", "enum": ["validation", "state_conflict", "operation_failed", "unknown"] }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/hegemonart/get-design-done/main/scripts/mcp-servers/gdd-state/schemas/add_decision.schema.json",
4
+ "title": "GddStateAddDecision",
5
+ "description": "MCP tool gdd_state__add_decision — append one entry to <decisions>. Emits state.mutation.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["input", "output"],
9
+ "properties": {
10
+ "input": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["text"],
14
+ "properties": {
15
+ "text": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "description": "Human-readable decision text."
19
+ },
20
+ "status": {
21
+ "type": "string",
22
+ "enum": ["locked", "tentative"],
23
+ "description": "Defaults to tentative."
24
+ },
25
+ "id": {
26
+ "type": "string",
27
+ "pattern": "^D-[0-9]+$",
28
+ "description": "Optional. Defaults to next available D-N based on existing decisions."
29
+ }
30
+ }
31
+ },
32
+ "output": {
33
+ "type": "object",
34
+ "additionalProperties": false,
35
+ "required": ["success"],
36
+ "properties": {
37
+ "success": { "type": "boolean" },
38
+ "data": {
39
+ "type": "object",
40
+ "additionalProperties": true,
41
+ "properties": {
42
+ "decision": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["id", "text", "status"],
46
+ "properties": {
47
+ "id": { "type": "string" },
48
+ "text": { "type": "string" },
49
+ "status": { "type": "string", "enum": ["locked", "tentative"] }
50
+ }
51
+ },
52
+ "count": { "type": "integer", "minimum": 1 }
53
+ }
54
+ },
55
+ "error": {
56
+ "type": "object",
57
+ "additionalProperties": true,
58
+ "required": ["code", "message", "kind"],
59
+ "properties": {
60
+ "code": { "type": "string" },
61
+ "message": { "type": "string" },
62
+ "kind": { "type": "string", "enum": ["validation", "state_conflict", "operation_failed", "unknown"] }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/hegemonart/get-design-done/main/scripts/mcp-servers/gdd-state/schemas/add_must_have.schema.json",
4
+ "title": "GddStateAddMustHave",
5
+ "description": "MCP tool gdd_state__add_must_have — append one entry to <must_haves>. Emits state.mutation.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["input", "output"],
9
+ "properties": {
10
+ "input": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["text"],
14
+ "properties": {
15
+ "text": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "description": "Human-readable must-have criterion."
19
+ },
20
+ "status": {
21
+ "type": "string",
22
+ "enum": ["pending", "pass", "fail"],
23
+ "description": "Defaults to pending."
24
+ },
25
+ "id": {
26
+ "type": "string",
27
+ "pattern": "^M-[0-9]+$",
28
+ "description": "Optional. Defaults to next available M-N based on existing must_haves."
29
+ }
30
+ }
31
+ },
32
+ "output": {
33
+ "type": "object",
34
+ "additionalProperties": false,
35
+ "required": ["success"],
36
+ "properties": {
37
+ "success": { "type": "boolean" },
38
+ "data": {
39
+ "type": "object",
40
+ "additionalProperties": true,
41
+ "properties": {
42
+ "must_have": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["id", "text", "status"],
46
+ "properties": {
47
+ "id": { "type": "string" },
48
+ "text": { "type": "string" },
49
+ "status": { "type": "string", "enum": ["pending", "pass", "fail"] }
50
+ }
51
+ },
52
+ "count": { "type": "integer", "minimum": 1 }
53
+ }
54
+ },
55
+ "error": {
56
+ "type": "object",
57
+ "additionalProperties": true,
58
+ "required": ["code", "message", "kind"],
59
+ "properties": {
60
+ "code": { "type": "string" },
61
+ "message": { "type": "string" },
62
+ "kind": { "type": "string", "enum": ["validation", "state_conflict", "operation_failed", "unknown"] }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/hegemonart/get-design-done/main/scripts/mcp-servers/gdd-state/schemas/checkpoint.schema.json",
4
+ "title": "GddStateCheckpoint",
5
+ "description": "MCP tool gdd_state__checkpoint — bump frontmatter.last_checkpoint + append a <timestamps> entry. Emits state.mutation.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["input", "output"],
9
+ "properties": {
10
+ "input": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "properties": {
14
+ "label": {
15
+ "type": "string",
16
+ "minLength": 1,
17
+ "description": "Optional label for the timestamp entry key. When absent the key is `<stage>_checkpoint_at`."
18
+ }
19
+ }
20
+ },
21
+ "output": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": ["success"],
25
+ "properties": {
26
+ "success": { "type": "boolean" },
27
+ "data": {
28
+ "type": "object",
29
+ "additionalProperties": true,
30
+ "properties": {
31
+ "last_checkpoint": {
32
+ "type": "string",
33
+ "format": "date-time"
34
+ },
35
+ "timestamp_key": { "type": "string" }
36
+ }
37
+ },
38
+ "error": {
39
+ "type": "object",
40
+ "additionalProperties": true,
41
+ "required": ["code", "message", "kind"],
42
+ "properties": {
43
+ "code": { "type": "string" },
44
+ "message": { "type": "string" },
45
+ "kind": { "type": "string", "enum": ["validation", "state_conflict", "operation_failed", "unknown"] }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }