@hegemonart/get-design-done 1.19.6 → 1.21.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 (140) hide show
  1. package/.claude-plugin/marketplace.json +11 -14
  2. package/.claude-plugin/plugin.json +9 -32
  3. package/CHANGELOG.md +138 -0
  4. package/README.md +54 -1
  5. package/agents/design-reflector.md +13 -0
  6. package/bin/gdd-sdk +55 -0
  7. package/connections/connections.md +3 -0
  8. package/connections/figma.md +2 -0
  9. package/connections/gdd-state.md +186 -0
  10. package/hooks/budget-enforcer.ts +716 -0
  11. package/hooks/context-exhaustion.ts +251 -0
  12. package/hooks/gdd-read-injection-scanner.ts +172 -0
  13. package/hooks/hooks.json +3 -3
  14. package/package.json +32 -51
  15. package/reference/codex-tools.md +53 -0
  16. package/reference/config-schema.md +2 -2
  17. package/reference/error-recovery.md +58 -0
  18. package/reference/gemini-tools.md +53 -0
  19. package/reference/registry.json +21 -0
  20. package/reference/schemas/budget.schema.json +42 -0
  21. package/reference/schemas/events.schema.json +55 -0
  22. package/reference/schemas/generated.d.ts +419 -0
  23. package/reference/schemas/iteration-budget.schema.json +36 -0
  24. package/reference/schemas/mcp-gdd-state-tools.schema.json +89 -0
  25. package/reference/schemas/rate-limits.schema.json +31 -0
  26. package/scripts/aggregate-agent-metrics.ts +282 -0
  27. package/scripts/codegen-schema-types.ts +149 -0
  28. package/scripts/e2e/run-headless.ts +514 -0
  29. package/scripts/lib/cli/commands/audit.ts +382 -0
  30. package/scripts/lib/cli/commands/init.ts +217 -0
  31. package/scripts/lib/cli/commands/query.ts +329 -0
  32. package/scripts/lib/cli/commands/run.ts +656 -0
  33. package/scripts/lib/cli/commands/stage.ts +468 -0
  34. package/scripts/lib/cli/index.ts +167 -0
  35. package/scripts/lib/cli/parse-args.ts +336 -0
  36. package/scripts/lib/context-engine/index.ts +116 -0
  37. package/scripts/lib/context-engine/manifest.ts +69 -0
  38. package/scripts/lib/context-engine/truncate.ts +282 -0
  39. package/scripts/lib/context-engine/types.ts +59 -0
  40. package/scripts/lib/discuss-parallel-runner/aggregator.ts +448 -0
  41. package/scripts/lib/discuss-parallel-runner/discussants.ts +430 -0
  42. package/scripts/lib/discuss-parallel-runner/index.ts +223 -0
  43. package/scripts/lib/discuss-parallel-runner/types.ts +184 -0
  44. package/scripts/lib/error-classifier.cjs +232 -0
  45. package/scripts/lib/error-classifier.d.cts +44 -0
  46. package/scripts/lib/event-stream/emitter.ts +88 -0
  47. package/scripts/lib/event-stream/index.ts +164 -0
  48. package/scripts/lib/event-stream/types.ts +127 -0
  49. package/scripts/lib/event-stream/writer.ts +154 -0
  50. package/scripts/lib/explore-parallel-runner/index.ts +294 -0
  51. package/scripts/lib/explore-parallel-runner/mappers.ts +290 -0
  52. package/scripts/lib/explore-parallel-runner/synthesizer.ts +295 -0
  53. package/scripts/lib/explore-parallel-runner/types.ts +139 -0
  54. package/scripts/lib/gdd-errors/classification.ts +124 -0
  55. package/scripts/lib/gdd-errors/index.ts +218 -0
  56. package/scripts/lib/gdd-state/gates.ts +216 -0
  57. package/scripts/lib/gdd-state/index.ts +167 -0
  58. package/scripts/lib/gdd-state/lockfile.ts +232 -0
  59. package/scripts/lib/gdd-state/mutator.ts +574 -0
  60. package/scripts/lib/gdd-state/parser.ts +523 -0
  61. package/scripts/lib/gdd-state/types.ts +179 -0
  62. package/scripts/lib/harness/detect.ts +90 -0
  63. package/scripts/lib/harness/index.ts +64 -0
  64. package/scripts/lib/harness/tool-map.ts +142 -0
  65. package/scripts/lib/init-runner/index.ts +396 -0
  66. package/scripts/lib/init-runner/researchers.ts +245 -0
  67. package/scripts/lib/init-runner/scaffold.ts +224 -0
  68. package/scripts/lib/init-runner/synthesizer.ts +224 -0
  69. package/scripts/lib/init-runner/types.ts +143 -0
  70. package/scripts/lib/iteration-budget.cjs +205 -0
  71. package/scripts/lib/iteration-budget.d.cts +32 -0
  72. package/scripts/lib/jittered-backoff.cjs +112 -0
  73. package/scripts/lib/jittered-backoff.d.cts +38 -0
  74. package/scripts/lib/lockfile.cjs +177 -0
  75. package/scripts/lib/lockfile.d.cts +21 -0
  76. package/scripts/lib/logger/index.ts +251 -0
  77. package/scripts/lib/logger/sinks.ts +269 -0
  78. package/scripts/lib/logger/types.ts +110 -0
  79. package/scripts/lib/pipeline-runner/human-gate.ts +134 -0
  80. package/scripts/lib/pipeline-runner/index.ts +527 -0
  81. package/scripts/lib/pipeline-runner/stage-handlers.ts +339 -0
  82. package/scripts/lib/pipeline-runner/state-machine.ts +144 -0
  83. package/scripts/lib/pipeline-runner/types.ts +183 -0
  84. package/scripts/lib/prompt-sanitizer/index.ts +435 -0
  85. package/scripts/lib/prompt-sanitizer/patterns.ts +173 -0
  86. package/scripts/lib/rate-guard.cjs +365 -0
  87. package/scripts/lib/rate-guard.d.cts +38 -0
  88. package/scripts/lib/session-runner/errors.ts +406 -0
  89. package/scripts/lib/session-runner/index.ts +715 -0
  90. package/scripts/lib/session-runner/transcript.ts +189 -0
  91. package/scripts/lib/session-runner/types.ts +144 -0
  92. package/scripts/lib/tool-scoping/index.ts +219 -0
  93. package/scripts/lib/tool-scoping/parse-agent-tools.ts +207 -0
  94. package/scripts/lib/tool-scoping/stage-scopes.ts +139 -0
  95. package/scripts/lib/tool-scoping/types.ts +77 -0
  96. package/scripts/mcp-servers/gdd-state/schemas/add_blocker.schema.json +67 -0
  97. package/scripts/mcp-servers/gdd-state/schemas/add_decision.schema.json +68 -0
  98. package/scripts/mcp-servers/gdd-state/schemas/add_must_have.schema.json +68 -0
  99. package/scripts/mcp-servers/gdd-state/schemas/checkpoint.schema.json +51 -0
  100. package/scripts/mcp-servers/gdd-state/schemas/frontmatter_update.schema.json +62 -0
  101. package/scripts/mcp-servers/gdd-state/schemas/get.schema.json +51 -0
  102. package/scripts/mcp-servers/gdd-state/schemas/probe_connections.schema.json +75 -0
  103. package/scripts/mcp-servers/gdd-state/schemas/resolve_blocker.schema.json +66 -0
  104. package/scripts/mcp-servers/gdd-state/schemas/set_status.schema.json +47 -0
  105. package/scripts/mcp-servers/gdd-state/schemas/transition_stage.schema.json +70 -0
  106. package/scripts/mcp-servers/gdd-state/schemas/update_progress.schema.json +58 -0
  107. package/scripts/mcp-servers/gdd-state/server.ts +288 -0
  108. package/scripts/mcp-servers/gdd-state/tools/add_blocker.ts +72 -0
  109. package/scripts/mcp-servers/gdd-state/tools/add_decision.ts +89 -0
  110. package/scripts/mcp-servers/gdd-state/tools/add_must_have.ts +113 -0
  111. package/scripts/mcp-servers/gdd-state/tools/checkpoint.ts +60 -0
  112. package/scripts/mcp-servers/gdd-state/tools/frontmatter_update.ts +91 -0
  113. package/scripts/mcp-servers/gdd-state/tools/get.ts +51 -0
  114. package/scripts/mcp-servers/gdd-state/tools/index.ts +51 -0
  115. package/scripts/mcp-servers/gdd-state/tools/probe_connections.ts +73 -0
  116. package/scripts/mcp-servers/gdd-state/tools/resolve_blocker.ts +84 -0
  117. package/scripts/mcp-servers/gdd-state/tools/set_status.ts +54 -0
  118. package/scripts/mcp-servers/gdd-state/tools/shared.ts +194 -0
  119. package/scripts/mcp-servers/gdd-state/tools/transition_stage.ts +80 -0
  120. package/scripts/mcp-servers/gdd-state/tools/update_progress.ts +81 -0
  121. package/scripts/validate-frontmatter.ts +114 -0
  122. package/scripts/validate-schemas.ts +401 -0
  123. package/skills/brief/SKILL.md +15 -6
  124. package/skills/design/SKILL.md +31 -13
  125. package/skills/explore/SKILL.md +41 -17
  126. package/skills/health/SKILL.md +15 -4
  127. package/skills/optimize/SKILL.md +3 -3
  128. package/skills/pause/SKILL.md +16 -10
  129. package/skills/plan/SKILL.md +33 -17
  130. package/skills/progress/SKILL.md +15 -11
  131. package/skills/resume/SKILL.md +19 -10
  132. package/skills/settings/SKILL.md +11 -3
  133. package/skills/todo/SKILL.md +12 -3
  134. package/skills/verify/SKILL.md +65 -29
  135. package/hooks/budget-enforcer.js +0 -329
  136. package/hooks/context-exhaustion.js +0 -127
  137. package/hooks/gdd-read-injection-scanner.js +0 -39
  138. package/scripts/aggregate-agent-metrics.js +0 -173
  139. package/scripts/validate-frontmatter.cjs +0 -68
  140. package/scripts/validate-schemas.cjs +0 -242
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/hegemonart/get-design-done/main/reference/schemas/iteration-budget.schema.json",
4
+ "title": "IterationBudget",
5
+ "description": "Shape of .design/iteration-budget.json produced by scripts/lib/iteration-budget.cjs. Caps the number of fix-loop iterations that can consume context before the pipeline halts for user input. All mutations are coordinated by scripts/lib/lockfile.cjs and written via temp+rename. See .planning/phases/20-gdd-sdk-foundation/20-14-PLAN.md §Task 4.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["budget", "remaining", "consumed", "refunded", "updatedAt"],
9
+ "properties": {
10
+ "budget": {
11
+ "type": "integer",
12
+ "minimum": 0,
13
+ "description": "The configured ceiling. Initialized by reset(). `remaining` never exceeds this value after refund()."
14
+ },
15
+ "remaining": {
16
+ "type": "integer",
17
+ "minimum": 0,
18
+ "description": "Iterations still available for consume() calls. Starts at `budget`, drops on consume, climbs (capped at `budget`) on refund."
19
+ },
20
+ "consumed": {
21
+ "type": "integer",
22
+ "minimum": 0,
23
+ "description": "Running total of successful consume() calls since last reset()."
24
+ },
25
+ "refunded": {
26
+ "type": "integer",
27
+ "minimum": 0,
28
+ "description": "Running total of refund amount since last reset() (useful for auditing the cache-hit refund path from budget-enforcer.ts)."
29
+ },
30
+ "updatedAt": {
31
+ "type": "string",
32
+ "format": "date-time",
33
+ "description": "ISO-8601 timestamp of the last mutation."
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,89 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/hegemonart/get-design-done/main/reference/schemas/mcp-gdd-state-tools.schema.json",
4
+ "title": "McpGddStateTools",
5
+ "description": "Combined manifest of all 11 gdd-state MCP tool input+output schemas (Plan 20-05). Individual tool schemas live under scripts/mcp-servers/gdd-state/schemas/ and the tool handlers reference them; this combined schema exists so downstream validators and codegen can compile a single surface.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["tools"],
9
+ "properties": {
10
+ "tools": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": [
14
+ "gdd_state__get",
15
+ "gdd_state__update_progress",
16
+ "gdd_state__transition_stage",
17
+ "gdd_state__add_blocker",
18
+ "gdd_state__resolve_blocker",
19
+ "gdd_state__add_decision",
20
+ "gdd_state__add_must_have",
21
+ "gdd_state__set_status",
22
+ "gdd_state__checkpoint",
23
+ "gdd_state__probe_connections",
24
+ "gdd_state__frontmatter_update"
25
+ ],
26
+ "properties": {
27
+ "gdd_state__get": { "$ref": "#/definitions/ToolSchemaEntry" },
28
+ "gdd_state__update_progress": { "$ref": "#/definitions/ToolSchemaEntry" },
29
+ "gdd_state__transition_stage": { "$ref": "#/definitions/ToolSchemaEntry" },
30
+ "gdd_state__add_blocker": { "$ref": "#/definitions/ToolSchemaEntry" },
31
+ "gdd_state__resolve_blocker": { "$ref": "#/definitions/ToolSchemaEntry" },
32
+ "gdd_state__add_decision": { "$ref": "#/definitions/ToolSchemaEntry" },
33
+ "gdd_state__add_must_have": { "$ref": "#/definitions/ToolSchemaEntry" },
34
+ "gdd_state__set_status": { "$ref": "#/definitions/ToolSchemaEntry" },
35
+ "gdd_state__checkpoint": { "$ref": "#/definitions/ToolSchemaEntry" },
36
+ "gdd_state__probe_connections": { "$ref": "#/definitions/ToolSchemaEntry" },
37
+ "gdd_state__frontmatter_update": { "$ref": "#/definitions/ToolSchemaEntry" }
38
+ }
39
+ }
40
+ },
41
+ "definitions": {
42
+ "ToolSchemaEntry": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["input", "output"],
46
+ "properties": {
47
+ "input": {
48
+ "type": "object",
49
+ "description": "JSON Schema fragment describing the tool's input parameters."
50
+ },
51
+ "output": {
52
+ "type": "object",
53
+ "description": "JSON Schema fragment describing the tool's response envelope.",
54
+ "required": ["type"],
55
+ "properties": {
56
+ "type": { "type": "string", "enum": ["object"] }
57
+ }
58
+ }
59
+ }
60
+ },
61
+ "ToolError": {
62
+ "type": "object",
63
+ "additionalProperties": false,
64
+ "required": ["code", "message", "kind"],
65
+ "properties": {
66
+ "code": { "type": "string", "minLength": 1 },
67
+ "message": { "type": "string", "minLength": 1 },
68
+ "kind": {
69
+ "type": "string",
70
+ "enum": ["validation", "state_conflict", "operation_failed", "unknown"]
71
+ },
72
+ "context": {
73
+ "type": "object",
74
+ "additionalProperties": true
75
+ }
76
+ }
77
+ },
78
+ "ToolResponseEnvelope": {
79
+ "type": "object",
80
+ "additionalProperties": false,
81
+ "required": ["success"],
82
+ "properties": {
83
+ "success": { "type": "boolean" },
84
+ "data": { "type": "object" },
85
+ "error": { "$ref": "#/definitions/ToolError" }
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/hegemonart/get-design-done/main/reference/schemas/rate-limits.schema.json",
4
+ "title": "RateLimits",
5
+ "description": "Shape of .design/rate-limits/<provider>.json produced by scripts/lib/rate-guard.cjs. One file per provider (anthropic, openai, figma, ...) — header ingestion overwrites atomically via tmp+rename under scripts/lib/lockfile.cjs protection. See .planning/phases/20-gdd-sdk-foundation/20-14-PLAN.md §Task 2.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["provider", "remaining", "resetAt", "updatedAt"],
9
+ "properties": {
10
+ "provider": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "description": "Provider identifier (e.g. 'anthropic', 'openai', 'figma'). Matches the state file basename."
14
+ },
15
+ "remaining": {
16
+ "type": "integer",
17
+ "minimum": 0,
18
+ "description": "Number of API calls the provider says are still allowed before the next reset. When ingestion sees both requests-remaining and tokens-remaining, the lower value wins (most-restrictive)."
19
+ },
20
+ "resetAt": {
21
+ "type": "string",
22
+ "format": "date-time",
23
+ "description": "ISO-8601 timestamp when the rate-limit window resets. Synthesized from whichever header is present: retry-after (seconds or HTTP date), x-ratelimit-reset-requests / -tokens (Unix seconds), anthropic-ratelimit-requests-reset (ISO string). When multiple candidates are present, the latest resetAt wins."
24
+ },
25
+ "updatedAt": {
26
+ "type": "string",
27
+ "format": "date-time",
28
+ "description": "ISO-8601 timestamp when this state file was last written (ingestHeaders call time)."
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aggregate-agent-metrics.ts — Incremental per-agent aggregator.
4
+ *
5
+ * Reads: .design/telemetry/costs.jsonl (append-only ledger from
6
+ * hooks/budget-enforcer.js)
7
+ * agents/{agent}.md (frontmatter source for default-tier, parallel-safe,
8
+ * reads-only, typical-duration-seconds)
9
+ * Writes: .design/agent-metrics.json (atomic overwrite via tmp-file + rename)
10
+ * .design/telemetry/phase-totals.json (same, WR-02)
11
+ *
12
+ * Invoked:
13
+ * 1. Detached child of hooks/budget-enforcer.js after every telemetry write.
14
+ * 2. Directly by /gdd:optimize skill as an explicit refresh step.
15
+ * 3. Manually: `node --experimental-strip-types scripts/aggregate-agent-metrics.ts`
16
+ * 4. With `--help` to print usage (used by the Plan 20-00 smoke check).
17
+ *
18
+ * OPT-09 contract: fields must match Phase 11 reflector's expectations.
19
+ *
20
+ * Converted from scripts/aggregate-agent-metrics.js in Plan 20-00 (Tier-1).
21
+ * Behavior preserved verbatim.
22
+ */
23
+
24
+ import {
25
+ existsSync,
26
+ mkdirSync,
27
+ readFileSync,
28
+ writeFileSync,
29
+ renameSync,
30
+ } from 'node:fs';
31
+ import { join, dirname, basename } from 'node:path';
32
+
33
+ // Generated-type import (unused at runtime, erased by strip-types) to satisfy
34
+ // Plan 20-00's requirement that every Tier-1 TS file participates in the
35
+ // codegen graph. We pick AuthoritySnapshotSchema as a stable anchor and
36
+ // re-export for downstream callers.
37
+ import type { AuthoritySnapshotSchema } from '../reference/schemas/generated.js';
38
+ export type { AuthoritySnapshotSchema };
39
+
40
+ const CWD: string = process.cwd();
41
+ const TELEMETRY_PATH: string = join(CWD, '.design', 'telemetry', 'costs.jsonl');
42
+ const METRICS_PATH: string = join(CWD, '.design', 'agent-metrics.json');
43
+ const PHASE_TOTALS_PATH: string = join(CWD, '.design', 'telemetry', 'phase-totals.json');
44
+ const AGENTS_DIR: string = join(CWD, 'agents');
45
+
46
+ /**
47
+ * Subset of the agent-markdown frontmatter we care about. `null` means the
48
+ * field is absent or unparseable (aggregator is tolerant — degraded mode
49
+ * preferred over hard-fail per OPT-09).
50
+ */
51
+ interface AgentFrontmatter {
52
+ default_tier: string | null;
53
+ parallel_safe: boolean | null;
54
+ reads_only: boolean | null;
55
+ typical_duration_seconds: number | null;
56
+ }
57
+
58
+ /** ---- frontmatter reader (no YAML dep) ---- */
59
+ function readAgentFrontmatter(agentName: string): Partial<AgentFrontmatter> {
60
+ const p: string = join(AGENTS_DIR, `${agentName}.md`);
61
+ if (!existsSync(p)) return {};
62
+ try {
63
+ const content: string = readFileSync(p, 'utf8');
64
+ const fm = content.match(/^---\s*\n([\s\S]*?)\n---/);
65
+ if (!fm) return {};
66
+ const body: string = fm[1] ?? '';
67
+ const get = (key: string): string | null => {
68
+ const m = body.match(new RegExp(`^${key}:\\s*"?([^"\\n]+)"?`, 'm'));
69
+ return m && m[1] !== undefined ? m[1].trim() : null;
70
+ };
71
+ const defaultTier: string | null = get('default-tier');
72
+ const parallelSafe: string | null = get('parallel-safe');
73
+ const readsOnly: string | null = get('reads-only');
74
+ const typicalDuration: string | null = get('typical-duration-seconds');
75
+ return {
76
+ default_tier: defaultTier ?? null,
77
+ parallel_safe: parallelSafe === null ? null : /^(true|yes)$/i.test(parallelSafe),
78
+ reads_only: readsOnly === null ? null : /^(true|yes)$/i.test(readsOnly),
79
+ typical_duration_seconds:
80
+ typicalDuration === null ? null : Number(typicalDuration) || null,
81
+ };
82
+ } catch {
83
+ return {};
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Shape of a single row in .design/telemetry/costs.jsonl. Mirrors the OPT-09
89
+ * schema: nine mandatory fields + four optional diagnostic fields. Unknown
90
+ * keys are tolerated (Phase 11 reflector ignores them).
91
+ */
92
+ export interface CostRow {
93
+ ts?: string;
94
+ agent?: string;
95
+ tier?: string;
96
+ tokens_in?: number | string;
97
+ tokens_out?: number | string;
98
+ cache_hit?: boolean;
99
+ est_cost_usd?: number | string;
100
+ cycle?: string;
101
+ phase?: string;
102
+ // Optional / diagnostic
103
+ tier_downgraded?: boolean;
104
+ enforcement_mode?: string;
105
+ lazy_skipped?: boolean;
106
+ block_reason?: string;
107
+ }
108
+
109
+ /** ---- telemetry reader ---- */
110
+ function readTelemetryRows(): CostRow[] {
111
+ if (!existsSync(TELEMETRY_PATH)) return [];
112
+ const raw: string = readFileSync(TELEMETRY_PATH, 'utf8');
113
+ const out: CostRow[] = [];
114
+ for (const line of raw.split(/\r?\n/)) {
115
+ if (!line.trim()) continue;
116
+ try {
117
+ out.push(JSON.parse(line) as CostRow);
118
+ } catch {
119
+ // tolerant: skip malformed lines (partial write, truncation)
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+
125
+ /** Per-agent roll-up accumulator. */
126
+ interface AgentAccumulator {
127
+ total_spawns: number;
128
+ total_cost_usd: number;
129
+ total_tokens_in: number;
130
+ total_tokens_out: number;
131
+ cache_hits: number;
132
+ lazy_skips: number;
133
+ }
134
+
135
+ /** Final per-agent shape written to .design/agent-metrics.json. */
136
+ export interface AgentMetrics {
137
+ typical_duration_seconds: number | null | undefined;
138
+ default_tier: string | null | undefined;
139
+ parallel_safe: boolean | null | undefined;
140
+ reads_only: boolean | null | undefined;
141
+ total_spawns: number;
142
+ total_cost_usd: number;
143
+ total_tokens_in: number;
144
+ total_tokens_out: number;
145
+ cache_hit_rate: number;
146
+ lazy_skip_rate: number;
147
+ }
148
+
149
+ /** ---- aggregator ---- */
150
+ function aggregate(rows: readonly CostRow[]): Record<string, AgentMetrics> {
151
+ const byAgent = new Map<string, AgentAccumulator>();
152
+ for (const r of rows) {
153
+ // Blocked rows represent a spawn that was denied at the hook — the agent
154
+ // never actually ran, so it must not contribute to spawn counts, cost, or
155
+ // token totals. Skip them here (mirror of the filter in aggregateByPhase).
156
+ if (r.block_reason) continue;
157
+ const agent: string = r.agent ?? 'unknown';
158
+ let a = byAgent.get(agent);
159
+ if (!a) {
160
+ a = {
161
+ total_spawns: 0,
162
+ total_cost_usd: 0,
163
+ total_tokens_in: 0,
164
+ total_tokens_out: 0,
165
+ cache_hits: 0,
166
+ lazy_skips: 0,
167
+ };
168
+ byAgent.set(agent, a);
169
+ }
170
+ a.total_spawns += 1;
171
+ a.total_cost_usd += Number(r.est_cost_usd ?? 0);
172
+ a.total_tokens_in += Number(r.tokens_in ?? 0);
173
+ a.total_tokens_out += Number(r.tokens_out ?? 0);
174
+ if (r.cache_hit === true) a.cache_hits += 1;
175
+ if (r.lazy_skipped === true) a.lazy_skips += 1;
176
+ }
177
+
178
+ const out: Record<string, AgentMetrics> = {};
179
+ for (const [agent, a] of byAgent.entries()) {
180
+ const fm = readAgentFrontmatter(agent);
181
+ const spawns: number = a.total_spawns || 1; // guard div-by-zero
182
+ out[agent] = {
183
+ typical_duration_seconds: fm.typical_duration_seconds,
184
+ default_tier: fm.default_tier,
185
+ parallel_safe: fm.parallel_safe,
186
+ reads_only: fm.reads_only,
187
+ total_spawns: a.total_spawns,
188
+ total_cost_usd: Number(a.total_cost_usd.toFixed(6)),
189
+ total_tokens_in: a.total_tokens_in,
190
+ total_tokens_out: a.total_tokens_out,
191
+ cache_hit_rate: Number((a.cache_hits / spawns).toFixed(4)),
192
+ lazy_skip_rate: Number((a.lazy_skips / spawns).toFixed(4)),
193
+ };
194
+ }
195
+ return out;
196
+ }
197
+
198
+ /** ---- atomic write ---- */
199
+ function writeAtomic(filePath: string, content: string): void {
200
+ const dir: string = dirname(filePath);
201
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
202
+ const tmp: string = join(
203
+ dir,
204
+ `.${basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
205
+ );
206
+ writeFileSync(tmp, content, 'utf8');
207
+ renameSync(tmp, filePath);
208
+ }
209
+
210
+ /** ---- phase totals aggregator (WR-02: avoids full JSONL replay in budget enforcer) ---- */
211
+ function aggregateByPhase(rows: readonly CostRow[]): Record<string, number> {
212
+ const byPhase: Record<string, number> = {};
213
+ for (const r of rows) {
214
+ // Blocked rows represent spawns that were denied by the hook — the agent
215
+ // never ran, so their est_cost_usd must not inflate cumulative phase spend.
216
+ // Counting them would make future hard-block and soft-threshold checks
217
+ // stricter than intended on every repeat cap hit.
218
+ if (r.block_reason) continue;
219
+ const phase: string = r.phase ?? 'unknown';
220
+ byPhase[phase] = (byPhase[phase] ?? 0) + Number(r.est_cost_usd ?? 0);
221
+ }
222
+ // Round to 6dp to match per-agent precision
223
+ for (const k of Object.keys(byPhase)) {
224
+ const v: number = byPhase[k] ?? 0;
225
+ byPhase[k] = Number(v.toFixed(6));
226
+ }
227
+ return byPhase;
228
+ }
229
+
230
+ /** ---- usage / --help ---- */
231
+ function printHelp(): void {
232
+ console.log(
233
+ `aggregate-agent-metrics.ts — Aggregate per-agent telemetry from .design/telemetry/costs.jsonl.\n` +
234
+ `\n` +
235
+ `Usage:\n` +
236
+ ` node --experimental-strip-types scripts/aggregate-agent-metrics.ts\n` +
237
+ ` node --experimental-strip-types scripts/aggregate-agent-metrics.ts --help\n` +
238
+ `\n` +
239
+ `Reads: .design/telemetry/costs.jsonl\n` +
240
+ ` agents/<agent>.md (frontmatter)\n` +
241
+ `Writes: .design/agent-metrics.json\n` +
242
+ ` .design/telemetry/phase-totals.json\n` +
243
+ `\n` +
244
+ `Invoked:\n` +
245
+ ` - Detached child of hooks/budget-enforcer.js after every telemetry row.\n` +
246
+ ` - Directly by /gdd:optimize as an explicit refresh step.\n` +
247
+ ` - Manually, on demand.\n`,
248
+ );
249
+ }
250
+
251
+ /** ---- main ---- */
252
+ function main(): void {
253
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
254
+ printHelp();
255
+ process.exit(0);
256
+ }
257
+
258
+ const rows: CostRow[] = readTelemetryRows();
259
+ const agents = aggregate(rows);
260
+ const payload = {
261
+ generated_at: new Date().toISOString(),
262
+ agents,
263
+ };
264
+ writeAtomic(METRICS_PATH, JSON.stringify(payload, null, 2) + '\n');
265
+ // Write lightweight phase-totals.json so budget-enforcer can read phase
266
+ // spend in O(1) without replaying the full JSONL on every agent spawn
267
+ // (WR-02).
268
+ const phaseTotals = {
269
+ generated_at: new Date().toISOString(),
270
+ totals: aggregateByPhase(rows),
271
+ };
272
+ writeAtomic(PHASE_TOTALS_PATH, JSON.stringify(phaseTotals, null, 2) + '\n');
273
+ }
274
+
275
+ try {
276
+ main();
277
+ } catch (err) {
278
+ // Fail open: aggregator must never block the hook or /gdd:optimize flow.
279
+ const msg: string = err instanceof Error ? err.message : String(err);
280
+ process.stderr.write(`aggregate-agent-metrics: ${msg}\n`);
281
+ process.exit(0);
282
+ }
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * codegen-schema-types.ts — Generate TypeScript interface declarations from
4
+ * every Draft-07 JSON Schema under `reference/schemas/*.schema.json`.
5
+ *
6
+ * Output: `reference/schemas/generated.d.ts` — single file containing one
7
+ * `export interface XSchema` per schema, named from the filename stem.
8
+ *
9
+ * Invoked: `npm run codegen:schemas` (requires repo-root cwd, which npm sets
10
+ * automatically). If invoked directly, `--repo-root <path>` can override.
11
+ *
12
+ * Exit codes:
13
+ * 0 — success
14
+ * 1 — any read/parse/compile failure
15
+ */
16
+ import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
17
+ import { resolve, join, dirname, basename } from 'node:path';
18
+ import { compile } from 'json-schema-to-typescript';
19
+
20
+ /**
21
+ * Resolve the repo root. Priority:
22
+ * 1. `--repo-root <path>` CLI arg.
23
+ * 2. `process.cwd()` — npm scripts run from the package root, so this is
24
+ * the common case.
25
+ *
26
+ * We deliberately avoid `import.meta.url` / `__dirname` so this module stays
27
+ * valid under both CommonJS type-checking (Node16 + no package "type") and
28
+ * the Node 22+ `--experimental-strip-types` runtime, which auto-detects ESM.
29
+ */
30
+ function resolveRepoRoot(): string {
31
+ const argv = process.argv.slice(2);
32
+ const idx = argv.indexOf('--repo-root');
33
+ if (idx !== -1 && idx + 1 < argv.length) {
34
+ const v = argv[idx + 1];
35
+ if (typeof v === 'string' && v.length > 0) return resolve(v);
36
+ }
37
+ return resolve(process.cwd());
38
+ }
39
+
40
+ const REPO_ROOT = resolveRepoRoot();
41
+ const SCHEMA_DIR = join(REPO_ROOT, 'reference', 'schemas');
42
+ const OUTPUT_PATH = join(SCHEMA_DIR, 'generated.d.ts');
43
+
44
+ const HEADER =
45
+ '// AUTO-GENERATED from reference/schemas/*.schema.json — DO NOT EDIT.\n' +
46
+ '// Regenerate: npm run codegen:schemas\n' +
47
+ '/* eslint-disable */\n';
48
+
49
+ /**
50
+ * Map a schema filename stem (e.g. "authority-snapshot" from
51
+ * "authority-snapshot.schema.json") to the canonical interface name per
52
+ * Plan 20-00: PascalCase + `Schema` suffix. Hyphens split word boundaries.
53
+ */
54
+ function stemToInterfaceName(stem: string): string {
55
+ const pascal = stem
56
+ .split(/[-_.]/)
57
+ .filter(Boolean)
58
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
59
+ .join('');
60
+ return `${pascal}Schema`;
61
+ }
62
+
63
+ async function main(): Promise<void> {
64
+ const entries = readdirSync(SCHEMA_DIR)
65
+ .filter((f) => f.endsWith('.schema.json'))
66
+ .sort();
67
+
68
+ if (entries.length === 0) {
69
+ console.error(`codegen-schema-types: no *.schema.json files found in ${SCHEMA_DIR}`);
70
+ process.exit(1);
71
+ }
72
+
73
+ const chunks: string[] = [HEADER];
74
+
75
+ for (const file of entries) {
76
+ const stem = basename(file, '.schema.json');
77
+ const interfaceName = stemToInterfaceName(stem);
78
+ const schemaPath = join(SCHEMA_DIR, file);
79
+
80
+ let schema: unknown;
81
+ try {
82
+ schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
83
+ } catch (err) {
84
+ const msg = err instanceof Error ? err.message : String(err);
85
+ console.error(`codegen-schema-types: failed to parse ${file}: ${msg}`);
86
+ process.exit(1);
87
+ }
88
+
89
+ try {
90
+ // compile() expects a JSONSchema; we pass our parsed object.
91
+ // bannerComment: '' — we add our own header once at the top.
92
+ const ts = await compile(schema as Parameters<typeof compile>[0], interfaceName, {
93
+ bannerComment: '',
94
+ additionalProperties: false,
95
+ style: { singleQuote: true, trailingComma: 'all' },
96
+ unreachableDefinitions: false,
97
+ });
98
+ chunks.push(`// ---- ${file} ----\n`);
99
+ // json-schema-to-typescript emits the top-level as the requested name,
100
+ // but when the schema's own `title` differs it may prefix. We rename
101
+ // the top-level export to our canonical name for stability.
102
+ const renamed = ensureExportInterface(ts, interfaceName);
103
+ chunks.push(renamed);
104
+ chunks.push('\n');
105
+ } catch (err) {
106
+ const msg = err instanceof Error ? err.message : String(err);
107
+ console.error(`codegen-schema-types: failed to compile ${file}: ${msg}`);
108
+ process.exit(1);
109
+ }
110
+ }
111
+
112
+ mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
113
+ writeFileSync(OUTPUT_PATH, chunks.join(''), 'utf8');
114
+ console.log(
115
+ `codegen-schema-types: wrote ${OUTPUT_PATH} (${entries.length} schema(s))`,
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Ensure that the compiled TS output exports an interface/type alias with the
121
+ * exact canonical name. `json-schema-to-typescript` normally emits this
122
+ * already, but some schemas whose `title` field contains non-identifier
123
+ * characters (e.g. ".design/config.json") get their interface named from a
124
+ * cleaned title rather than our requested name. We add an `export` alias at
125
+ * the end so every generated chunk guarantees `export interface XSchema` (or
126
+ * `export type XSchema = ...`) is available.
127
+ */
128
+ function ensureExportInterface(ts: string, canonical: string): string {
129
+ const hasCanonical = new RegExp(
130
+ `export\\s+(interface|type)\\s+${canonical}\\b`,
131
+ ).test(ts);
132
+ if (hasCanonical) return ts;
133
+
134
+ const firstExport = ts.match(
135
+ /export\s+(interface|type)\s+([A-Za-z_][A-Za-z0-9_]*)/,
136
+ );
137
+ if (!firstExport) {
138
+ return ts + `\nexport type ${canonical} = unknown;\n`;
139
+ }
140
+ const firstName = firstExport[2];
141
+ if (firstName === canonical) return ts;
142
+ return ts + `\nexport type ${canonical} = ${firstName};\n`;
143
+ }
144
+
145
+ main().catch((err) => {
146
+ const msg = err instanceof Error ? err.message : String(err);
147
+ console.error(`codegen-schema-types: ${msg}`);
148
+ process.exit(1);
149
+ });