@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.
- package/.claude-plugin/marketplace.json +11 -14
- package/.claude-plugin/plugin.json +9 -32
- package/CHANGELOG.md +138 -0
- package/README.md +54 -1
- package/agents/design-reflector.md +13 -0
- package/bin/gdd-sdk +55 -0
- package/connections/connections.md +3 -0
- package/connections/figma.md +2 -0
- package/connections/gdd-state.md +186 -0
- package/hooks/budget-enforcer.ts +716 -0
- package/hooks/context-exhaustion.ts +251 -0
- package/hooks/gdd-read-injection-scanner.ts +172 -0
- package/hooks/hooks.json +3 -3
- package/package.json +32 -51
- package/reference/codex-tools.md +53 -0
- package/reference/config-schema.md +2 -2
- package/reference/error-recovery.md +58 -0
- package/reference/gemini-tools.md +53 -0
- package/reference/registry.json +21 -0
- package/reference/schemas/budget.schema.json +42 -0
- package/reference/schemas/events.schema.json +55 -0
- package/reference/schemas/generated.d.ts +419 -0
- package/reference/schemas/iteration-budget.schema.json +36 -0
- package/reference/schemas/mcp-gdd-state-tools.schema.json +89 -0
- package/reference/schemas/rate-limits.schema.json +31 -0
- package/scripts/aggregate-agent-metrics.ts +282 -0
- package/scripts/codegen-schema-types.ts +149 -0
- package/scripts/e2e/run-headless.ts +514 -0
- package/scripts/lib/cli/commands/audit.ts +382 -0
- package/scripts/lib/cli/commands/init.ts +217 -0
- package/scripts/lib/cli/commands/query.ts +329 -0
- package/scripts/lib/cli/commands/run.ts +656 -0
- package/scripts/lib/cli/commands/stage.ts +468 -0
- package/scripts/lib/cli/index.ts +167 -0
- package/scripts/lib/cli/parse-args.ts +336 -0
- package/scripts/lib/context-engine/index.ts +116 -0
- package/scripts/lib/context-engine/manifest.ts +69 -0
- package/scripts/lib/context-engine/truncate.ts +282 -0
- package/scripts/lib/context-engine/types.ts +59 -0
- package/scripts/lib/discuss-parallel-runner/aggregator.ts +448 -0
- package/scripts/lib/discuss-parallel-runner/discussants.ts +430 -0
- package/scripts/lib/discuss-parallel-runner/index.ts +223 -0
- package/scripts/lib/discuss-parallel-runner/types.ts +184 -0
- package/scripts/lib/error-classifier.cjs +232 -0
- package/scripts/lib/error-classifier.d.cts +44 -0
- package/scripts/lib/event-stream/emitter.ts +88 -0
- package/scripts/lib/event-stream/index.ts +164 -0
- package/scripts/lib/event-stream/types.ts +127 -0
- package/scripts/lib/event-stream/writer.ts +154 -0
- package/scripts/lib/explore-parallel-runner/index.ts +294 -0
- package/scripts/lib/explore-parallel-runner/mappers.ts +290 -0
- package/scripts/lib/explore-parallel-runner/synthesizer.ts +295 -0
- package/scripts/lib/explore-parallel-runner/types.ts +139 -0
- package/scripts/lib/gdd-errors/classification.ts +124 -0
- package/scripts/lib/gdd-errors/index.ts +218 -0
- package/scripts/lib/gdd-state/gates.ts +216 -0
- package/scripts/lib/gdd-state/index.ts +167 -0
- package/scripts/lib/gdd-state/lockfile.ts +232 -0
- package/scripts/lib/gdd-state/mutator.ts +574 -0
- package/scripts/lib/gdd-state/parser.ts +523 -0
- package/scripts/lib/gdd-state/types.ts +179 -0
- package/scripts/lib/harness/detect.ts +90 -0
- package/scripts/lib/harness/index.ts +64 -0
- package/scripts/lib/harness/tool-map.ts +142 -0
- package/scripts/lib/init-runner/index.ts +396 -0
- package/scripts/lib/init-runner/researchers.ts +245 -0
- package/scripts/lib/init-runner/scaffold.ts +224 -0
- package/scripts/lib/init-runner/synthesizer.ts +224 -0
- package/scripts/lib/init-runner/types.ts +143 -0
- package/scripts/lib/iteration-budget.cjs +205 -0
- package/scripts/lib/iteration-budget.d.cts +32 -0
- package/scripts/lib/jittered-backoff.cjs +112 -0
- package/scripts/lib/jittered-backoff.d.cts +38 -0
- package/scripts/lib/lockfile.cjs +177 -0
- package/scripts/lib/lockfile.d.cts +21 -0
- package/scripts/lib/logger/index.ts +251 -0
- package/scripts/lib/logger/sinks.ts +269 -0
- package/scripts/lib/logger/types.ts +110 -0
- package/scripts/lib/pipeline-runner/human-gate.ts +134 -0
- package/scripts/lib/pipeline-runner/index.ts +527 -0
- package/scripts/lib/pipeline-runner/stage-handlers.ts +339 -0
- package/scripts/lib/pipeline-runner/state-machine.ts +144 -0
- package/scripts/lib/pipeline-runner/types.ts +183 -0
- package/scripts/lib/prompt-sanitizer/index.ts +435 -0
- package/scripts/lib/prompt-sanitizer/patterns.ts +173 -0
- package/scripts/lib/rate-guard.cjs +365 -0
- package/scripts/lib/rate-guard.d.cts +38 -0
- package/scripts/lib/session-runner/errors.ts +406 -0
- package/scripts/lib/session-runner/index.ts +715 -0
- package/scripts/lib/session-runner/transcript.ts +189 -0
- package/scripts/lib/session-runner/types.ts +144 -0
- package/scripts/lib/tool-scoping/index.ts +219 -0
- package/scripts/lib/tool-scoping/parse-agent-tools.ts +207 -0
- package/scripts/lib/tool-scoping/stage-scopes.ts +139 -0
- package/scripts/lib/tool-scoping/types.ts +77 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_blocker.schema.json +67 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_decision.schema.json +68 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_must_have.schema.json +68 -0
- package/scripts/mcp-servers/gdd-state/schemas/checkpoint.schema.json +51 -0
- package/scripts/mcp-servers/gdd-state/schemas/frontmatter_update.schema.json +62 -0
- package/scripts/mcp-servers/gdd-state/schemas/get.schema.json +51 -0
- package/scripts/mcp-servers/gdd-state/schemas/probe_connections.schema.json +75 -0
- package/scripts/mcp-servers/gdd-state/schemas/resolve_blocker.schema.json +66 -0
- package/scripts/mcp-servers/gdd-state/schemas/set_status.schema.json +47 -0
- package/scripts/mcp-servers/gdd-state/schemas/transition_stage.schema.json +70 -0
- package/scripts/mcp-servers/gdd-state/schemas/update_progress.schema.json +58 -0
- package/scripts/mcp-servers/gdd-state/server.ts +288 -0
- package/scripts/mcp-servers/gdd-state/tools/add_blocker.ts +72 -0
- package/scripts/mcp-servers/gdd-state/tools/add_decision.ts +89 -0
- package/scripts/mcp-servers/gdd-state/tools/add_must_have.ts +113 -0
- package/scripts/mcp-servers/gdd-state/tools/checkpoint.ts +60 -0
- package/scripts/mcp-servers/gdd-state/tools/frontmatter_update.ts +91 -0
- package/scripts/mcp-servers/gdd-state/tools/get.ts +51 -0
- package/scripts/mcp-servers/gdd-state/tools/index.ts +51 -0
- package/scripts/mcp-servers/gdd-state/tools/probe_connections.ts +73 -0
- package/scripts/mcp-servers/gdd-state/tools/resolve_blocker.ts +84 -0
- package/scripts/mcp-servers/gdd-state/tools/set_status.ts +54 -0
- package/scripts/mcp-servers/gdd-state/tools/shared.ts +194 -0
- package/scripts/mcp-servers/gdd-state/tools/transition_stage.ts +80 -0
- package/scripts/mcp-servers/gdd-state/tools/update_progress.ts +81 -0
- package/scripts/validate-frontmatter.ts +114 -0
- package/scripts/validate-schemas.ts +401 -0
- package/skills/brief/SKILL.md +15 -6
- package/skills/design/SKILL.md +31 -13
- package/skills/explore/SKILL.md +41 -17
- package/skills/health/SKILL.md +15 -4
- package/skills/optimize/SKILL.md +3 -3
- package/skills/pause/SKILL.md +16 -10
- package/skills/plan/SKILL.md +33 -17
- package/skills/progress/SKILL.md +15 -11
- package/skills/resume/SKILL.md +19 -10
- package/skills/settings/SKILL.md +11 -3
- package/skills/todo/SKILL.md +12 -3
- package/skills/verify/SKILL.md +65 -29
- package/hooks/budget-enforcer.js +0 -329
- package/hooks/context-exhaustion.js +0 -127
- package/hooks/gdd-read-injection-scanner.js +0 -39
- package/scripts/aggregate-agent-metrics.js +0 -173
- package/scripts/validate-frontmatter.cjs +0 -68
- 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
|
+
});
|