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