@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,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* context-exhaustion.ts — PostToolUse hook
|
|
4
|
+
*
|
|
5
|
+
* Phase 20 Plan 20-13 rewrite of the original context-exhaustion.js.
|
|
6
|
+
* Behavior is byte-equivalent: when tool_response reports context
|
|
7
|
+
* consumption at or above THRESHOLD (default 0.85), the hook writes a
|
|
8
|
+
* <paused> resumption block into .design/STATE.md so the next session
|
|
9
|
+
* can resume with full context. Only writes once per session — if a
|
|
10
|
+
* <paused> block from the same trigger already exists, the hook exits
|
|
11
|
+
* silently.
|
|
12
|
+
*
|
|
13
|
+
* Phase 20 addition: every decision (ok / warn) fires a hook.fired
|
|
14
|
+
* event to .design/telemetry/events.jsonl via appendEvent() (Plan 20-06).
|
|
15
|
+
*
|
|
16
|
+
* Hook type: PostToolUse (any tool)
|
|
17
|
+
* Input: JSON on stdin { tool_name, tool_input, tool_response }
|
|
18
|
+
* Output: JSON on stdout { continue, suppressOutput, message } or nothing
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
existsSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
appendFileSync,
|
|
27
|
+
} from 'node:fs';
|
|
28
|
+
import { dirname, join } from 'node:path';
|
|
29
|
+
import { createInterface } from 'node:readline';
|
|
30
|
+
|
|
31
|
+
import { appendEvent } from '../scripts/lib/event-stream/index.ts';
|
|
32
|
+
import type { HookFiredEvent } from '../scripts/lib/event-stream/index.ts';
|
|
33
|
+
|
|
34
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
interface ToolResponseMeta {
|
|
37
|
+
context_usage?: number | string;
|
|
38
|
+
contextUsage?: number | string;
|
|
39
|
+
}
|
|
40
|
+
interface ToolResponse {
|
|
41
|
+
context_usage?: number | string;
|
|
42
|
+
contextUsage?: number | string;
|
|
43
|
+
metadata?: ToolResponseMeta;
|
|
44
|
+
meta?: ToolResponseMeta;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface HookStdin {
|
|
49
|
+
tool_name?: string;
|
|
50
|
+
tool_input?: Record<string, unknown>;
|
|
51
|
+
tool_response?: ToolResponse;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface HookOutput {
|
|
56
|
+
continue: boolean;
|
|
57
|
+
suppressOutput?: boolean;
|
|
58
|
+
message?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Hook decision emitted on the event stream. */
|
|
62
|
+
export type HookDecision = 'ok' | 'warn';
|
|
63
|
+
|
|
64
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Context-usage fraction above which the hook paints a <paused> block.
|
|
68
|
+
* Override via GDD_CONTEXT_THRESHOLD env var (float in [0,1]).
|
|
69
|
+
*/
|
|
70
|
+
export const THRESHOLD: number = (() => {
|
|
71
|
+
const raw = process.env['GDD_CONTEXT_THRESHOLD'];
|
|
72
|
+
const parsed = raw !== undefined ? Number.parseFloat(raw) : Number.NaN;
|
|
73
|
+
return Number.isFinite(parsed) ? parsed : 0.85;
|
|
74
|
+
})();
|
|
75
|
+
|
|
76
|
+
const STATE_PATH = join(process.cwd(), '.design', 'STATE.md');
|
|
77
|
+
|
|
78
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function now(): string {
|
|
81
|
+
return new Date().toISOString();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Claude Code injects context usage in several shapes across versions.
|
|
86
|
+
* Try direct fields, then metadata.meta alias, then string forms
|
|
87
|
+
* (fraction or percentage). Returns null when no usage data is present.
|
|
88
|
+
*/
|
|
89
|
+
export function extractContextUsage(
|
|
90
|
+
toolResponse: ToolResponse | null | undefined,
|
|
91
|
+
): number | null {
|
|
92
|
+
if (typeof toolResponse !== 'object' || toolResponse === null) return null;
|
|
93
|
+
|
|
94
|
+
if (typeof toolResponse.context_usage === 'number') return toolResponse.context_usage;
|
|
95
|
+
if (typeof toolResponse.contextUsage === 'number') return toolResponse.contextUsage;
|
|
96
|
+
|
|
97
|
+
const meta: ToolResponseMeta =
|
|
98
|
+
toolResponse.metadata ?? toolResponse.meta ?? {};
|
|
99
|
+
if (typeof meta.context_usage === 'number') return meta.context_usage;
|
|
100
|
+
if (typeof meta.contextUsage === 'number') return meta.contextUsage;
|
|
101
|
+
|
|
102
|
+
const raw =
|
|
103
|
+
toolResponse.context_usage ??
|
|
104
|
+
toolResponse.contextUsage ??
|
|
105
|
+
meta.context_usage ??
|
|
106
|
+
meta.contextUsage;
|
|
107
|
+
if (typeof raw === 'string') {
|
|
108
|
+
if (raw.endsWith('%')) return Number.parseFloat(raw) / 100;
|
|
109
|
+
const n = Number.parseFloat(raw);
|
|
110
|
+
if (Number.isFinite(n)) return n > 1 ? n / 100 : n;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function buildPausedBlock(toolName: string, usage: number): string {
|
|
116
|
+
const pct = Math.round(usage * 100);
|
|
117
|
+
const thresholdPct = Math.round(THRESHOLD * 100);
|
|
118
|
+
return `
|
|
119
|
+
<paused>
|
|
120
|
+
recorded: ${now()}
|
|
121
|
+
trigger: context-exhaustion-hook
|
|
122
|
+
context_usage: ${pct}%
|
|
123
|
+
last_tool: ${toolName}
|
|
124
|
+
|
|
125
|
+
## Resumption instructions
|
|
126
|
+
|
|
127
|
+
Context reached ${pct}% during the previous session (threshold: ${thresholdPct}%).
|
|
128
|
+
The session was auto-paused to preserve quality.
|
|
129
|
+
|
|
130
|
+
To resume:
|
|
131
|
+
1. Run \`/gdd:resume\` — it will read this block and restore working context
|
|
132
|
+
2. If mid-plan: check .design/STATE.md for the last completed task
|
|
133
|
+
3. Re-read the active PLAN.md to orient before continuing
|
|
134
|
+
|
|
135
|
+
Intel store status at pause time:
|
|
136
|
+
ls .design/intel/files.json 2>/dev/null && echo "present" || echo "missing"
|
|
137
|
+
</paused>
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function stateFileHasPausedBlock(): boolean {
|
|
142
|
+
if (!existsSync(STATE_PATH)) return false;
|
|
143
|
+
const content = readFileSync(STATE_PATH, 'utf8');
|
|
144
|
+
return (
|
|
145
|
+
content.includes('<paused>') &&
|
|
146
|
+
content.includes('context-exhaustion-hook')
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function appendPausedBlock(block: string): void {
|
|
151
|
+
if (!existsSync(dirname(STATE_PATH))) {
|
|
152
|
+
mkdirSync(dirname(STATE_PATH), { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
if (!existsSync(STATE_PATH)) {
|
|
155
|
+
writeFileSync(STATE_PATH, '# Design State\n\n', 'utf8');
|
|
156
|
+
}
|
|
157
|
+
appendFileSync(STATE_PATH, block, 'utf8');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── event-stream emitter ────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
let CACHED_SESSION_ID: string | null = null;
|
|
163
|
+
function getSessionId(): string {
|
|
164
|
+
if (CACHED_SESSION_ID === null) {
|
|
165
|
+
const iso = new Date().toISOString().replace(/[:.]/g, '-');
|
|
166
|
+
CACHED_SESSION_ID = `gdd-hook-${iso}-${process.pid}`;
|
|
167
|
+
}
|
|
168
|
+
return CACHED_SESSION_ID;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function emitHookFired(decision: HookDecision): void {
|
|
172
|
+
const ev: HookFiredEvent = {
|
|
173
|
+
type: 'hook.fired',
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
sessionId: getSessionId(),
|
|
176
|
+
payload: { hook: 'context-exhaustion', decision },
|
|
177
|
+
};
|
|
178
|
+
try {
|
|
179
|
+
appendEvent(ev);
|
|
180
|
+
} catch {
|
|
181
|
+
// Fail open — event-stream errors must never block the hook.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── main ────────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
async function readStdin(): Promise<string> {
|
|
188
|
+
const rl = createInterface({ input: process.stdin });
|
|
189
|
+
let data = '';
|
|
190
|
+
for await (const line of rl) data += line + '\n';
|
|
191
|
+
return data;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function main(): Promise<void> {
|
|
195
|
+
const inputData = await readStdin();
|
|
196
|
+
|
|
197
|
+
let parsed: HookStdin;
|
|
198
|
+
try {
|
|
199
|
+
parsed = JSON.parse(inputData) as HookStdin;
|
|
200
|
+
} catch {
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const toolName =
|
|
205
|
+
typeof parsed.tool_name === 'string' && parsed.tool_name.length > 0
|
|
206
|
+
? parsed.tool_name
|
|
207
|
+
: 'unknown';
|
|
208
|
+
const toolResponse: ToolResponse = parsed.tool_response ?? {};
|
|
209
|
+
|
|
210
|
+
const usage = extractContextUsage(toolResponse);
|
|
211
|
+
|
|
212
|
+
// No usage data — cannot act. Do not emit a hook.fired event; this
|
|
213
|
+
// is a non-decision, not an "ok" outcome.
|
|
214
|
+
if (usage === null) process.exit(0);
|
|
215
|
+
|
|
216
|
+
// Below threshold — explicit "ok" decision.
|
|
217
|
+
if (usage < THRESHOLD) {
|
|
218
|
+
emitHookFired('ok');
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// At or above threshold but block already present — emit ok (we did
|
|
223
|
+
// the right thing earlier) and bail.
|
|
224
|
+
if (stateFileHasPausedBlock()) {
|
|
225
|
+
emitHookFired('ok');
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const block = buildPausedBlock(toolName, usage);
|
|
230
|
+
appendPausedBlock(block);
|
|
231
|
+
emitHookFired('warn');
|
|
232
|
+
|
|
233
|
+
const response: HookOutput = {
|
|
234
|
+
continue: true,
|
|
235
|
+
suppressOutput: false,
|
|
236
|
+
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.`,
|
|
237
|
+
};
|
|
238
|
+
process.stdout.write(JSON.stringify(response));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const isDirectInvocation =
|
|
242
|
+
process.argv[1] !== undefined &&
|
|
243
|
+
/context-exhaustion\.ts$/.test(process.argv[1]);
|
|
244
|
+
|
|
245
|
+
if (isDirectInvocation) {
|
|
246
|
+
main().catch((err: unknown) => {
|
|
247
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
248
|
+
process.stderr.write(`context-exhaustion hook error: ${msg}\n`);
|
|
249
|
+
process.exit(0);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gdd-read-injection-scanner.ts — PostToolUse hook (matcher: Read)
|
|
4
|
+
*
|
|
5
|
+
* Phase 20 Plan 20-13 rewrite of the original
|
|
6
|
+
* gdd-read-injection-scanner.js. Scans Read tool output for common
|
|
7
|
+
* prompt-injection patterns and warns (does not block) when suspicious
|
|
8
|
+
* content is found in a read file. Advisory-only — output is a JSON
|
|
9
|
+
* response containing a `message` field the user / agent can act on.
|
|
10
|
+
*
|
|
11
|
+
* Injection patterns come from scripts/injection-patterns.cjs (Tier-2;
|
|
12
|
+
* not TypeScript-converted per Plan 20-00's policy). We require-load
|
|
13
|
+
* them through Node's CJS interop using `createRequire()` — this works
|
|
14
|
+
* under --experimental-strip-types without `package.json "type":"module"`
|
|
15
|
+
* changes and keeps the .cjs module shared with
|
|
16
|
+
* scripts/run-injection-scanner-ci.cjs unchanged.
|
|
17
|
+
*
|
|
18
|
+
* Phase 20 addition: every decision (block / allow) fires a hook.fired
|
|
19
|
+
* event via appendEvent() (Plan 20-06). "block" is used for the
|
|
20
|
+
* warning-emission path even though the hook itself never hard-blocks
|
|
21
|
+
* — it signals the advisory decision to downstream consumers.
|
|
22
|
+
*
|
|
23
|
+
* Hook type: PostToolUse (matcher: Read)
|
|
24
|
+
* Input: JSON on stdin { tool_name, tool_input, tool_response }
|
|
25
|
+
* Output: JSON on stdout { continue, suppressOutput, message } or nothing
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
import { createInterface } from 'node:readline';
|
|
30
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
31
|
+
|
|
32
|
+
import { appendEvent } from '../scripts/lib/event-stream/index.ts';
|
|
33
|
+
import type { HookFiredEvent } from '../scripts/lib/event-stream/index.ts';
|
|
34
|
+
|
|
35
|
+
// ── require-bridge to the shared .cjs pattern file ──────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load injection-patterns.cjs through Node's CJS require even though
|
|
39
|
+
* this TS file runs under --experimental-strip-types (which auto-detects
|
|
40
|
+
* ES-module mode). createRequire() can be anchored on any absolute
|
|
41
|
+
* filesystem path (or a `file://` URL string) — we deliberately avoid
|
|
42
|
+
* `import.meta.url` so this module stays compatible with the `Node16`
|
|
43
|
+
* tsconfig module setting without forcing `"type":"module"` in
|
|
44
|
+
* package.json (which would break the Tier-2 .cjs tests per Plan 20-00).
|
|
45
|
+
*
|
|
46
|
+
* Path resolution: when Claude Code invokes the hook, it passes the
|
|
47
|
+
* absolute path as argv[1]. We anchor against that (so the .cjs
|
|
48
|
+
* resolves relative to this file's own directory). Falls back to
|
|
49
|
+
* process.cwd() — scripts/injection-patterns.cjs lives under the
|
|
50
|
+
* package root, which is cwd in both CI and npm script contexts.
|
|
51
|
+
*/
|
|
52
|
+
function loadPatterns(): readonly RegExp[] {
|
|
53
|
+
const hookPath =
|
|
54
|
+
typeof process.argv[1] === 'string' && process.argv[1].length > 0
|
|
55
|
+
? isAbsolute(process.argv[1])
|
|
56
|
+
? process.argv[1]
|
|
57
|
+
: resolve(process.argv[1])
|
|
58
|
+
: resolve('hooks/gdd-read-injection-scanner.ts');
|
|
59
|
+
const require = createRequire(hookPath);
|
|
60
|
+
const candidatePaths: string[] = [
|
|
61
|
+
join(dirname(hookPath), '..', 'scripts', 'injection-patterns.cjs'),
|
|
62
|
+
join(process.cwd(), 'scripts', 'injection-patterns.cjs'),
|
|
63
|
+
];
|
|
64
|
+
let lastErr: unknown = null;
|
|
65
|
+
for (const p of candidatePaths) {
|
|
66
|
+
try {
|
|
67
|
+
const mod = require(p) as {
|
|
68
|
+
INJECTION_PATTERNS: Array<{ name: string; re: RegExp }>;
|
|
69
|
+
};
|
|
70
|
+
return mod.INJECTION_PATTERNS.map((entry) => entry.re);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
lastErr = err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const msg =
|
|
76
|
+
lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
77
|
+
throw new Error(
|
|
78
|
+
`gdd-read-injection-scanner: failed to load injection-patterns.cjs (${msg})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const INJECTION_PATTERNS: readonly RegExp[] = loadPatterns();
|
|
83
|
+
|
|
84
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
interface HookStdin {
|
|
87
|
+
tool_name?: string;
|
|
88
|
+
tool_input?: { file_path?: string };
|
|
89
|
+
tool_response?: { content?: string };
|
|
90
|
+
[key: string]: unknown;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface HookOutput {
|
|
94
|
+
continue: boolean;
|
|
95
|
+
suppressOutput?: boolean;
|
|
96
|
+
message?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Hook decision tag for the event stream. */
|
|
100
|
+
export type HookDecision = 'block' | 'allow';
|
|
101
|
+
|
|
102
|
+
// ── event-stream emitter ────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
let CACHED_SESSION_ID: string | null = null;
|
|
105
|
+
function getSessionId(): string {
|
|
106
|
+
if (CACHED_SESSION_ID === null) {
|
|
107
|
+
const iso = new Date().toISOString().replace(/[:.]/g, '-');
|
|
108
|
+
CACHED_SESSION_ID = `gdd-hook-${iso}-${process.pid}`;
|
|
109
|
+
}
|
|
110
|
+
return CACHED_SESSION_ID;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function emitHookFired(decision: HookDecision): void {
|
|
114
|
+
const ev: HookFiredEvent = {
|
|
115
|
+
type: 'hook.fired',
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
sessionId: getSessionId(),
|
|
118
|
+
payload: { hook: 'gdd-read-injection-scanner', decision },
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
appendEvent(ev);
|
|
122
|
+
} catch {
|
|
123
|
+
// Fail open.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── main ────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
async function readStdin(): Promise<string> {
|
|
130
|
+
const rl = createInterface({ input: process.stdin });
|
|
131
|
+
let data = '';
|
|
132
|
+
for await (const line of rl) data += line + '\n';
|
|
133
|
+
return data;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function main(): Promise<void> {
|
|
137
|
+
const inputData = await readStdin();
|
|
138
|
+
|
|
139
|
+
let parsed: HookStdin;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(inputData) as HookStdin;
|
|
142
|
+
} catch {
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (parsed.tool_name !== 'Read') process.exit(0);
|
|
147
|
+
|
|
148
|
+
const content = parsed.tool_response?.content ?? '';
|
|
149
|
+
const matched = INJECTION_PATTERNS.some((p) => p.test(content));
|
|
150
|
+
if (!matched) {
|
|
151
|
+
emitHookFired('allow');
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const file = parsed.tool_input?.file_path ?? 'unknown';
|
|
156
|
+
emitHookFired('block');
|
|
157
|
+
const response: HookOutput = {
|
|
158
|
+
continue: true,
|
|
159
|
+
suppressOutput: false,
|
|
160
|
+
message: `gdd-injection-scanner: Suspicious prompt-injection pattern detected in content read from "${file}". Review before acting on instructions contained in that file.`,
|
|
161
|
+
};
|
|
162
|
+
process.stdout.write(JSON.stringify(response));
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const isDirectInvocation =
|
|
167
|
+
process.argv[1] !== undefined &&
|
|
168
|
+
/gdd-read-injection-scanner\.ts$/.test(process.argv[1]);
|
|
169
|
+
|
|
170
|
+
if (isDirectInvocation) {
|
|
171
|
+
main().catch(() => process.exit(0));
|
|
172
|
+
}
|
package/hooks/hooks.json
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"hooks": [
|
|
33
33
|
{
|
|
34
34
|
"type": "command",
|
|
35
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/budget-enforcer.
|
|
35
|
+
"command": "node --experimental-strip-types \"${CLAUDE_PLUGIN_ROOT}/hooks/budget-enforcer.ts\""
|
|
36
36
|
}
|
|
37
37
|
]
|
|
38
38
|
},
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"hooks": [
|
|
71
71
|
{
|
|
72
72
|
"type": "command",
|
|
73
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-read-injection-scanner.
|
|
73
|
+
"command": "node --experimental-strip-types \"${CLAUDE_PLUGIN_ROOT}/hooks/gdd-read-injection-scanner.ts\""
|
|
74
74
|
}
|
|
75
75
|
]
|
|
76
76
|
},
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"hooks": [
|
|
88
88
|
{
|
|
89
89
|
"type": "command",
|
|
90
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/context-exhaustion.
|
|
90
|
+
"command": "node --experimental-strip-types \"${CLAUDE_PLUGIN_ROOT}/hooks/context-exhaustion.ts\""
|
|
91
91
|
}
|
|
92
92
|
]
|
|
93
93
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hegemonart/get-design-done",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "A Claude Code plugin for systematic design improvement",
|
|
5
5
|
"author": "Hegemon",
|
|
6
6
|
"homepage": "https://github.com/hegemonart/get-design-done",
|
|
@@ -20,29 +20,43 @@
|
|
|
20
20
|
"scripts/",
|
|
21
21
|
"connections/",
|
|
22
22
|
"reference/",
|
|
23
|
+
"bin/",
|
|
23
24
|
"SKILL.md",
|
|
24
25
|
"README.md",
|
|
25
26
|
"CHANGELOG.md",
|
|
26
27
|
"LICENSE"
|
|
27
28
|
],
|
|
28
29
|
"bin": {
|
|
29
|
-
"get-design-done": "./scripts/install.cjs"
|
|
30
|
+
"get-design-done": "./scripts/install.cjs",
|
|
31
|
+
"gdd-state-mcp": "./scripts/mcp-servers/gdd-state/server.ts",
|
|
32
|
+
"gdd-sdk": "./bin/gdd-sdk"
|
|
30
33
|
},
|
|
31
34
|
"publishConfig": {
|
|
32
35
|
"access": "public",
|
|
33
36
|
"provenance": true
|
|
34
37
|
},
|
|
35
38
|
"scripts": {
|
|
36
|
-
"test": "node --test \"tests/**/*.cjs\"",
|
|
39
|
+
"test": "node --test --experimental-strip-types \"tests/**/*.cjs\" \"tests/**/*.ts\"",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"codegen:schemas": "node --experimental-strip-types scripts/codegen-schema-types.ts",
|
|
37
42
|
"lint:md": "npx --yes markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.planning\" \"#.claude\" \"#test-fixture/baselines\"",
|
|
38
43
|
"lint:links": "npx --yes lychee --no-progress --accept 200,206,403,429 \"**/*.md\" || true",
|
|
39
|
-
"validate:schemas": "node scripts/validate-schemas.
|
|
40
|
-
"validate:frontmatter": "node scripts/validate-frontmatter.
|
|
44
|
+
"validate:schemas": "node --experimental-strip-types scripts/validate-schemas.ts",
|
|
45
|
+
"validate:frontmatter": "node --experimental-strip-types scripts/validate-frontmatter.ts agents/",
|
|
41
46
|
"detect:stale-refs": "node scripts/detect-stale-refs.cjs",
|
|
42
47
|
"scan:injection": "node scripts/run-injection-scanner-ci.cjs",
|
|
43
48
|
"test:size-budget": "node --test tests/agent-size-budget.test.cjs",
|
|
44
49
|
"release:extract-changelog": "node scripts/extract-changelog-section.cjs",
|
|
45
|
-
"verify:version-sync": "node scripts/verify-version-sync.cjs"
|
|
50
|
+
"verify:version-sync": "node scripts/verify-version-sync.cjs",
|
|
51
|
+
"typecheck:session-runner": "tsc --noEmit",
|
|
52
|
+
"gdd-sdk": "node --experimental-strip-types scripts/lib/cli/index.ts"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^22.0.0",
|
|
56
|
+
"ajv-cli": "^5.0.0",
|
|
57
|
+
"ajv-formats": "^3.0.1",
|
|
58
|
+
"json-schema-to-typescript": "^15.0.0",
|
|
59
|
+
"typescript": "^5.5.0"
|
|
46
60
|
},
|
|
47
61
|
"keywords": [
|
|
48
62
|
"claude",
|
|
@@ -55,53 +69,20 @@
|
|
|
55
69
|
"reflection",
|
|
56
70
|
"tested",
|
|
57
71
|
"ci",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"make-interfaces-feel-better",
|
|
66
|
-
"palette-catalog",
|
|
67
|
-
"style-vocabulary",
|
|
68
|
-
"industry-palettes",
|
|
69
|
-
"ui-style-vocabulary",
|
|
70
|
-
"variable-fonts",
|
|
71
|
-
"container-queries",
|
|
72
|
-
"view-transitions",
|
|
73
|
-
"motion-vocabulary",
|
|
74
|
-
"motion-easings",
|
|
75
|
-
"transition-taxonomy",
|
|
76
|
-
"gesture-mechanics",
|
|
77
|
-
"clip-path-animation",
|
|
78
|
-
"component-specs",
|
|
79
|
-
"design-system-benchmarks",
|
|
80
|
-
"i18n",
|
|
81
|
-
"user-research",
|
|
82
|
-
"information-architecture",
|
|
83
|
-
"form-patterns",
|
|
84
|
-
"data-viz",
|
|
85
|
-
"platforms",
|
|
86
|
-
"cross-cycle-memory",
|
|
87
|
-
"fts5",
|
|
88
|
-
"checkpoints",
|
|
89
|
-
"experience-archive",
|
|
90
|
-
"recall",
|
|
91
|
-
"relevance-counter",
|
|
92
|
-
"record-contract",
|
|
93
|
-
"first-principles",
|
|
94
|
-
"emotional-design",
|
|
95
|
-
"component-authoring",
|
|
96
|
-
"disney-12-principles",
|
|
97
|
-
"peak-end-rule",
|
|
98
|
-
"loss-aversion",
|
|
99
|
-
"cognitive-load",
|
|
100
|
-
"doherty-threshold",
|
|
101
|
-
"rams-lens"
|
|
72
|
+
"headless",
|
|
73
|
+
"cli",
|
|
74
|
+
"codex",
|
|
75
|
+
"gemini",
|
|
76
|
+
"mcp",
|
|
77
|
+
"parallel-agents",
|
|
78
|
+
"agent-sdk"
|
|
102
79
|
],
|
|
103
80
|
"skills": [
|
|
104
81
|
"SKILL.md"
|
|
105
82
|
],
|
|
106
|
-
"hooks": "hooks/hooks.json"
|
|
83
|
+
"hooks": "hooks/hooks.json",
|
|
84
|
+
"dependencies": {
|
|
85
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.119",
|
|
86
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
87
|
+
}
|
|
107
88
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Codex CLI Tool Map
|
|
2
|
+
|
|
3
|
+
Last verified: 2026-04-24
|
|
4
|
+
|
|
5
|
+
When a GDD skill references a Claude Code tool name, the Codex runtime
|
|
6
|
+
translates to the equivalent below. Skills do NOT need to branch — the tool
|
|
7
|
+
name in prose is authoritative; Codex resolves via this map.
|
|
8
|
+
|
|
9
|
+
## Tool-name mapping
|
|
10
|
+
|
|
11
|
+
| CC name | Codex name | Notes |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `Read` | `read_file` | Takes `path`; returns file content. |
|
|
14
|
+
| `Write` | `apply_patch` (create mode) | Requires a diff-style patch; ensure tool call emits `{action:'create', path, content}`. |
|
|
15
|
+
| `Edit` | `apply_patch` (update mode) | Emits `{action:'update', path, patch}` with unified diff. |
|
|
16
|
+
| `Bash` | `shell` | Takes `{command: string, cwd?, timeout_sec?}`. |
|
|
17
|
+
| `Grep` | `shell` | Compose a `rg` / `grep -rn` invocation; no native Codex grep tool. |
|
|
18
|
+
| `Glob` | `shell` | Compose `ls` / `find`; no native Codex glob. |
|
|
19
|
+
| `Task` | Sub-invocation via nested Codex | Codex spawns nested sessions via its own CLI, not a tool call. Skills requiring Task should prefer the MCP `gdd-state` tool layer instead. |
|
|
20
|
+
| `WebSearch` | `web_search` | If enabled in Codex policy. |
|
|
21
|
+
| `WebFetch` | `shell` (curl) or `web_search.open` | Prefer curl for deterministic output. |
|
|
22
|
+
|
|
23
|
+
## MCP server `gdd-state`
|
|
24
|
+
|
|
25
|
+
The gdd-state MCP server works unchanged on Codex. Configure Codex to load
|
|
26
|
+
it by adding to `~/.codex/config.toml`:
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
[[mcp_servers]]
|
|
30
|
+
name = "gdd-state"
|
|
31
|
+
command = "node"
|
|
32
|
+
args = ["--experimental-strip-types", "<pkg-root>/scripts/mcp-servers/gdd-state/server.ts"]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
All 11 tools exposed by the server appear as `mcp__gdd_state__*` in Codex.
|
|
36
|
+
|
|
37
|
+
## Known gaps
|
|
38
|
+
|
|
39
|
+
- `Task` spawning: Codex does not expose nested-session as a tool call. For
|
|
40
|
+
now, skills that rely on `Task` (parallel mappers in Plan 21-06, parallel
|
|
41
|
+
discussants in Plan 21-07) should invoke the gdd-sdk CLI as a shell
|
|
42
|
+
subprocess: `shell("npx gdd-sdk stage explore --parallel")`. This is
|
|
43
|
+
documented in AGENTS.md.
|
|
44
|
+
- `apply_patch` diff format differs from CC's Edit: Codex expects unified
|
|
45
|
+
diff (`---`/`+++`/`@@` hunks), while CC's Edit takes `old_string`/`new_string`.
|
|
46
|
+
The plugin's skill prose uses Edit's semantics; on Codex the runtime must
|
|
47
|
+
generate the unified diff from the old/new pair. A helper lives at
|
|
48
|
+
`scripts/lib/harness/edit-to-patch.ts` (reserved for Phase 22 wiring).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
Last verified: 2026-04-24 — tool surface re-checked against Codex CLI docs
|
|
53
|
+
current to this date. Revisit whenever Codex ships a tool-vocabulary change.
|
|
@@ -185,7 +185,7 @@ If `.design/budget.json` is missing when any `/gdd:*` command runs, `scripts/boo
|
|
|
185
185
|
|
|
186
186
|
## .design/telemetry/costs.jsonl + .design/agent-metrics.json (Phase 10.1)
|
|
187
187
|
|
|
188
|
-
Phase 10.1 introduces two measurement artifacts written by `hooks/budget-enforcer.js` (PreToolUse on `Agent` spawns) and `scripts/aggregate-agent-metrics.
|
|
188
|
+
Phase 10.1 introduces two measurement artifacts written by `hooks/budget-enforcer.js` (PreToolUse on `Agent` spawns) and `scripts/aggregate-agent-metrics.ts` (detached child of the hook + refresh step of `/gdd:optimize`). Both files live under the gitignored `.design/` directory — they are local session state, not committed.
|
|
189
189
|
|
|
190
190
|
### .design/telemetry/costs.jsonl
|
|
191
191
|
|
|
@@ -223,7 +223,7 @@ Append-only ledger. One JSON object per line. Written by `hooks/budget-enforcer.
|
|
|
223
223
|
|
|
224
224
|
### .design/agent-metrics.json
|
|
225
225
|
|
|
226
|
-
Per-agent aggregate derived from `costs.jsonl` by `scripts/aggregate-agent-metrics.
|
|
226
|
+
Per-agent aggregate derived from `costs.jsonl` by `scripts/aggregate-agent-metrics.ts`. Written atomically via tmp-file + rename. Overwritten in full on every refresh — not append-only. Consumers should treat it as a snapshot.
|
|
227
227
|
|
|
228
228
|
**Schema:**
|
|
229
229
|
|