@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
@@ -1,127 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * context-exhaustion.js — PostToolUse hook
4
- *
5
- * Monitors context usage reported in the tool_response. When usage exceeds
6
- * THRESHOLD (default 85%), writes a <paused> resumption block to
7
- * .design/STATE.md so the next session can resume without losing context.
8
- *
9
- * Hook type: PostToolUse (any tool)
10
- * Input: JSON on stdin { tool_name, tool_input, tool_response }
11
- * Output: JSON on stdout { continue, suppressOutput, message } or nothing
12
- */
13
-
14
- 'use strict';
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
- const readline = require('readline');
19
-
20
- const THRESHOLD = parseFloat(process.env.GDD_CONTEXT_THRESHOLD || '0.85');
21
- const STATE_PATH = path.join(process.cwd(), '.design', 'STATE.md');
22
-
23
- function now() {
24
- return new Date().toISOString();
25
- }
26
-
27
- function extractContextUsage(toolResponse) {
28
- // Claude Code injects context usage as a field in the tool response envelope.
29
- // Try common field names used across Claude Code versions.
30
- if (typeof toolResponse !== 'object' || toolResponse === null) return null;
31
-
32
- // Direct field
33
- if (typeof toolResponse.context_usage === 'number') return toolResponse.context_usage;
34
- if (typeof toolResponse.contextUsage === 'number') return toolResponse.contextUsage;
35
-
36
- // Nested under metadata
37
- const meta = toolResponse.metadata || toolResponse.meta || {};
38
- if (typeof meta.context_usage === 'number') return meta.context_usage;
39
- if (typeof meta.contextUsage === 'number') return meta.contextUsage;
40
-
41
- // Fraction string "0.87" or percentage "87%"
42
- const raw = toolResponse.context_usage || toolResponse.contextUsage
43
- || meta.context_usage || meta.contextUsage;
44
- if (typeof raw === 'string') {
45
- if (raw.endsWith('%')) return parseFloat(raw) / 100;
46
- const n = parseFloat(raw);
47
- if (!isNaN(n)) return n > 1 ? n / 100 : n;
48
- }
49
-
50
- return null;
51
- }
52
-
53
- function buildPausedBlock(toolName, usage) {
54
- const pct = Math.round(usage * 100);
55
- return `
56
- <paused>
57
- recorded: ${now()}
58
- trigger: context-exhaustion-hook
59
- context_usage: ${pct}%
60
- last_tool: ${toolName}
61
-
62
- ## Resumption instructions
63
-
64
- Context reached ${pct}% during the previous session (threshold: ${Math.round(THRESHOLD * 100)}%).
65
- The session was auto-paused to preserve quality.
66
-
67
- To resume:
68
- 1. Run \`/gdd:resume\` — it will read this block and restore working context
69
- 2. If mid-plan: check .design/STATE.md for the last completed task
70
- 3. Re-read the active PLAN.md to orient before continuing
71
-
72
- Intel store status at pause time:
73
- ls .design/intel/files.json 2>/dev/null && echo "present" || echo "missing"
74
- </paused>
75
- `;
76
- }
77
-
78
- function stateFileHasPausedBlock() {
79
- if (!fs.existsSync(STATE_PATH)) return false;
80
- const content = fs.readFileSync(STATE_PATH, 'utf8');
81
- return content.includes('<paused>') && content.includes('context-exhaustion-hook');
82
- }
83
-
84
- function appendPausedBlock(block) {
85
- if (!fs.existsSync(path.dirname(STATE_PATH))) {
86
- fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true });
87
- }
88
- if (!fs.existsSync(STATE_PATH)) {
89
- fs.writeFileSync(STATE_PATH, '# Design State\n\n', 'utf8');
90
- }
91
- fs.appendFileSync(STATE_PATH, block, 'utf8');
92
- }
93
-
94
- async function main() {
95
- const rl = readline.createInterface({ input: process.stdin });
96
- let inputData = '';
97
- for await (const line of rl) inputData += line + '\n';
98
-
99
- let parsed;
100
- try { parsed = JSON.parse(inputData); } catch { process.exit(0); }
101
-
102
- const toolName = parsed?.tool_name || 'unknown';
103
- const toolResponse = parsed?.tool_response || {};
104
-
105
- const usage = extractContextUsage(toolResponse);
106
-
107
- // No usage data — cannot act
108
- if (usage === null) process.exit(0);
109
-
110
- // Below threshold — do nothing
111
- if (usage < THRESHOLD) process.exit(0);
112
-
113
- // At or above threshold — write paused block (only once per session)
114
- if (stateFileHasPausedBlock()) process.exit(0);
115
-
116
- const block = buildPausedBlock(toolName, usage);
117
- appendPausedBlock(block);
118
-
119
- const response = {
120
- continue: true,
121
- suppressOutput: false,
122
- 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.`,
123
- };
124
- process.stdout.write(JSON.stringify(response));
125
- }
126
-
127
- main().catch(err => { console.error('context-exhaustion hook error:', err); process.exit(0); });
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * gdd-read-injection-scanner — PostToolUse hook
4
- * Scans Read tool output for common prompt-injection patterns and warns
5
- * (does not block) when suspicious content is found in a read file.
6
- */
7
-
8
- const readline = require('readline');
9
- const path = require('path');
10
- const { INJECTION_PATTERNS: RAW_PATTERNS } = require(path.join(__dirname, '..', 'scripts', 'injection-patterns.cjs'));
11
-
12
- // The hook needs bare RegExp objects; extract them from the shared {name,re} entries.
13
- const INJECTION_PATTERNS = RAW_PATTERNS.map(p => p.re);
14
-
15
- async function main() {
16
- const rl = readline.createInterface({ input: process.stdin });
17
- let inputData = '';
18
- for await (const line of rl) inputData += line + '\n';
19
-
20
- let parsed;
21
- try { parsed = JSON.parse(inputData); } catch { process.exit(0); }
22
-
23
- if (parsed?.tool_name !== 'Read') process.exit(0);
24
-
25
- const content = parsed?.tool_response?.content || '';
26
- const matched = INJECTION_PATTERNS.some(p => p.test(content));
27
- if (!matched) process.exit(0);
28
-
29
- const file = parsed?.tool_input?.file_path || 'unknown';
30
- const response = {
31
- continue: true,
32
- suppressOutput: false,
33
- message: `gdd-injection-scanner: Suspicious prompt-injection pattern detected in content read from "${file}". Review before acting on instructions contained in that file.`,
34
- };
35
- process.stdout.write(JSON.stringify(response));
36
- process.exit(0);
37
- }
38
-
39
- main().catch(() => process.exit(0));
@@ -1,173 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * aggregate-agent-metrics.js — Incremental per-agent aggregator.
4
- *
5
- * Reads: .design/telemetry/costs.jsonl (append-only ledger from hooks/budget-enforcer.js)
6
- * agents/{agent}.md (frontmatter source for default-tier, parallel-safe, reads-only,
7
- * typical-duration-seconds)
8
- * Writes: .design/agent-metrics.json (atomic overwrite via tmp-file + rename)
9
- *
10
- * Invoked:
11
- * 1. Detached child of hooks/budget-enforcer.js after every telemetry write.
12
- * 2. Directly by /gdd:optimize skill as an explicit refresh step.
13
- * 3. Manually: `node scripts/aggregate-agent-metrics.js`
14
- *
15
- * OPT-09 contract: fields must match Phase 11 reflector's expectations
16
- * (see .planning/phases/11-self-improvement/11-02-PLAN.md).
17
- */
18
-
19
- 'use strict';
20
-
21
- const fs = require('fs');
22
- const path = require('path');
23
- const os = require('os');
24
-
25
- const CWD = process.cwd();
26
- const TELEMETRY_PATH = path.join(CWD, '.design', 'telemetry', 'costs.jsonl');
27
- const METRICS_PATH = path.join(CWD, '.design', 'agent-metrics.json');
28
- const PHASE_TOTALS_PATH = path.join(CWD, '.design', 'telemetry', 'phase-totals.json');
29
- const AGENTS_DIR = path.join(CWD, 'agents');
30
-
31
- // ---- frontmatter reader (no YAML dep) ----
32
- function readAgentFrontmatter(agentName) {
33
- const p = path.join(AGENTS_DIR, `${agentName}.md`);
34
- if (!fs.existsSync(p)) return {};
35
- try {
36
- const content = fs.readFileSync(p, 'utf8');
37
- const fm = content.match(/^---\s*\n([\s\S]*?)\n---/);
38
- if (!fm) return {};
39
- const body = fm[1];
40
- const get = (key) => {
41
- const m = body.match(new RegExp(`^${key}:\\s*"?([^"\\n]+)"?`, 'm'));
42
- return m ? m[1].trim() : null;
43
- };
44
- const defaultTier = get('default-tier');
45
- const parallelSafe = get('parallel-safe');
46
- const readsOnly = get('reads-only');
47
- const typicalDuration = get('typical-duration-seconds');
48
- return {
49
- default_tier: defaultTier || null,
50
- parallel_safe: parallelSafe === null ? null : /^(true|yes)$/i.test(parallelSafe),
51
- reads_only: readsOnly === null ? null : /^(true|yes)$/i.test(readsOnly),
52
- typical_duration_seconds: typicalDuration === null ? null : Number(typicalDuration) || null,
53
- };
54
- } catch {
55
- return {};
56
- }
57
- }
58
-
59
- // ---- telemetry reader ----
60
- function readTelemetryRows() {
61
- if (!fs.existsSync(TELEMETRY_PATH)) return [];
62
- const raw = fs.readFileSync(TELEMETRY_PATH, 'utf8');
63
- const out = [];
64
- for (const line of raw.split(/\r?\n/)) {
65
- if (!line.trim()) continue;
66
- try {
67
- out.push(JSON.parse(line));
68
- } catch {
69
- // tolerant: skip malformed lines (partial write, truncation)
70
- }
71
- }
72
- return out;
73
- }
74
-
75
- // ---- aggregator ----
76
- function aggregate(rows) {
77
- const byAgent = new Map();
78
- for (const r of rows) {
79
- // Blocked rows represent a spawn that was denied at the hook — the agent
80
- // never actually ran, so it must not contribute to spawn counts, cost, or
81
- // token totals. Skip them here (mirror of the filter in aggregateByPhase).
82
- if (r.block_reason) continue;
83
- const agent = r.agent || 'unknown';
84
- if (!byAgent.has(agent)) {
85
- byAgent.set(agent, {
86
- total_spawns: 0,
87
- total_cost_usd: 0,
88
- total_tokens_in: 0,
89
- total_tokens_out: 0,
90
- cache_hits: 0,
91
- lazy_skips: 0,
92
- });
93
- }
94
- const a = byAgent.get(agent);
95
- a.total_spawns += 1;
96
- a.total_cost_usd += Number(r.est_cost_usd || 0);
97
- a.total_tokens_in += Number(r.tokens_in || 0);
98
- a.total_tokens_out += Number(r.tokens_out || 0);
99
- if (r.cache_hit === true) a.cache_hits += 1;
100
- if (r.lazy_skipped === true) a.lazy_skips += 1;
101
- }
102
-
103
- const out = {};
104
- for (const [agent, a] of byAgent.entries()) {
105
- const fm = readAgentFrontmatter(agent);
106
- const spawns = a.total_spawns || 1; // guard div-by-zero
107
- out[agent] = {
108
- typical_duration_seconds: fm.typical_duration_seconds,
109
- default_tier: fm.default_tier,
110
- parallel_safe: fm.parallel_safe,
111
- reads_only: fm.reads_only,
112
- total_spawns: a.total_spawns,
113
- total_cost_usd: Number(a.total_cost_usd.toFixed(6)),
114
- total_tokens_in: a.total_tokens_in,
115
- total_tokens_out: a.total_tokens_out,
116
- cache_hit_rate: Number((a.cache_hits / spawns).toFixed(4)),
117
- lazy_skip_rate: Number((a.lazy_skips / spawns).toFixed(4)),
118
- };
119
- }
120
- return out;
121
- }
122
-
123
- // ---- atomic write ----
124
- function writeAtomic(filePath, content) {
125
- const dir = path.dirname(filePath);
126
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
127
- const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
128
- fs.writeFileSync(tmp, content, 'utf8');
129
- fs.renameSync(tmp, filePath);
130
- }
131
-
132
- // ---- phase totals aggregator (WR-02: avoids full JSONL replay in budget enforcer) ----
133
- function aggregateByPhase(rows) {
134
- const byPhase = {};
135
- for (const r of rows) {
136
- // Blocked rows represent spawns that were denied by the hook — the agent
137
- // never ran, so their est_cost_usd must not inflate cumulative phase spend.
138
- // Counting them would make future hard-block and soft-threshold checks
139
- // stricter than intended on every repeat cap hit.
140
- if (r.block_reason) continue;
141
- const phase = r.phase || 'unknown';
142
- byPhase[phase] = (byPhase[phase] || 0) + Number(r.est_cost_usd || 0);
143
- }
144
- // Round to 6dp to match per-agent precision
145
- for (const k of Object.keys(byPhase)) byPhase[k] = Number(byPhase[k].toFixed(6));
146
- return byPhase;
147
- }
148
-
149
- // ---- main ----
150
- function main() {
151
- const rows = readTelemetryRows();
152
- const agents = aggregate(rows);
153
- const payload = {
154
- generated_at: new Date().toISOString(),
155
- agents,
156
- };
157
- writeAtomic(METRICS_PATH, JSON.stringify(payload, null, 2) + '\n');
158
- // Write lightweight phase-totals.json so budget-enforcer can read phase spend
159
- // in O(1) without replaying the full JSONL on every agent spawn (WR-02).
160
- const phaseTotals = {
161
- generated_at: new Date().toISOString(),
162
- totals: aggregateByPhase(rows),
163
- };
164
- writeAtomic(PHASE_TOTALS_PATH, JSON.stringify(phaseTotals, null, 2) + '\n');
165
- }
166
-
167
- try {
168
- main();
169
- } catch (err) {
170
- // Fail open: aggregator must never block the hook or /gdd:optimize flow.
171
- process.stderr.write(`aggregate-agent-metrics: ${err.message}\n`);
172
- process.exit(0);
173
- }
@@ -1,68 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- // CI-friendly frontmatter validator for agents/*.md.
4
- // Enforces the Phase 7 agent frontmatter hygiene contract.
5
- // Exits 0 on success, 1 on any violation. One finding per stdout line.
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
- const { readFrontmatter } = require('../tests/helpers.cjs');
10
-
11
- const REQUIRED_FIELDS = [
12
- 'name',
13
- 'description',
14
- 'tools',
15
- 'color',
16
- 'parallel-safe',
17
- 'typical-duration-seconds',
18
- 'reads-only',
19
- 'writes',
20
- ];
21
-
22
- function walkMd(dir) {
23
- const out = [];
24
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
25
- const full = path.join(dir, entry.name);
26
- if (entry.isDirectory()) out.push(...walkMd(full));
27
- else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
28
- }
29
- return out;
30
- }
31
-
32
- function main() {
33
- const args = process.argv.slice(2).filter((a) => !a.startsWith('--'));
34
- const targets = args.length ? args : ['agents/'];
35
- const files = [];
36
- for (const t of targets) {
37
- if (!fs.existsSync(t)) {
38
- console.error(`${t}: path does not exist`);
39
- process.exit(1);
40
- }
41
- const stat = fs.statSync(t);
42
- if (stat.isDirectory()) files.push(...walkMd(t));
43
- else files.push(t);
44
- }
45
-
46
- let violations = 0;
47
- for (const f of files) {
48
- const fm = readFrontmatter(f);
49
- if (Object.keys(fm).length === 0) {
50
- // README.md under agents/ may have no frontmatter — skip
51
- if (path.basename(f).toLowerCase() === 'readme.md') continue;
52
- console.log(`${f}:frontmatter: missing`);
53
- violations++;
54
- continue;
55
- }
56
- for (const field of REQUIRED_FIELDS) {
57
- if (!(field in fm) || fm[field] === undefined || fm[field] === null || fm[field] === '') {
58
- console.log(`${f}:${field}: missing`);
59
- violations++;
60
- }
61
- }
62
- }
63
-
64
- console.log(`summary: ${files.length} file(s) checked, ${violations} violation(s)`);
65
- process.exit(violations === 0 ? 0 : 1);
66
- }
67
-
68
- main();
@@ -1,242 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * validate-schemas.cjs
6
- *
7
- * Runs all Draft-07 JSON schemas under reference/schemas/ against their
8
- * corresponding subject files. Used by `npm run validate:schemas` and by the
9
- * CI validate job (added in plan 13-03).
10
- *
11
- * Primary path: spawn `npx --yes ajv-cli@5 validate -s <schema> -d <data>`.
12
- *
13
- * Fallback path: if `npx --yes` cannot fetch ajv-cli (offline/sandboxed
14
- * environment), fall back to a structural parse check: confirm each schema is
15
- * valid Draft-07 JSON (has $schema, type) and each data file is parseable JSON.
16
- * The fallback exits 0 on structural validity; CI will run the real ajv pass.
17
- *
18
- * Usage:
19
- * node scripts/validate-schemas.cjs # validate all schemas
20
- * node scripts/validate-schemas.cjs --no-npx # force fallback path
21
- *
22
- * Exit codes:
23
- * 0 — all present (schema, data) pairs pass
24
- * 1 — one or more pairs failed validation or a present schema is invalid
25
- */
26
-
27
- const { spawnSync } = require('child_process');
28
- const fs = require('fs');
29
- const path = require('path');
30
-
31
- const REPO_ROOT = path.resolve(__dirname, '..');
32
- const SCHEMA_DIR = path.join(REPO_ROOT, 'reference', 'schemas');
33
-
34
- /**
35
- * (schema, data) pairs. `dataPath` is relative to repo root. `required=true`
36
- * means the data file MUST exist in the repo; `required=false` means the data
37
- * file is generated at runtime (gitignored) and only the schema itself is
38
- * compiled.
39
- */
40
- const PAIRS = [
41
- {
42
- name: 'plugin',
43
- schema: 'reference/schemas/plugin.schema.json',
44
- data: '.claude-plugin/plugin.json',
45
- required: true,
46
- },
47
- {
48
- name: 'marketplace',
49
- schema: 'reference/schemas/marketplace.schema.json',
50
- data: '.claude-plugin/marketplace.json',
51
- required: true,
52
- },
53
- {
54
- name: 'hooks',
55
- schema: 'reference/schemas/hooks.schema.json',
56
- data: 'hooks/hooks.json',
57
- required: true,
58
- },
59
- {
60
- name: 'config',
61
- schema: 'reference/schemas/config.schema.json',
62
- data: '.design/config.json',
63
- required: false,
64
- },
65
- {
66
- name: 'intel',
67
- schema: 'reference/schemas/intel.schema.json',
68
- // intel files are runtime-only (gitignored). Only schema-compile it.
69
- data: null,
70
- required: false,
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
- },
80
- ];
81
-
82
- const USE_NPX = !process.argv.includes('--no-npx');
83
-
84
- /**
85
- * Try running ajv-cli via npx. Returns { ok, stdout, stderr, status, fetchFailed }.
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.
92
- */
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');
98
- const result = spawnSync(
99
- 'npx',
100
- ['--yes', '-p', 'ajv-cli@5', '-p', 'ajv-formats@3', 'ajv', ...injected],
101
- { encoding: 'utf8', cwd: REPO_ROOT }
102
- );
103
- const combined = (result.stdout || '') + (result.stderr || '');
104
- // Heuristics for "network/fetch failed" — in that case fall back silently.
105
- const fetchFailed =
106
- result.status !== 0 &&
107
- /(ENOTFOUND|ECONNREFUSED|ETIMEDOUT|getaddrinfo|network|unable to resolve|unable to fetch|offline|E404|registry\.npmjs\.org)/i.test(combined);
108
- return {
109
- ok: result.status === 0,
110
- stdout: result.stdout || '',
111
- stderr: result.stderr || '',
112
- status: result.status,
113
- fetchFailed,
114
- };
115
- }
116
-
117
- /**
118
- * Fallback: confirm schema file is a valid Draft-07 JSON Schema (has $schema
119
- * pointing at draft-07). Confirm data file (if present) parses as JSON.
120
- */
121
- function structuralCheck(schemaAbs, dataAbs) {
122
- let schema;
123
- try {
124
- schema = JSON.parse(fs.readFileSync(schemaAbs, 'utf8'));
125
- } catch (e) {
126
- return { ok: false, reason: `schema not parseable JSON: ${e.message}` };
127
- }
128
- if (!schema.$schema || !/draft-07/.test(schema.$schema)) {
129
- return { ok: false, reason: `schema missing Draft-07 $schema declaration` };
130
- }
131
- if (dataAbs) {
132
- try {
133
- JSON.parse(fs.readFileSync(dataAbs, 'utf8'));
134
- } catch (e) {
135
- return { ok: false, reason: `data file not parseable JSON: ${e.message}` };
136
- }
137
- }
138
- return { ok: true };
139
- }
140
-
141
- function main() {
142
- const results = [];
143
- let fallbackReason = null;
144
-
145
- for (const pair of PAIRS) {
146
- const schemaAbs = path.join(REPO_ROOT, pair.schema);
147
- const dataAbs = pair.data ? path.join(REPO_ROOT, pair.data) : null;
148
-
149
- if (!fs.existsSync(schemaAbs)) {
150
- results.push({
151
- name: pair.name,
152
- ok: false,
153
- via: 'missing-schema',
154
- reason: `schema not found at ${pair.schema}`,
155
- });
156
- continue;
157
- }
158
-
159
- const dataPresent = dataAbs && fs.existsSync(dataAbs);
160
-
161
- if (!dataPresent && pair.required) {
162
- results.push({
163
- name: pair.name,
164
- ok: false,
165
- via: 'missing-data',
166
- reason: `required data file missing at ${pair.data}`,
167
- });
168
- continue;
169
- }
170
-
171
- if (!dataPresent) {
172
- // Schema-only: compile to confirm the schema is structurally valid.
173
- if (USE_NPX && !fallbackReason) {
174
- const r = runAjv(['compile', '-s', schemaAbs]);
175
- if (r.ok) {
176
- results.push({ name: pair.name, ok: true, via: 'ajv-compile', note: 'data not present in repo tree — schema compiled only' });
177
- continue;
178
- }
179
- if (r.fetchFailed) {
180
- fallbackReason = `npx fetch failed: ${r.stderr.split('\n')[0] || 'unknown'}`;
181
- // fall through to structural check below
182
- } else {
183
- results.push({ name: pair.name, ok: false, via: 'ajv-compile', reason: r.stderr || r.stdout || `exit ${r.status}` });
184
- continue;
185
- }
186
- }
187
- const s = structuralCheck(schemaAbs, null);
188
- results.push({
189
- name: pair.name,
190
- ok: s.ok,
191
- via: 'structural-compile',
192
- reason: s.ok ? 'schema is valid Draft-07 (data not in repo tree)' : s.reason,
193
- });
194
- continue;
195
- }
196
-
197
- // Both schema and data present — full validation.
198
- if (USE_NPX && !fallbackReason) {
199
- const r = runAjv(['validate', '-s', schemaAbs, '-d', dataAbs]);
200
- if (r.ok) {
201
- results.push({ name: pair.name, ok: true, via: 'ajv-validate' });
202
- continue;
203
- }
204
- if (r.fetchFailed) {
205
- fallbackReason = `npx fetch failed: ${r.stderr.split('\n')[0] || 'unknown'}`;
206
- // fall through to structural check below
207
- } else {
208
- results.push({ name: pair.name, ok: false, via: 'ajv-validate', reason: r.stderr || r.stdout || `exit ${r.status}` });
209
- continue;
210
- }
211
- }
212
- const s = structuralCheck(schemaAbs, dataAbs);
213
- results.push({
214
- name: pair.name,
215
- ok: s.ok,
216
- via: 'structural-validate',
217
- reason: s.ok ? 'schema is valid Draft-07 and data parses as JSON' : s.reason,
218
- });
219
- }
220
-
221
- // Report.
222
- console.log('validate-schemas: results');
223
- for (const r of results) {
224
- const status = r.ok ? 'OK' : 'FAIL';
225
- const note = r.note ? ` (${r.note})` : '';
226
- const reason = r.reason ? ` — ${r.reason}` : '';
227
- console.log(` [${status}] ${r.name} via ${r.via}${note}${reason}`);
228
- }
229
- if (fallbackReason) {
230
- console.log(`\nnote: ajv-cli via npx unavailable (${fallbackReason}); fell back to structural checks. CI will run the authoritative ajv pass.`);
231
- }
232
- const failed = results.filter(r => !r.ok);
233
- console.log(`summary: ${results.length} pair(s) checked, ${failed.length} failure(s)`);
234
-
235
- process.exit(failed.length === 0 ? 0 : 1);
236
- }
237
-
238
- if (require.main === module) {
239
- main();
240
- }
241
-
242
- module.exports = { PAIRS };