@hegemonart/get-design-done 1.30.6 → 1.31.5

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 (175) hide show
  1. package/.claude-plugin/marketplace.json +6 -3
  2. package/.claude-plugin/plugin.json +5 -2
  3. package/CHANGELOG.md +105 -0
  4. package/NOTICE +224 -0
  5. package/README.md +22 -1
  6. package/SKILL.md +1 -0
  7. package/agents/design-authority-watcher.md +1 -1
  8. package/agents/perf-analyzer.md +2 -2
  9. package/bin/gdd-mcp +78 -0
  10. package/bin/gdd-sdk +34 -24
  11. package/bin/gdd-state-mcp +78 -0
  12. package/{README.de.md → docs/i18n/README.de.md} +1 -1
  13. package/{README.fr.md → docs/i18n/README.fr.md} +1 -1
  14. package/{README.it.md → docs/i18n/README.it.md} +1 -1
  15. package/{README.ja.md → docs/i18n/README.ja.md} +1 -1
  16. package/{README.ko.md → docs/i18n/README.ko.md} +1 -1
  17. package/{README.zh-CN.md → docs/i18n/README.zh-CN.md} +1 -1
  18. package/hooks/_hook-emit.js +1 -1
  19. package/hooks/budget-enforcer.ts +5 -5
  20. package/hooks/context-exhaustion.ts +2 -2
  21. package/hooks/gdd-precompact-snapshot.js +3 -3
  22. package/hooks/gdd-read-injection-scanner.ts +2 -2
  23. package/hooks/gdd-sessionstart-recap.js +1 -1
  24. package/hooks/gdd-turn-closeout.js +1 -1
  25. package/package.json +24 -10
  26. package/recipes/.gitkeep +0 -0
  27. package/reference/schemas/recipe.schema.json +33 -0
  28. package/scripts/cli/gdd-events.mjs +5 -5
  29. package/scripts/lib/cache/gdd-cache-manager.cjs +1 -1
  30. package/scripts/lib/cli/index.ts +22 -160
  31. package/scripts/lib/connection-probe/index.cjs +1 -1
  32. package/scripts/lib/discuss-parallel-runner/aggregator.ts +1 -1
  33. package/scripts/lib/discuss-parallel-runner/index.ts +1 -1
  34. package/scripts/lib/error-classifier.cjs +24 -227
  35. package/scripts/lib/event-stream/index.ts +25 -193
  36. package/scripts/lib/figma-extract/digest.cjs +430 -0
  37. package/scripts/lib/figma-extract/parse-url.cjs +87 -0
  38. package/scripts/lib/figma-extract/payload-schema.json +108 -0
  39. package/scripts/lib/figma-extract/pull.cjs +394 -0
  40. package/scripts/lib/figma-extract/receiver.cjs +273 -0
  41. package/scripts/lib/figma-extract/render-md.cjs +143 -0
  42. package/scripts/lib/figma-extract/styles-resolver.cjs +147 -0
  43. package/scripts/lib/figma-extract/walk.cjs +100 -0
  44. package/scripts/lib/gdd-errors/index.ts +24 -213
  45. package/scripts/lib/gdd-state/index.ts +23 -161
  46. package/scripts/lib/health-mirror/index.cjs +88 -1
  47. package/scripts/lib/iteration-budget.cjs +23 -199
  48. package/scripts/lib/jittered-backoff.cjs +24 -107
  49. package/scripts/lib/lockfile.cjs +23 -195
  50. package/scripts/lib/logger/index.ts +1 -1
  51. package/scripts/lib/parallelism-engine/concurrency-tuner.cjs +1 -1
  52. package/scripts/lib/perf-analyzer/index.cjs +1 -1
  53. package/scripts/lib/pipeline-runner/index.ts +4 -4
  54. package/scripts/lib/pipeline-runner/state-machine.ts +1 -1
  55. package/scripts/lib/prompt-dedup/index.cjs +1 -1
  56. package/scripts/lib/rate-guard.cjs +2 -2
  57. package/scripts/lib/recipe-loader.cjs +142 -0
  58. package/scripts/lib/session-runner/errors.ts +3 -3
  59. package/scripts/lib/session-runner/index.ts +3 -3
  60. package/scripts/lib/session-runner/transcript.ts +1 -1
  61. package/scripts/lib/tool-scoping/index.ts +1 -1
  62. package/scripts/mcp-servers/gdd-mcp/server.ts +29 -311
  63. package/scripts/mcp-servers/gdd-state/server.ts +28 -282
  64. package/sdk/README.md +45 -0
  65. package/{scripts/lib → sdk}/cli/commands/audit.ts +3 -3
  66. package/{scripts/lib → sdk}/cli/commands/init.ts +3 -3
  67. package/{scripts/lib → sdk}/cli/commands/query.ts +4 -4
  68. package/{scripts/lib → sdk}/cli/commands/run.ts +5 -5
  69. package/{scripts/lib → sdk}/cli/commands/stage.ts +5 -5
  70. package/sdk/cli/index.js +8091 -0
  71. package/sdk/cli/index.ts +172 -0
  72. package/{scripts/lib → sdk}/cli/parse-args.ts +2 -2
  73. package/{scripts/lib/gdd-errors → sdk/errors}/classification.ts +1 -1
  74. package/sdk/errors/index.ts +218 -0
  75. package/{scripts/lib → sdk}/event-stream/emitter.ts +1 -1
  76. package/sdk/event-stream/index.ts +197 -0
  77. package/{scripts/lib → sdk}/event-stream/reader.ts +1 -1
  78. package/{scripts/lib → sdk}/event-stream/types.ts +2 -2
  79. package/{scripts/lib → sdk}/event-stream/writer.ts +1 -1
  80. package/sdk/index.ts +19 -0
  81. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/README.md +3 -3
  82. package/sdk/mcp/gdd-mcp/server.js +1924 -0
  83. package/sdk/mcp/gdd-mcp/server.ts +325 -0
  84. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_cycle_recap.ts +3 -3
  85. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_decisions_list.ts +2 -2
  86. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_events_tail.ts +3 -3
  87. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_health.ts +2 -2
  88. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_intel_get.ts +2 -2
  89. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_learnings_digest.ts +2 -2
  90. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phase_current.ts +2 -2
  91. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phases_list.ts +2 -2
  92. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_plans_list.ts +2 -2
  93. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_reflections_latest.ts +2 -2
  94. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_status.ts +3 -3
  95. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_telemetry_query.ts +3 -3
  96. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/index.ts +2 -2
  97. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/shared.ts +3 -3
  98. package/sdk/mcp/gdd-state/server.js +2790 -0
  99. package/sdk/mcp/gdd-state/server.ts +294 -0
  100. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_blocker.ts +3 -3
  101. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_decision.ts +3 -3
  102. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_must_have.ts +3 -3
  103. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/checkpoint.ts +2 -2
  104. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/frontmatter_update.ts +2 -2
  105. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/get.ts +3 -3
  106. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/index.ts +1 -1
  107. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/probe_connections.ts +3 -3
  108. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/resolve_blocker.ts +3 -3
  109. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/set_status.ts +2 -2
  110. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/shared.ts +8 -8
  111. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/transition_stage.ts +4 -4
  112. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/update_progress.ts +2 -2
  113. package/sdk/primitives/error-classifier.cjs +232 -0
  114. package/sdk/primitives/iteration-budget.cjs +205 -0
  115. package/sdk/primitives/jittered-backoff.cjs +112 -0
  116. package/sdk/primitives/lockfile.cjs +201 -0
  117. package/{scripts/lib/gdd-state → sdk/state}/gates.ts +1 -1
  118. package/sdk/state/index.ts +167 -0
  119. package/{scripts/lib/gdd-state → sdk/state}/lockfile.ts +1 -1
  120. package/{scripts/lib/gdd-state → sdk/state}/mutator.ts +1 -1
  121. package/{scripts/lib/gdd-state → sdk/state}/parser.ts +1 -1
  122. package/{scripts/lib/gdd-state → sdk/state}/types.ts +4 -4
  123. package/skills/figma-extract/SKILL.md +64 -0
  124. package/skills/health/SKILL.md +10 -0
  125. package/skills/quality-gate/SKILL.md +2 -2
  126. package/scripts/aggregate-agent-metrics.ts +0 -282
  127. package/scripts/bootstrap-manifest.txt +0 -3
  128. package/scripts/bootstrap.sh +0 -80
  129. package/scripts/build-distribution-bundles.cjs +0 -549
  130. package/scripts/build-intel.cjs +0 -486
  131. package/scripts/codegen-schema-types.ts +0 -149
  132. package/scripts/detect-stale-refs.cjs +0 -107
  133. package/scripts/e2e/run-headless.ts +0 -514
  134. package/scripts/extract-changelog-section.cjs +0 -58
  135. package/scripts/gsd-cleanup-incubator.cjs +0 -367
  136. package/scripts/injection-patterns.cjs +0 -58
  137. package/scripts/lint-agentskills-spec.cjs +0 -457
  138. package/scripts/release-smoke-test.cjs +0 -200
  139. package/scripts/rollback-release.sh +0 -42
  140. package/scripts/run-injection-scanner-ci.cjs +0 -83
  141. package/scripts/tests/test-authority-rejected-kinds.sh +0 -58
  142. package/scripts/tests/test-authority-watcher-diff.sh +0 -113
  143. package/scripts/tests/test-motion-provenance.sh +0 -64
  144. package/scripts/validate-frontmatter.ts +0 -409
  145. package/scripts/validate-incubator-scope.cjs +0 -133
  146. package/scripts/validate-schemas.ts +0 -401
  147. package/scripts/validate-skill-length.cjs +0 -283
  148. package/scripts/verify-version-sync.cjs +0 -30
  149. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_cycle_recap.schema.json +0 -0
  150. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_decisions_list.schema.json +0 -0
  151. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_events_tail.schema.json +0 -0
  152. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_health.schema.json +0 -0
  153. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_intel_get.schema.json +0 -0
  154. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_learnings_digest.schema.json +0 -0
  155. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phase_current.schema.json +0 -0
  156. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phases_list.schema.json +0 -0
  157. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_plans_list.schema.json +0 -0
  158. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_reflections_latest.schema.json +0 -0
  159. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_status.schema.json +0 -0
  160. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_telemetry_query.schema.json +0 -0
  161. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_blocker.schema.json +0 -0
  162. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_decision.schema.json +0 -0
  163. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_must_have.schema.json +0 -0
  164. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/checkpoint.schema.json +0 -0
  165. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/frontmatter_update.schema.json +0 -0
  166. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/get.schema.json +0 -0
  167. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/probe_connections.schema.json +0 -0
  168. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/resolve_blocker.schema.json +0 -0
  169. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/set_status.schema.json +0 -0
  170. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/transition_stage.schema.json +0 -0
  171. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/update_progress.schema.json +0 -0
  172. /package/{scripts/lib → sdk/primitives}/error-classifier.d.cts +0 -0
  173. /package/{scripts/lib → sdk/primitives}/iteration-budget.d.cts +0 -0
  174. /package/{scripts/lib → sdk/primitives}/jittered-backoff.d.cts +0 -0
  175. /package/{scripts/lib → sdk/primitives}/lockfile.d.cts +0 -0
@@ -1,107 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- // Detect deprecated namespace and stage/agent references in shipped markdown.
4
- // Reads reference/DEPRECATIONS.md as the authoritative list of stale tokens.
5
- // Exits 0 if clean, 1 on any match.
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
-
10
- const REPO_ROOT = path.resolve(__dirname, '..');
11
- const DEPRECATIONS_PATH = path.join(REPO_ROOT, 'reference/DEPRECATIONS.md');
12
-
13
- // Patterns checked against every .md file body (not against DEPRECATIONS.md itself).
14
- // Scope: only truly unambiguous deprecations — names that must not appear anywhere
15
- // outside DEPRECATIONS.md. Current-but-historically-renamed agents/skills
16
- // (design-context-builder, design-pattern-mapper, scan/, discover/) are NOT
17
- // flagged here because they still exist as live files in the tree; the
18
- // rename/split documented in DEPRECATIONS.md was partial, so static detection
19
- // would over-fire. Cover those cases via targeted review rather than grep.
20
- const PATTERNS = [
21
- { name: '/design: namespace (replaced by /gdd:)', regex: /\/design:[a-z-]+/g },
22
- // Phase 30.6: runtime dispatch to upstream gsd-tools.cjs is now disallowed.
23
- // Native CLI lives at bin/gdd-graph (graph ops) and is invoked as `node bin/gdd-graph <sub>`.
24
- // The pattern is precise — only the explicit bash dispatch is flagged. Prose mentions
25
- // of "gsd-tools" or "get-shit-done" in attribution contexts (NOTICE, CHANGELOG, README)
26
- // are NOT matched because they don't include the leading `node "$HOME/.claude/...` token.
27
- { name: 'gsd-tools.cjs runtime dispatch (use bin/gdd-graph instead)', regex: /node\s+["']?\$HOME\/\.claude\/get-shit-done\/bin\/gsd-tools\.cjs/g },
28
- ];
29
-
30
- const EXCLUDE_DIRS = new Set([
31
- 'node_modules',
32
- '.planning',
33
- '.claude',
34
- '.design',
35
- 'test-fixture',
36
- '.git',
37
- ]);
38
-
39
- const EXCLUDE_FILES = new Set([
40
- // Normalize to forward-slashes so Windows matches this set correctly.
41
- path.relative(REPO_ROOT, DEPRECATIONS_PATH).split(path.sep).join('/'),
42
- ]);
43
-
44
- function ensureDeprecationsExists() {
45
- if (fs.existsSync(DEPRECATIONS_PATH)) return;
46
- const stub = [
47
- '# Deprecated Namespaces and Names',
48
- '',
49
- 'Auto-generated stub — edit this file to declare deprecations authoritatively.',
50
- '',
51
- '## Stale command namespaces',
52
- '- `/design:*` — replaced by `/gdd:*`',
53
- '',
54
- '## Stale agent names',
55
- '- `design-context-builder` — replaced',
56
- '- `design-pattern-mapper` (as single blob) — replaced',
57
- '',
58
- '## Stale stage names',
59
- '- `scan` — folded into `/gdd:explore`',
60
- '- `discover` — folded into `/gdd:explore`',
61
- '',
62
- ].join('\n');
63
- fs.mkdirSync(path.dirname(DEPRECATIONS_PATH), { recursive: true });
64
- fs.writeFileSync(DEPRECATIONS_PATH, stub, 'utf8');
65
- }
66
-
67
- function walk(dir, out) {
68
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
69
- if (entry.isDirectory()) {
70
- if (EXCLUDE_DIRS.has(entry.name)) continue;
71
- walk(path.join(dir, entry.name), out);
72
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
73
- const full = path.join(dir, entry.name);
74
- const rel = path.relative(REPO_ROOT, full).split(path.sep).join('/');
75
- if (EXCLUDE_FILES.has(rel)) continue;
76
- out.push(full);
77
- }
78
- }
79
- }
80
-
81
- function main() {
82
- ensureDeprecationsExists();
83
- const files = [];
84
- walk(REPO_ROOT, files);
85
-
86
- let findings = 0;
87
- for (const file of files) {
88
- const body = fs.readFileSync(file, 'utf8');
89
- const lines = body.split('\n');
90
- for (let i = 0; i < lines.length; i++) {
91
- for (const { name, regex } of PATTERNS) {
92
- regex.lastIndex = 0;
93
- let m;
94
- while ((m = regex.exec(lines[i])) !== null) {
95
- const rel = path.relative(REPO_ROOT, file);
96
- console.log(`${rel}:${i + 1}: ${name} → ${m[0]}`);
97
- findings++;
98
- }
99
- }
100
- }
101
- }
102
-
103
- console.log(`summary: ${files.length} files scanned, ${findings} stale refs found`);
104
- process.exit(findings === 0 ? 0 : 1);
105
- }
106
-
107
- main();
@@ -1,514 +0,0 @@
1
- // scripts/e2e/run-headless.ts — Plan 21-11 Task 2 (SDK-24).
2
- //
3
- // E2E test harness for the Phase-21 headless pipeline runner.
4
- //
5
- // Spawns `bin/gdd-sdk run` against a copy of the fixture project under
6
- // `test-fixture/headless-e2e/`, captures stdout/stderr + the resulting
7
- // `.design/` artifacts, and runs a fixed set of shape assertions:
8
- //
9
- // 1. Every expected artifact exists.
10
- // 2. DESIGN-PATTERNS.md contains the 4 locked headings
11
- // (Tokens, Components, Accessibility, Visual Hierarchy).
12
- // 3. DESIGN-PLAN.md contains at least one `Wave` section + one
13
- // `Type:` line.
14
- // 4. SUMMARY.md contains `## VERIFY COMPLETE`.
15
- // 5. events.jsonl has >= 5 `state.transition` events (one per
16
- // stage boundary brief->explore->plan->design->verify).
17
- // 6. Dry-run mode: usd_cost === 0.
18
- // 7. Live mode: usd_cost < maxUsdCost (default 5.0).
19
- // 8. Wall-clock < timeoutMs (default 15 min).
20
- // 9. Exit code === 0.
21
- //
22
- // The harness has TWO modes:
23
- //
24
- // * 'dry-run' — spawns `gdd-sdk run --dry-run --fixture <src> --cwd <tmp>`
25
- // which installs canned-session overrides. Never hits the Anthropic
26
- // API. Runs on every PR.
27
- // * 'live' — spawns `gdd-sdk run --cwd <tmp>` with the real Agent
28
- // SDK. Gated on `process.env.ANTHROPIC_API_KEY`; returns
29
- // `status: 'skipped'` when the key is absent. Runs on main-branch
30
- // push with secret.
31
- //
32
- // The fixture is NEVER mutated. Every run first copies
33
- // `test-fixture/headless-e2e/` into a unique temp directory so repeated
34
- // runs (and the seeded `.design/`) stay pristine.
35
-
36
- import {
37
- existsSync,
38
- mkdirSync,
39
- mkdtempSync,
40
- readFileSync,
41
- readdirSync,
42
- statSync,
43
- writeFileSync,
44
- } from 'node:fs';
45
- import { tmpdir } from 'node:os';
46
- import { join as joinPath, resolve as resolvePath, relative as relPath, dirname } from 'node:path';
47
- import { spawn } from 'node:child_process';
48
-
49
- // ---------------------------------------------------------------------------
50
- // Public types.
51
- // ---------------------------------------------------------------------------
52
-
53
- export type E2EMode = 'dry-run' | 'live';
54
- export type E2EStatus = 'pass' | 'fail' | 'skipped';
55
-
56
- /** Artifact presence + size report keyed by relative path. */
57
- export type ArtifactReport = Readonly<Record<string, { readonly exists: boolean; readonly bytes: number }>>;
58
-
59
- export interface E2EResult {
60
- readonly status: E2EStatus;
61
- readonly mode: E2EMode;
62
- /** Wall-clock duration in ms. */
63
- readonly duration_ms: number;
64
- /** Extracted from the pipeline-result JSON. Zero under dry-run. */
65
- readonly usd_cost: number;
66
- /** Artifact presence snapshot. */
67
- readonly artifacts: ArtifactReport;
68
- /** Ordered assertion-failure messages (empty on pass). */
69
- readonly assertion_failures: readonly string[];
70
- /** gdd-sdk exit code; 0 on success. */
71
- readonly exit_code: number;
72
- /** Path to the temp directory where the fixture was copied + run. */
73
- readonly run_dir: string;
74
- /** Captured stdout (trimmed to 64KiB tail). */
75
- readonly stdout_tail: string;
76
- /** Captured stderr (trimmed to 64KiB tail). */
77
- readonly stderr_tail: string;
78
- }
79
-
80
- export interface RunHeadlessE2EOptions {
81
- readonly mode: E2EMode;
82
- /** Absolute path to `test-fixture/headless-e2e/`. */
83
- readonly fixtureDir: string;
84
- /** Override the default 15-minute wall-clock cap. */
85
- readonly timeoutMs?: number;
86
- /** Override the default 5.0 USD budget cap for live mode. */
87
- readonly maxUsdCost?: number;
88
- /** Override the default gdd-sdk bin path (`./bin/gdd-sdk`). */
89
- readonly gddSdkBin?: string;
90
- /**
91
- * Optional environment overrides layered on top of process.env. Used
92
- * by tests to pass through `ANTHROPIC_API_KEY` deliberately.
93
- */
94
- readonly env?: Readonly<Record<string, string>>;
95
- }
96
-
97
- // ---------------------------------------------------------------------------
98
- // Public entry.
99
- // ---------------------------------------------------------------------------
100
-
101
- /**
102
- * Run the headless E2E harness against a fixture. Never throws —
103
- * failures surface on `result.assertion_failures` or `result.status`.
104
- *
105
- * When `mode === 'live'` and `ANTHROPIC_API_KEY` is absent the harness
106
- * returns `status: 'skipped'` without spawning a subprocess. The caller
107
- * treats this as a pass (matches the `run-injection-scanner-ci.cjs`
108
- * gating pattern — no secret => no-op).
109
- */
110
- export async function runHeadlessE2E(opts: RunHeadlessE2EOptions): Promise<E2EResult> {
111
- const t0 = Date.now();
112
- const timeoutMs = opts.timeoutMs ?? 15 * 60 * 1000; // 15 min default
113
- const maxUsdCost = opts.maxUsdCost ?? 5.0;
114
-
115
- const env = { ...process.env, ...(opts.env ?? {}) };
116
-
117
- // Live mode gating — skip cleanly if the secret is absent.
118
- if (opts.mode === 'live' && !env['ANTHROPIC_API_KEY']) {
119
- return {
120
- status: 'skipped',
121
- mode: 'live',
122
- duration_ms: Date.now() - t0,
123
- usd_cost: 0,
124
- artifacts: Object.freeze({}),
125
- assertion_failures: Object.freeze([]),
126
- exit_code: 0,
127
- run_dir: '',
128
- stdout_tail: '',
129
- stderr_tail: '',
130
- };
131
- }
132
-
133
- // Resolve + validate the fixture source.
134
- const srcFixture = resolvePath(opts.fixtureDir);
135
- if (!existsSync(srcFixture)) {
136
- return failEarly(
137
- opts.mode,
138
- Date.now() - t0,
139
- [`fixture directory does not exist: ${srcFixture}`],
140
- );
141
- }
142
-
143
- // Copy the fixture into a temp dir so the source stays pristine.
144
- const runDir = mkdtempSync(joinPath(tmpdir(), 'gdd-e2e-'));
145
- try {
146
- copyDirSync(srcFixture, runDir);
147
- } catch (err) {
148
- return failEarly(opts.mode, Date.now() - t0, [
149
- `fixture copy failed: ${err instanceof Error ? err.message : String(err)}`,
150
- ]);
151
- }
152
-
153
- // Resolve gdd-sdk binary.
154
- const gddSdkBin =
155
- opts.gddSdkBin !== undefined
156
- ? resolvePath(opts.gddSdkBin)
157
- : resolvePath(process.cwd(), 'bin', 'gdd-sdk');
158
-
159
- // Point the subprocess's event-stream at the fixture copy's
160
- // .design/telemetry so the events.jsonl assertion sees the run's
161
- // transitions. The subprocess cannot chdir into runDir (its
162
- // package.json is the fixture stub without the real repo's
163
- // scripts/lib/ tree — createRequire in session-runner/errors.ts
164
- // would fail resolving error-classifier.cjs), so we keep cwd at
165
- // the repo root and steer writes via the env var instead.
166
- const subprocessEnv: Record<string, string | undefined> = {
167
- ...env,
168
- GDD_EVENTS_PATH: joinPath(runDir, '.design/telemetry/events.jsonl'),
169
- };
170
-
171
- // Build argv.
172
- const argv: string[] =
173
- opts.mode === 'dry-run'
174
- ? [
175
- 'run',
176
- '--dry-run',
177
- '--cwd',
178
- runDir,
179
- '--fixture',
180
- runDir,
181
- '--json',
182
- '--log-level',
183
- 'warn',
184
- ]
185
- : [
186
- 'run',
187
- '--cwd',
188
- runDir,
189
- '--budget-usd',
190
- String(maxUsdCost),
191
- '--max-turns',
192
- '20',
193
- '--json',
194
- '--log-level',
195
- 'info',
196
- ];
197
-
198
- // Spawn gdd-sdk from the REPO root (so createRequire anchors resolve
199
- // correctly) but steer all state + event writes into runDir via
200
- // --cwd and GDD_EVENTS_PATH.
201
- const subprocessCwd = process.cwd();
202
- const spawned = await spawnWithTimeout({
203
- bin: gddSdkBin,
204
- argv,
205
- cwd: subprocessCwd,
206
- env: subprocessEnv,
207
- timeoutMs,
208
- });
209
-
210
- const exitCode = spawned.exitCode;
211
- const stdout = spawned.stdout;
212
- const stderr = spawned.stderr;
213
-
214
- // Parse pipeline-result JSON from stdout (last JSON object in tail).
215
- const pipelineResult = extractPipelineResult(stdout);
216
-
217
- // Assert.
218
- const assertionFailures: string[] = [];
219
-
220
- // 1. Exit code.
221
- if (exitCode !== 0) {
222
- assertionFailures.push(`gdd-sdk exited with code ${exitCode} (expected 0)`);
223
- }
224
-
225
- // 2. Wall-clock.
226
- const elapsed = Date.now() - t0;
227
- if (elapsed > timeoutMs) {
228
- assertionFailures.push(`wall-clock ${elapsed}ms exceeded cap ${timeoutMs}ms`);
229
- }
230
-
231
- // 3. Pipeline result shape.
232
- if (pipelineResult === null) {
233
- assertionFailures.push('pipeline-result JSON could not be parsed from stdout');
234
- } else if (pipelineResult.status !== 'completed') {
235
- assertionFailures.push(
236
- `pipeline status = ${pipelineResult.status} (expected "completed")`,
237
- );
238
- }
239
-
240
- // 4. Cost gates.
241
- const usdCost = pipelineResult?.total_usage?.usd_cost ?? 0;
242
- if (opts.mode === 'dry-run' && usdCost !== 0) {
243
- assertionFailures.push(`dry-run usd_cost = ${usdCost} (expected 0)`);
244
- }
245
- if (opts.mode === 'live' && usdCost >= maxUsdCost) {
246
- assertionFailures.push(
247
- `live usd_cost = ${usdCost} exceeds cap ${maxUsdCost}`,
248
- );
249
- }
250
-
251
- // 5. Artifact presence.
252
- const expected: readonly string[] = [
253
- '.design/BRIEF.md',
254
- '.design/DESIGN-PATTERNS.md',
255
- '.design/DESIGN-PLAN.md',
256
- '.design/DESIGN.md',
257
- '.design/SUMMARY.md',
258
- ];
259
- const artifacts: Record<string, { exists: boolean; bytes: number }> = {};
260
- for (const rel of expected) {
261
- const abs = joinPath(runDir, rel);
262
- if (existsSync(abs)) {
263
- const st = statSync(abs);
264
- artifacts[rel] = { exists: true, bytes: st.size };
265
- } else {
266
- artifacts[rel] = { exists: false, bytes: 0 };
267
- assertionFailures.push(`missing artifact: ${rel}`);
268
- }
269
- }
270
-
271
- // 6. Content-pattern assertions.
272
- const patternsPath = joinPath(runDir, '.design/DESIGN-PATTERNS.md');
273
- if (existsSync(patternsPath)) {
274
- const body = readFileSync(patternsPath, 'utf8');
275
- for (const heading of ['## Tokens', '## Components', '## Accessibility', '## Visual Hierarchy']) {
276
- if (!body.includes(heading)) {
277
- assertionFailures.push(`DESIGN-PATTERNS.md missing heading "${heading}"`);
278
- }
279
- }
280
- }
281
-
282
- const planPath = joinPath(runDir, '.design/DESIGN-PLAN.md');
283
- if (existsSync(planPath)) {
284
- const body = readFileSync(planPath, 'utf8');
285
- if (!/(^|\n)##\s*Wave\b/i.test(body)) {
286
- assertionFailures.push('DESIGN-PLAN.md missing "Wave" section');
287
- }
288
- if (!body.includes('Type:')) {
289
- assertionFailures.push('DESIGN-PLAN.md missing "Type:" line');
290
- }
291
- }
292
-
293
- const summaryPath = joinPath(runDir, '.design/SUMMARY.md');
294
- if (existsSync(summaryPath)) {
295
- const body = readFileSync(summaryPath, 'utf8');
296
- if (!body.includes('## VERIFY COMPLETE')) {
297
- assertionFailures.push('SUMMARY.md missing "## VERIFY COMPLETE" marker');
298
- }
299
- }
300
-
301
- // 7. events.jsonl: at least 5 state.transition events.
302
- // Phase 20 + 21 emit stage.entered/stage.exited; dry-run's permissive
303
- // transition override skips state.transition events. We accept
304
- // EITHER >=5 state.transition events OR >=5 stage.entered events
305
- // (the dry-run baseline). The Plan 21-11 success criterion is
306
- // "5 transition events emitted across run" — we honor the spirit
307
- // by checking the stage boundary count.
308
- const eventsPath = joinPath(runDir, '.design/telemetry/events.jsonl');
309
- if (existsSync(eventsPath)) {
310
- const body = readFileSync(eventsPath, 'utf8');
311
- const lines = body.split('\n').filter((l) => l.length > 0);
312
- let transitionCount = 0;
313
- let stageEnteredCount = 0;
314
- for (const line of lines) {
315
- try {
316
- const ev = JSON.parse(line) as { type?: string };
317
- if (ev.type === 'state.transition') transitionCount++;
318
- if (ev.type === 'stage.entered') stageEnteredCount++;
319
- } catch {
320
- // Skip unparseable lines (shouldn't happen — JSONL is
321
- // newline-delimited JSON per line).
322
- }
323
- }
324
- const boundaryCount = Math.max(transitionCount, stageEnteredCount);
325
- if (boundaryCount < 5) {
326
- assertionFailures.push(
327
- `events.jsonl has ${boundaryCount} stage-boundary events (expected >=5; ` +
328
- `${transitionCount} state.transition + ${stageEnteredCount} stage.entered)`,
329
- );
330
- }
331
- } else {
332
- assertionFailures.push('missing events.jsonl');
333
- }
334
-
335
- const status: E2EStatus =
336
- assertionFailures.length === 0 && exitCode === 0 ? 'pass' : 'fail';
337
-
338
- return {
339
- status,
340
- mode: opts.mode,
341
- duration_ms: elapsed,
342
- usd_cost: usdCost,
343
- artifacts: Object.freeze(artifacts),
344
- assertion_failures: Object.freeze(assertionFailures),
345
- exit_code: exitCode,
346
- run_dir: runDir,
347
- stdout_tail: tailBytes(stdout, 64 * 1024),
348
- stderr_tail: tailBytes(stderr, 64 * 1024),
349
- };
350
- }
351
-
352
- // ---------------------------------------------------------------------------
353
- // Helpers.
354
- // ---------------------------------------------------------------------------
355
-
356
- /** Build a skipped/failed result without spawning anything. */
357
- function failEarly(
358
- mode: E2EMode,
359
- durationMs: number,
360
- failures: readonly string[],
361
- ): E2EResult {
362
- return {
363
- status: 'fail',
364
- mode,
365
- duration_ms: durationMs,
366
- usd_cost: 0,
367
- artifacts: Object.freeze({}),
368
- assertion_failures: Object.freeze([...failures]),
369
- exit_code: -1,
370
- run_dir: '',
371
- stdout_tail: '',
372
- stderr_tail: '',
373
- };
374
- }
375
-
376
- interface SpawnOutcome {
377
- readonly exitCode: number;
378
- readonly stdout: string;
379
- readonly stderr: string;
380
- readonly timedOut: boolean;
381
- }
382
-
383
- /**
384
- * Spawn `gdd-sdk` with an externally-enforced wall-clock timeout.
385
- * Node's child_process does not expose a native timeout on async calls
386
- * matching our needs — we roll our own via setTimeout + signal kill.
387
- */
388
- function spawnWithTimeout(args: {
389
- readonly bin: string;
390
- readonly argv: readonly string[];
391
- readonly cwd: string;
392
- readonly env: Readonly<Record<string, string | undefined>>;
393
- readonly timeoutMs: number;
394
- }): Promise<SpawnOutcome> {
395
- return new Promise<SpawnOutcome>((resolve) => {
396
- // Compose the node invocation explicitly so we do not depend on a
397
- // `.cmd` shim being discoverable on Windows. The bin is the CJS
398
- // trampoline (bin/gdd-sdk), which spawns TS — we skip that layer
399
- // by invoking the TS entry directly with the experimental flag.
400
- const tsEntry = resolvePath(dirname(args.bin), '..', 'scripts', 'lib', 'cli', 'index.ts');
401
- const nodeArgs = ['--experimental-strip-types', tsEntry, ...args.argv];
402
- const child = spawn(process.execPath, nodeArgs, {
403
- cwd: args.cwd,
404
- env: args.env as NodeJS.ProcessEnv,
405
- stdio: ['ignore', 'pipe', 'pipe'],
406
- shell: false,
407
- });
408
-
409
- const stdoutChunks: Buffer[] = [];
410
- const stderrChunks: Buffer[] = [];
411
- child.stdout.on('data', (b: Buffer) => stdoutChunks.push(b));
412
- child.stderr.on('data', (b: Buffer) => stderrChunks.push(b));
413
-
414
- let timedOut = false;
415
- const timer = setTimeout(() => {
416
- timedOut = true;
417
- try {
418
- child.kill('SIGTERM');
419
- } catch {
420
- // Best-effort kill.
421
- }
422
- }, args.timeoutMs);
423
- timer.unref();
424
-
425
- child.on('error', () => {
426
- clearTimeout(timer);
427
- resolve({
428
- exitCode: -1,
429
- stdout: Buffer.concat(stdoutChunks).toString('utf8'),
430
- stderr: Buffer.concat(stderrChunks).toString('utf8'),
431
- timedOut,
432
- });
433
- });
434
-
435
- child.on('exit', (code) => {
436
- clearTimeout(timer);
437
- resolve({
438
- exitCode: typeof code === 'number' ? code : -1,
439
- stdout: Buffer.concat(stdoutChunks).toString('utf8'),
440
- stderr: Buffer.concat(stderrChunks).toString('utf8'),
441
- timedOut,
442
- });
443
- });
444
- });
445
- }
446
-
447
- /**
448
- * Tight-loop recursive copy. Avoids the `fs.cpSync` dependency to keep
449
- * this module Node-16-compatible (experimental-strip-types works under
450
- * 22; cpSync is stable there, but we prefer explicit behavior for
451
- * test-fixtures).
452
- */
453
- function copyDirSync(src: string, dest: string): void {
454
- if (!existsSync(dest)) {
455
- mkdirSync(dest, { recursive: true });
456
- }
457
- for (const entry of readdirSync(src, { withFileTypes: true })) {
458
- const srcPath = joinPath(src, entry.name);
459
- const destPath = joinPath(dest, entry.name);
460
- if (entry.isDirectory()) {
461
- copyDirSync(srcPath, destPath);
462
- } else if (entry.isFile()) {
463
- const data = readFileSync(srcPath);
464
- const parent = dirname(destPath);
465
- if (!existsSync(parent)) mkdirSync(parent, { recursive: true });
466
- writeFileSync(destPath, data);
467
- }
468
- // Symlinks + other types are skipped — fixtures are plain files.
469
- }
470
- // Silence "unused import" under exactOptionalPropertyTypes.
471
- void relPath;
472
- }
473
-
474
- /**
475
- * Extract the gdd-sdk pipeline-result JSON from stdout. The CLI emits
476
- * a single pretty-printed JSON object under `--json`; older output may
477
- * have been prefixed by node warnings. We locate the last `{` that
478
- * begins a top-level object and attempt to parse from there.
479
- */
480
- function extractPipelineResult(stdout: string): { status?: string; total_usage?: { usd_cost?: number } } | null {
481
- // Fast path: whole stdout is the JSON body.
482
- const trimmed = stdout.trim();
483
- if (trimmed.startsWith('{')) {
484
- try {
485
- return JSON.parse(trimmed) as { status?: string; total_usage?: { usd_cost?: number } };
486
- } catch {
487
- // Fall through to slow-path heuristic.
488
- }
489
- }
490
- // Slow path: find the last `}\n` and scan backward for a matching
491
- // `{` at the start of a line.
492
- const lines = stdout.split('\n');
493
- for (let end = lines.length - 1; end >= 0; end--) {
494
- const endLine = lines[end];
495
- if (endLine === undefined || endLine.trim() !== '}') continue;
496
- for (let start = end - 1; start >= 0; start--) {
497
- const startLine = lines[start];
498
- if (startLine !== undefined && startLine.trim() === '{') {
499
- const candidate = lines.slice(start, end + 1).join('\n');
500
- try {
501
- return JSON.parse(candidate) as { status?: string; total_usage?: { usd_cost?: number } };
502
- } catch {
503
- // Keep scanning.
504
- }
505
- }
506
- }
507
- }
508
- return null;
509
- }
510
-
511
- function tailBytes(s: string, cap: number): string {
512
- if (s.length <= cap) return s;
513
- return '…' + s.slice(s.length - cap + 1);
514
- }
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- // Extract the `## [<version>]` section of CHANGELOG.md and print to stdout.
4
- // Used by .github/workflows/release.yml to build the GitHub Release body.
5
- //
6
- // Usage: node scripts/extract-changelog-section.cjs <version>
7
- // Example: node scripts/extract-changelog-section.cjs 1.0.7
8
- //
9
- // Exit codes:
10
- // 0 — section found, body printed
11
- // 1 — no matching section
12
- // 2 — missing or empty version argument
13
-
14
- const fs = require('fs');
15
- const path = require('path');
16
-
17
- const version = (process.argv[2] || '').trim();
18
- if (!version) {
19
- console.error('Usage: node scripts/extract-changelog-section.cjs <version>');
20
- process.exit(2);
21
- }
22
-
23
- const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
24
- if (!fs.existsSync(changelogPath)) {
25
- console.error(`ERROR: CHANGELOG.md not found at ${changelogPath}`);
26
- process.exit(1);
27
- }
28
-
29
- const body = fs.readFileSync(changelogPath, 'utf8').replace(/\r\n/g, '\n');
30
- const lines = body.split('\n');
31
- const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
- const headingRe = new RegExp(`^##\\s*\\[${escapedVersion}\\]`);
33
- const nextHeadingRe = /^##\s*\[/;
34
-
35
- let capture = false;
36
- const out = [];
37
- for (let i = 0; i < lines.length; i++) {
38
- const line = lines[i];
39
- if (!capture) {
40
- if (headingRe.test(line)) capture = true;
41
- continue;
42
- }
43
- // Stop at next version heading or a standalone horizontal rule
44
- if (nextHeadingRe.test(line)) break;
45
- if (line.trim() === '---') break;
46
- out.push(line);
47
- }
48
-
49
- if (!capture) {
50
- console.error(`ERROR: no section for version ${version}`);
51
- process.exit(1);
52
- }
53
-
54
- // Trim leading + trailing blank lines
55
- while (out.length && out[0].trim() === '') out.shift();
56
- while (out.length && out[out.length - 1].trim() === '') out.pop();
57
-
58
- process.stdout.write(out.join('\n') + '\n');