@hegemonart/get-design-done 1.0.7
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 +63 -0
- package/.claude-plugin/plugin.json +54 -0
- package/CHANGELOG.md +221 -0
- package/LICENSE +21 -0
- package/README.md +724 -0
- package/SKILL.md +232 -0
- package/agents/README.md +226 -0
- package/agents/a11y-mapper.md +118 -0
- package/agents/component-taxonomy-mapper.md +88 -0
- package/agents/design-advisor.md +139 -0
- package/agents/design-assumptions-analyzer.md +171 -0
- package/agents/design-auditor.md +383 -0
- package/agents/design-context-builder.md +544 -0
- package/agents/design-context-checker-gate.md +90 -0
- package/agents/design-context-checker.md +260 -0
- package/agents/design-discussant.md +98 -0
- package/agents/design-doc-writer.md +229 -0
- package/agents/design-executor.md +452 -0
- package/agents/design-figma-writer.md +302 -0
- package/agents/design-fixer.md +180 -0
- package/agents/design-integration-checker-gate.md +93 -0
- package/agents/design-integration-checker.md +326 -0
- package/agents/design-pattern-mapper.md +206 -0
- package/agents/design-phase-researcher.md +229 -0
- package/agents/design-plan-checker.md +164 -0
- package/agents/design-planner.md +352 -0
- package/agents/design-reflector.md +175 -0
- package/agents/design-research-synthesizer.md +127 -0
- package/agents/design-verifier-gate.md +97 -0
- package/agents/design-verifier.md +605 -0
- package/agents/gdd-graphify-sync.md +100 -0
- package/agents/gdd-intel-updater.md +88 -0
- package/agents/gdd-learnings-extractor.md +85 -0
- package/agents/motion-mapper.md +103 -0
- package/agents/token-mapper.md +103 -0
- package/agents/visual-hierarchy-mapper.md +95 -0
- package/connections/chromatic.md +247 -0
- package/connections/claude-design.md +190 -0
- package/connections/connections.md +218 -0
- package/connections/figma-writer.md +139 -0
- package/connections/figma.md +146 -0
- package/connections/graphify.md +197 -0
- package/connections/pinterest.md +153 -0
- package/connections/preview.md +173 -0
- package/connections/refero.md +189 -0
- package/connections/storybook.md +280 -0
- package/hooks/budget-enforcer.js +318 -0
- package/hooks/context-exhaustion.js +127 -0
- package/hooks/gdd-read-injection-scanner.js +44 -0
- package/hooks/hooks.json +44 -0
- package/package.json +60 -0
- package/reference/BRANCH-PROTECTION.md +65 -0
- package/reference/DEPRECATIONS.md +41 -0
- package/reference/STATE-TEMPLATE.md +200 -0
- package/reference/accessibility.md +190 -0
- package/reference/anti-patterns.md +336 -0
- package/reference/audit-scoring.md +205 -0
- package/reference/checklists.md +137 -0
- package/reference/config-schema.md +319 -0
- package/reference/debugger-philosophy.md +32 -0
- package/reference/heuristics.md +201 -0
- package/reference/intel-schema.md +266 -0
- package/reference/model-prices.md +37 -0
- package/reference/model-tiers.md +118 -0
- package/reference/motion.md +285 -0
- package/reference/parallelism-rules.md +108 -0
- package/reference/priority-matrix.md +31 -0
- package/reference/project-skills-guide.md +42 -0
- package/reference/review-format.md +107 -0
- package/reference/schemas/config.schema.json +41 -0
- package/reference/schemas/hooks.schema.json +55 -0
- package/reference/schemas/intel.schema.json +191 -0
- package/reference/schemas/marketplace.schema.json +72 -0
- package/reference/schemas/plugin.schema.json +59 -0
- package/reference/shared-preamble.md +82 -0
- package/reference/typography.md +229 -0
- package/scripts/aggregate-agent-metrics.js +144 -0
- package/scripts/apply-branch-protection.sh +75 -0
- package/scripts/bootstrap-manifest.txt +3 -0
- package/scripts/bootstrap.sh +80 -0
- package/scripts/build-intel.cjs +458 -0
- package/scripts/detect-stale-refs.cjs +101 -0
- package/scripts/extract-changelog-section.cjs +57 -0
- package/scripts/release-smoke-test.cjs +169 -0
- package/scripts/rollback-release.sh +42 -0
- package/scripts/run-injection-scanner-ci.cjs +92 -0
- package/scripts/validate-frontmatter.cjs +68 -0
- package/scripts/validate-schemas.cjs +225 -0
- package/scripts/verify-version-sync.cjs +30 -0
- package/skills/add-backlog/SKILL.md +47 -0
- package/skills/analyze-dependencies/SKILL.md +184 -0
- package/skills/apply-reflections/SKILL.md +112 -0
- package/skills/audit/SKILL.md +54 -0
- package/skills/brief/SKILL.md +75 -0
- package/skills/cache-manager/SKILL.md +120 -0
- package/skills/compare/SKILL.md +322 -0
- package/skills/complete-cycle/SKILL.md +33 -0
- package/skills/darkmode/SKILL.md +331 -0
- package/skills/debug/SKILL.md +38 -0
- package/skills/design/SKILL.md +281 -0
- package/skills/discover/SKILL.md +172 -0
- package/skills/discuss/SKILL.md +67 -0
- package/skills/do/SKILL.md +45 -0
- package/skills/explore/SKILL.md +109 -0
- package/skills/extract-learnings/SKILL.md +98 -0
- package/skills/fast/SKILL.md +44 -0
- package/skills/figma-write/SKILL.md +40 -0
- package/skills/graphify/SKILL.md +48 -0
- package/skills/health/SKILL.md +48 -0
- package/skills/help/SKILL.md +76 -0
- package/skills/list-assumptions/SKILL.md +60 -0
- package/skills/map/SKILL.md +112 -0
- package/skills/new-cycle/SKILL.md +35 -0
- package/skills/new-project/SKILL.md +53 -0
- package/skills/next/SKILL.md +42 -0
- package/skills/note/SKILL.md +47 -0
- package/skills/optimize/SKILL.md +120 -0
- package/skills/pause/SKILL.md +41 -0
- package/skills/plan/SKILL.md +251 -0
- package/skills/plant-seed/SKILL.md +47 -0
- package/skills/pr-branch/SKILL.md +31 -0
- package/skills/progress/SKILL.md +60 -0
- package/skills/quick/SKILL.md +43 -0
- package/skills/reapply-patches/SKILL.md +31 -0
- package/skills/reflect/SKILL.md +73 -0
- package/skills/resume/SKILL.md +37 -0
- package/skills/review-backlog/SKILL.md +45 -0
- package/skills/router/SKILL.md +67 -0
- package/skills/scan/SKILL.md +721 -0
- package/skills/settings/SKILL.md +78 -0
- package/skills/ship/SKILL.md +31 -0
- package/skills/sketch/SKILL.md +78 -0
- package/skills/sketch-wrap-up/SKILL.md +88 -0
- package/skills/skill-manifest/SKILL.md +79 -0
- package/skills/spike/SKILL.md +67 -0
- package/skills/spike-wrap-up/SKILL.md +81 -0
- package/skills/stats/SKILL.md +50 -0
- package/skills/style/SKILL.md +193 -0
- package/skills/synthesize/SKILL.md +93 -0
- package/skills/todo/SKILL.md +54 -0
- package/skills/undo/SKILL.md +30 -0
- package/skills/update/SKILL.md +36 -0
- package/skills/verify/SKILL.md +452 -0
- package/skills/warm-cache/SKILL.md +113 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* budget-enforcer.js — PreToolUse hook (matcher: Agent)
|
|
4
|
+
*
|
|
5
|
+
* Intercepts every Agent tool spawn. Consults:
|
|
6
|
+
* (a) router decision (from tool_input.context.router_decision if supplied)
|
|
7
|
+
* (b) .design/cache-manifest.json for short-circuit cached answers (D-05)
|
|
8
|
+
* (c) .design/budget.json for tier_overrides + caps (D-01, D-04, D-10)
|
|
9
|
+
*
|
|
10
|
+
* Enforcement (D-02, D-03, D-11):
|
|
11
|
+
* - enforcement_mode: "enforce" + 100% cap → block with actionable error
|
|
12
|
+
* - enforcement_mode: "enforce" + 80% soft-threshold + auto_downgrade_on_cap → rewrite tier to haiku
|
|
13
|
+
* - enforcement_mode: "warn" → log warning, allow spawn
|
|
14
|
+
* - enforcement_mode: "log" → advisory only
|
|
15
|
+
*
|
|
16
|
+
* Logs every decision to .design/telemetry/costs.jsonl (OPT-09 schema).
|
|
17
|
+
* Every telemetry write fires a detached child aggregator (scripts/aggregate-agent-metrics.js)
|
|
18
|
+
* that rebuilds .design/agent-metrics.json incrementally.
|
|
19
|
+
*
|
|
20
|
+
* Hook type: PreToolUse
|
|
21
|
+
* Input: JSON on stdin { tool_name, tool_input }
|
|
22
|
+
* Output: JSON on stdout { continue, suppressOutput, message, modified_tool_input? }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const readline = require('readline');
|
|
30
|
+
const { spawn } = require('child_process');
|
|
31
|
+
|
|
32
|
+
const BUDGET_PATH = path.join(process.cwd(), '.design', 'budget.json');
|
|
33
|
+
const MANIFEST_PATH = path.join(process.cwd(), '.design', 'cache-manifest.json');
|
|
34
|
+
const TELEMETRY_PATH = path.join(process.cwd(), '.design', 'telemetry', 'costs.jsonl');
|
|
35
|
+
const STATE_PATH = path.join(process.cwd(), '.design', 'STATE.md');
|
|
36
|
+
|
|
37
|
+
// ---- budget.json loader with defaults per D-12 ----
|
|
38
|
+
function loadBudget() {
|
|
39
|
+
const defaults = {
|
|
40
|
+
per_task_cap_usd: 2.00,
|
|
41
|
+
per_phase_cap_usd: 20.00,
|
|
42
|
+
tier_overrides: {},
|
|
43
|
+
auto_downgrade_on_cap: true,
|
|
44
|
+
cache_ttl_seconds: 3600,
|
|
45
|
+
enforcement_mode: 'enforce'
|
|
46
|
+
};
|
|
47
|
+
if (!fs.existsSync(BUDGET_PATH)) return defaults;
|
|
48
|
+
try { return { ...defaults, ...JSON.parse(fs.readFileSync(BUDGET_PATH, 'utf8')) }; }
|
|
49
|
+
catch { return defaults; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---- cumulative phase spend from telemetry (D-02 per_phase_cap_usd check) ----
|
|
53
|
+
function currentPhaseSpend(phase) {
|
|
54
|
+
if (!fs.existsSync(TELEMETRY_PATH)) return 0;
|
|
55
|
+
const lines = fs.readFileSync(TELEMETRY_PATH, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
56
|
+
let sum = 0;
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
try {
|
|
59
|
+
const row = JSON.parse(line);
|
|
60
|
+
if (row.phase === phase) sum += Number(row.est_cost_usd || 0);
|
|
61
|
+
} catch { /* tolerant */ }
|
|
62
|
+
}
|
|
63
|
+
return sum;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- cycle + phase reader (STATE.md frontmatter) ----
|
|
67
|
+
function readCycleAndPhase() {
|
|
68
|
+
const defaults = { cycle: 'unknown', phase: 'unknown' };
|
|
69
|
+
if (!fs.existsSync(STATE_PATH)) return defaults;
|
|
70
|
+
try {
|
|
71
|
+
const content = fs.readFileSync(STATE_PATH, 'utf8');
|
|
72
|
+
// Match the first frontmatter block: between opening '---' and next '---'
|
|
73
|
+
const fm = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
74
|
+
const body = fm ? fm[1] : content;
|
|
75
|
+
const cycleMatch = body.match(/^cycle:\s*"?([^"\n]+)"?/m);
|
|
76
|
+
const phaseMatch = body.match(/^phase:\s*"?([^"\n]+)"?/m);
|
|
77
|
+
return {
|
|
78
|
+
cycle: cycleMatch ? cycleMatch[1].trim() : 'unknown',
|
|
79
|
+
phase: phaseMatch ? phaseMatch[1].trim() : 'unknown',
|
|
80
|
+
};
|
|
81
|
+
} catch {
|
|
82
|
+
return defaults;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Deprecated alias for plan 01 callers (and any other hook/script that imports this).
|
|
87
|
+
// Returns only the phase string; prefer readCycleAndPhase() for new code.
|
|
88
|
+
function currentPhase() {
|
|
89
|
+
return readCycleAndPhase().phase;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- cache short-circuit (D-05) ----
|
|
93
|
+
function cacheLookup(agent, inputHash) {
|
|
94
|
+
if (!fs.existsSync(MANIFEST_PATH)) return null;
|
|
95
|
+
try {
|
|
96
|
+
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
|
97
|
+
const entry = manifest.entries?.[`${agent}:${inputHash}`];
|
|
98
|
+
if (!entry) return null;
|
|
99
|
+
const age = Date.now() / 1000 - entry.ts_unix;
|
|
100
|
+
if (age > (manifest.ttl_seconds || 3600)) return null;
|
|
101
|
+
return entry.result; // cached blob
|
|
102
|
+
} catch { return null; }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- tier resolution (D-04) ----
|
|
106
|
+
function resolveTier(agent, agentDefaultTier, overrides) {
|
|
107
|
+
return overrides?.[agent] || agentDefaultTier || 'sonnet';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- detached aggregator invocation ----
|
|
111
|
+
// Fire-and-forget: do not block the hook. The aggregator reads costs.jsonl tail
|
|
112
|
+
// and rewrites .design/agent-metrics.json atomically.
|
|
113
|
+
function spawnAggregator() {
|
|
114
|
+
try {
|
|
115
|
+
const aggregatorPath = path.join(process.cwd(), 'scripts', 'aggregate-agent-metrics.js');
|
|
116
|
+
if (!fs.existsSync(aggregatorPath)) return; // script not installed — fail open
|
|
117
|
+
const child = spawn('node', [aggregatorPath], {
|
|
118
|
+
cwd: process.cwd(),
|
|
119
|
+
detached: true,
|
|
120
|
+
stdio: 'ignore',
|
|
121
|
+
env: process.env,
|
|
122
|
+
});
|
|
123
|
+
child.unref();
|
|
124
|
+
} catch {
|
|
125
|
+
// Aggregator failures are non-fatal to the hook.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- locked-schema row builder (OPT-09) ----
|
|
130
|
+
function buildTelemetryRow(partial) {
|
|
131
|
+
const { cycle, phase } = partial._cyclePhase || readCycleAndPhase();
|
|
132
|
+
// The nine mandatory fields per OPT-09, always in this order.
|
|
133
|
+
const row = {
|
|
134
|
+
ts: partial.ts || new Date().toISOString(),
|
|
135
|
+
agent: String(partial.agent || 'unknown'),
|
|
136
|
+
tier: String(partial.tier || 'unknown'),
|
|
137
|
+
tokens_in: Number(partial.tokens_in || 0),
|
|
138
|
+
tokens_out: Number(partial.tokens_out || 0),
|
|
139
|
+
cache_hit: Boolean(partial.cache_hit),
|
|
140
|
+
est_cost_usd: Number(partial.est_cost_usd || 0),
|
|
141
|
+
cycle: partial.cycle || cycle,
|
|
142
|
+
phase: partial.phase || phase,
|
|
143
|
+
};
|
|
144
|
+
// Optional diagnostic fields (Phase 11 reflector ignores unknown fields gracefully).
|
|
145
|
+
if (partial.tier_downgraded !== undefined) row.tier_downgraded = Boolean(partial.tier_downgraded);
|
|
146
|
+
if (partial.enforcement_mode !== undefined) row.enforcement_mode = String(partial.enforcement_mode);
|
|
147
|
+
if (partial.lazy_skipped !== undefined) row.lazy_skipped = Boolean(partial.lazy_skipped);
|
|
148
|
+
if (partial.block_reason !== undefined) row.block_reason = String(partial.block_reason);
|
|
149
|
+
return row;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- telemetry writer: append one JSON row to costs.jsonl ----
|
|
153
|
+
function writeTelemetry(partial) {
|
|
154
|
+
const dir = path.dirname(TELEMETRY_PATH);
|
|
155
|
+
try {
|
|
156
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
157
|
+
const row = buildTelemetryRow(partial);
|
|
158
|
+
fs.appendFileSync(TELEMETRY_PATH, JSON.stringify(row) + '\n', 'utf8');
|
|
159
|
+
// Fire-and-forget aggregator — rebuilds .design/agent-metrics.json incrementally.
|
|
160
|
+
spawnAggregator();
|
|
161
|
+
} catch {
|
|
162
|
+
// Fail open: telemetry must never block the hook.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Backward-compat alias (plan 01 called it appendTelemetry; keep it working).
|
|
167
|
+
const appendTelemetry = writeTelemetry;
|
|
168
|
+
|
|
169
|
+
// ---- main ----
|
|
170
|
+
async function main() {
|
|
171
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
172
|
+
let inputData = '';
|
|
173
|
+
for await (const line of rl) inputData += line + '\n';
|
|
174
|
+
|
|
175
|
+
let parsed;
|
|
176
|
+
try { parsed = JSON.parse(inputData); } catch { process.exit(0); }
|
|
177
|
+
|
|
178
|
+
if (parsed.tool_name !== 'Agent') process.exit(0); // only guard Agent spawns
|
|
179
|
+
|
|
180
|
+
const toolInput = parsed.tool_input || {};
|
|
181
|
+
const agent = toolInput.subagent_type || toolInput.agent || 'unknown';
|
|
182
|
+
const inputHash = toolInput._input_hash || null; // supplied by orchestrator if cache-manager pre-computed it
|
|
183
|
+
|
|
184
|
+
// Resolve cycle + phase once so every branch can stamp consistent values.
|
|
185
|
+
const { cycle, phase } = readCycleAndPhase();
|
|
186
|
+
const cyclePhase = { cycle, phase };
|
|
187
|
+
|
|
188
|
+
// Branch A: lazy-gate signal from plan 10.1-04 agents (design-verifier-gate, etc.).
|
|
189
|
+
// Gate agents set tool_input.lazy_skipped === true when the heuristic declines
|
|
190
|
+
// to spawn the full checker. We log a zero-cost row and pass through.
|
|
191
|
+
if (toolInput.lazy_skipped === true) {
|
|
192
|
+
writeTelemetry({
|
|
193
|
+
agent,
|
|
194
|
+
tier: 'gate',
|
|
195
|
+
tokens_in: 0,
|
|
196
|
+
tokens_out: 0,
|
|
197
|
+
cache_hit: false,
|
|
198
|
+
est_cost_usd: 0,
|
|
199
|
+
lazy_skipped: true,
|
|
200
|
+
_cyclePhase: cyclePhase,
|
|
201
|
+
});
|
|
202
|
+
const response = { continue: true, suppressOutput: true };
|
|
203
|
+
process.stdout.write(JSON.stringify(response));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const budget = loadBudget();
|
|
208
|
+
|
|
209
|
+
// Branch B: cache short-circuit (D-05)
|
|
210
|
+
if (inputHash) {
|
|
211
|
+
const cached = cacheLookup(agent, inputHash);
|
|
212
|
+
if (cached !== null) {
|
|
213
|
+
writeTelemetry({
|
|
214
|
+
agent,
|
|
215
|
+
tier: 'cache',
|
|
216
|
+
tokens_in: 0,
|
|
217
|
+
tokens_out: 0,
|
|
218
|
+
cache_hit: true,
|
|
219
|
+
est_cost_usd: 0,
|
|
220
|
+
_cyclePhase: cyclePhase,
|
|
221
|
+
});
|
|
222
|
+
const response = {
|
|
223
|
+
continue: false, // block the real spawn; orchestrator reads suppressOutput.message for cached blob
|
|
224
|
+
suppressOutput: false,
|
|
225
|
+
message: `gdd-budget-enforcer: SkippedCached — returning cached result for ${agent}:${inputHash}`,
|
|
226
|
+
cached_result: cached,
|
|
227
|
+
};
|
|
228
|
+
process.stdout.write(JSON.stringify(response));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Layer B: cap checks (D-02)
|
|
234
|
+
const estCost = Number(toolInput._est_cost_usd || 0);
|
|
235
|
+
const phaseSpend = currentPhaseSpend(phase);
|
|
236
|
+
|
|
237
|
+
if (budget.enforcement_mode === 'enforce') {
|
|
238
|
+
// Branch C: 100% per_task cap (hard block)
|
|
239
|
+
if (estCost >= budget.per_task_cap_usd) {
|
|
240
|
+
writeTelemetry({
|
|
241
|
+
agent,
|
|
242
|
+
tier: toolInput._tier_override || toolInput._default_tier || 'sonnet',
|
|
243
|
+
tokens_in: Number(toolInput._tokens_in_est || 0),
|
|
244
|
+
tokens_out: Number(toolInput._tokens_out_est || 0),
|
|
245
|
+
cache_hit: false,
|
|
246
|
+
est_cost_usd: estCost,
|
|
247
|
+
enforcement_mode: budget.enforcement_mode,
|
|
248
|
+
block_reason: 'per_task_cap',
|
|
249
|
+
_cyclePhase: cyclePhase,
|
|
250
|
+
});
|
|
251
|
+
const response = {
|
|
252
|
+
continue: false,
|
|
253
|
+
suppressOutput: false,
|
|
254
|
+
message: `Budget cap reached for per-task. Estimated: $${estCost.toFixed(4)}, cap: $${budget.per_task_cap_usd.toFixed(2)}. Raise cap in .design/budget.json or retry after next task.`,
|
|
255
|
+
};
|
|
256
|
+
process.stdout.write(JSON.stringify(response));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Branch D: 100% per_phase cap (hard block)
|
|
260
|
+
if (phaseSpend + estCost >= budget.per_phase_cap_usd) {
|
|
261
|
+
writeTelemetry({
|
|
262
|
+
agent,
|
|
263
|
+
tier: toolInput._tier_override || toolInput._default_tier || 'sonnet',
|
|
264
|
+
tokens_in: Number(toolInput._tokens_in_est || 0),
|
|
265
|
+
tokens_out: Number(toolInput._tokens_out_est || 0),
|
|
266
|
+
cache_hit: false,
|
|
267
|
+
est_cost_usd: estCost,
|
|
268
|
+
enforcement_mode: budget.enforcement_mode,
|
|
269
|
+
block_reason: 'per_phase_cap',
|
|
270
|
+
_cyclePhase: cyclePhase,
|
|
271
|
+
});
|
|
272
|
+
const response = {
|
|
273
|
+
continue: false,
|
|
274
|
+
suppressOutput: false,
|
|
275
|
+
message: `Budget cap reached for per-phase (${phase}). Cumulative: $${(phaseSpend + estCost).toFixed(4)}, cap: $${budget.per_phase_cap_usd.toFixed(2)}. Raise cap in .design/budget.json or retry after next phase.`,
|
|
276
|
+
};
|
|
277
|
+
process.stdout.write(JSON.stringify(response));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// 80% soft-threshold downgrade (D-03)
|
|
281
|
+
if (budget.auto_downgrade_on_cap && (phaseSpend + estCost) >= (0.80 * budget.per_task_cap_usd)) {
|
|
282
|
+
toolInput._tier_override = 'haiku';
|
|
283
|
+
toolInput._tier_downgraded = true;
|
|
284
|
+
}
|
|
285
|
+
} else if (budget.enforcement_mode === 'warn') {
|
|
286
|
+
if (estCost >= budget.per_task_cap_usd) {
|
|
287
|
+
process.stderr.write(`gdd-budget-enforcer WARN: per-task cap will be exceeded ($${estCost.toFixed(4)} >= $${budget.per_task_cap_usd})\n`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// enforcement_mode === 'log': no blocking, just telemetry
|
|
291
|
+
|
|
292
|
+
// D-04: tier_overrides rewrite
|
|
293
|
+
if (budget.tier_overrides[agent]) {
|
|
294
|
+
toolInput._tier_override = budget.tier_overrides[agent];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Branch E: standard spawn-allowed (includes tier-downgraded path D-03)
|
|
298
|
+
writeTelemetry({
|
|
299
|
+
agent,
|
|
300
|
+
tier: toolInput._tier_override || toolInput._default_tier || 'sonnet',
|
|
301
|
+
tokens_in: Number(toolInput._tokens_in_est || 0),
|
|
302
|
+
tokens_out: Number(toolInput._tokens_out_est || 0),
|
|
303
|
+
cache_hit: false,
|
|
304
|
+
est_cost_usd: estCost,
|
|
305
|
+
tier_downgraded: !!toolInput._tier_downgraded,
|
|
306
|
+
enforcement_mode: budget.enforcement_mode,
|
|
307
|
+
_cyclePhase: cyclePhase,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const response = {
|
|
311
|
+
continue: true,
|
|
312
|
+
suppressOutput: true,
|
|
313
|
+
modified_tool_input: toolInput
|
|
314
|
+
};
|
|
315
|
+
process.stdout.write(JSON.stringify(response));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
main().catch(err => { console.error('budget-enforcer hook error:', err); process.exit(0); });
|
|
@@ -0,0 +1,127 @@
|
|
|
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); });
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
|
|
10
|
+
const INJECTION_PATTERNS = [
|
|
11
|
+
/ignore\s+(all\s+)?(previous|prior|above)\s+instructions?/i,
|
|
12
|
+
/disregard\s+(all\s+)?(previous|prior|above)\s+instructions?/i,
|
|
13
|
+
/you\s+are\s+now\s+a\s+different/i,
|
|
14
|
+
/system\s*:\s*you\s+are/i,
|
|
15
|
+
/<\s*\/?\s*(system|assistant|human)\s*>/i,
|
|
16
|
+
/\[INST\]/i,
|
|
17
|
+
/###\s*instruction/i,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
22
|
+
let inputData = '';
|
|
23
|
+
for await (const line of rl) inputData += line + '\n';
|
|
24
|
+
|
|
25
|
+
let parsed;
|
|
26
|
+
try { parsed = JSON.parse(inputData); } catch { process.exit(0); }
|
|
27
|
+
|
|
28
|
+
if (parsed?.tool_name !== 'Read') process.exit(0);
|
|
29
|
+
|
|
30
|
+
const content = parsed?.tool_response?.content || '';
|
|
31
|
+
const matched = INJECTION_PATTERNS.some(p => p.test(content));
|
|
32
|
+
if (!matched) process.exit(0);
|
|
33
|
+
|
|
34
|
+
const file = parsed?.tool_input?.file_path || 'unknown';
|
|
35
|
+
const response = {
|
|
36
|
+
continue: true,
|
|
37
|
+
suppressOutput: false,
|
|
38
|
+
message: `gdd-injection-scanner: Suspicious prompt-injection pattern detected in content read from "${file}". Review before acting on instructions contained in that file.`,
|
|
39
|
+
};
|
|
40
|
+
process.stdout.write(JSON.stringify(response));
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
main().catch(() => process.exit(0));
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.sh\""
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"PreToolUse": [
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Agent",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/budget-enforcer.js\""
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"PostToolUse": [
|
|
25
|
+
{
|
|
26
|
+
"matcher": "Read",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-read-injection-scanner.js\""
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"hooks": [
|
|
36
|
+
{
|
|
37
|
+
"type": "command",
|
|
38
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/context-exhaustion.js\""
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hegemonart/get-design-done",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "A Claude Code plugin for systematic design improvement",
|
|
5
|
+
"author": "Hegemon",
|
|
6
|
+
"homepage": "https://github.com/hegemonart/get-design-done",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/hegemonart/get-design-done.git"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
".claude-plugin/",
|
|
17
|
+
"agents/",
|
|
18
|
+
"skills/",
|
|
19
|
+
"hooks/",
|
|
20
|
+
"scripts/",
|
|
21
|
+
"connections/",
|
|
22
|
+
"reference/",
|
|
23
|
+
"SKILL.md",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"provenance": true
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node --test \"tests/**/*.cjs\"",
|
|
34
|
+
"lint:md": "npx --yes markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.planning\" \"#.claude\" \"#test-fixture/baselines\"",
|
|
35
|
+
"lint:links": "npx --yes lychee --no-progress --accept 200,206,403,429 \"**/*.md\" || true",
|
|
36
|
+
"validate:schemas": "node scripts/validate-schemas.cjs",
|
|
37
|
+
"validate:frontmatter": "node scripts/validate-frontmatter.cjs agents/",
|
|
38
|
+
"detect:stale-refs": "node scripts/detect-stale-refs.cjs",
|
|
39
|
+
"scan:injection": "node scripts/run-injection-scanner-ci.cjs",
|
|
40
|
+
"test:size-budget": "node --test tests/agent-size-budget.test.cjs",
|
|
41
|
+
"release:extract-changelog": "node scripts/extract-changelog-section.cjs",
|
|
42
|
+
"verify:version-sync": "node scripts/verify-version-sync.cjs"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"claude",
|
|
46
|
+
"claude-code",
|
|
47
|
+
"claude-code-plugin",
|
|
48
|
+
"plugin",
|
|
49
|
+
"design",
|
|
50
|
+
"design-system",
|
|
51
|
+
"self-improvement",
|
|
52
|
+
"reflection",
|
|
53
|
+
"tested",
|
|
54
|
+
"ci"
|
|
55
|
+
],
|
|
56
|
+
"skills": [
|
|
57
|
+
"SKILL.md"
|
|
58
|
+
],
|
|
59
|
+
"hooks": "hooks/hooks.json"
|
|
60
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Branch Protection — Two-Phase Rollout
|
|
2
|
+
|
|
3
|
+
Per D-16 / D-17: branch protection on `main` is rolled out in two phases. The
|
|
4
|
+
repo admin applies each phase manually via `scripts/apply-branch-protection.sh`
|
|
5
|
+
(no CI automation — avoids leaking repo admin credentials).
|
|
6
|
+
|
|
7
|
+
## Phase A — Advisory (initial state)
|
|
8
|
+
|
|
9
|
+
Status checks **run** on every push, but they are **not required to merge**.
|
|
10
|
+
This is the default posture while CI stabilizes and baselines are established.
|
|
11
|
+
|
|
12
|
+
Apply:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bash scripts/apply-branch-protection.sh --advisory
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Effects:
|
|
19
|
+
|
|
20
|
+
- Required status checks: **none** (checks run but don't block)
|
|
21
|
+
- `main` accepts direct pushes (the solo maintainer's existing workflow)
|
|
22
|
+
- Force-push: allowed by admins
|
|
23
|
+
|
|
24
|
+
## Phase B — Enforcing (after first clean release)
|
|
25
|
+
|
|
26
|
+
After the first full release cycle ships clean (plan 13-06 / 13-07 smoke test
|
|
27
|
+
passes on a real tag + GitHub Release), tighten to enforcing.
|
|
28
|
+
|
|
29
|
+
Apply:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bash scripts/apply-branch-protection.sh --enforcing
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Effects:
|
|
36
|
+
|
|
37
|
+
- Required status checks (all must pass to merge):
|
|
38
|
+
- `Lint (markdown + frontmatter + stale-refs)`
|
|
39
|
+
- `Validate (schemas + plugin + shellcheck)`
|
|
40
|
+
- `Test (Node 22 / ubuntu-latest)`
|
|
41
|
+
- `Test (Node 22 / macos-latest)`
|
|
42
|
+
- `Test (Node 22 / windows-latest)`
|
|
43
|
+
- `Test (Node 24 / ubuntu-latest)`
|
|
44
|
+
- `Test (Node 24 / macos-latest)`
|
|
45
|
+
- `Test (Node 24 / windows-latest)`
|
|
46
|
+
- `Security (secrets + injection scan)`
|
|
47
|
+
- `Size budget (blocking)`
|
|
48
|
+
- Require linear history (no merge commits)
|
|
49
|
+
- Disallow force-push
|
|
50
|
+
- Admins still bypass for emergency fixes (logged)
|
|
51
|
+
|
|
52
|
+
## When to promote Phase A → Phase B
|
|
53
|
+
|
|
54
|
+
- [ ] Wave A + B of Phase 13 merged to main
|
|
55
|
+
- [ ] `release.yml` (plan 13-06) merged and triggered at least once successfully
|
|
56
|
+
- [ ] Release smoke test (plan 13-07) passed on a real tag
|
|
57
|
+
- [ ] Baseline lock (plan 13-08) committed
|
|
58
|
+
|
|
59
|
+
When all four are true, run `apply-branch-protection.sh --enforcing`.
|
|
60
|
+
|
|
61
|
+
## Rollback
|
|
62
|
+
|
|
63
|
+
To revert either phase: `apply-branch-protection.sh --disable` removes all
|
|
64
|
+
protection rules. Use only if a protection misconfiguration is blocking a
|
|
65
|
+
legitimate merge.
|