@hegemonart/get-design-done 1.19.5 → 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 +90 -0
- package/README.md +12 -0
- package/agents/design-auditor.md +12 -0
- package/agents/design-discussant.md +14 -0
- package/agents/design-reflector.md +23 -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 +29 -7
- package/reference/authority-feeds.md +4 -2
- package/reference/checklists.md +30 -0
- package/reference/component-authoring.md +184 -0
- package/reference/config-schema.md +2 -2
- package/reference/emotional-design.md +124 -0
- package/reference/error-recovery.md +58 -0
- package/reference/first-principles.md +89 -0
- package/reference/heuristics.md +70 -0
- package/reference/motion-advanced.md +192 -3
- package/reference/registry.json +28 -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/reference/shared-preamble.md +10 -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
|
@@ -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.20.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",
|
|
@@ -26,24 +26,34 @@
|
|
|
26
26
|
"LICENSE"
|
|
27
27
|
],
|
|
28
28
|
"bin": {
|
|
29
|
-
"get-design-done": "./scripts/install.cjs"
|
|
29
|
+
"get-design-done": "./scripts/install.cjs",
|
|
30
|
+
"gdd-state-mcp": "./scripts/mcp-servers/gdd-state/server.ts"
|
|
30
31
|
},
|
|
31
32
|
"publishConfig": {
|
|
32
33
|
"access": "public",
|
|
33
34
|
"provenance": true
|
|
34
35
|
},
|
|
35
36
|
"scripts": {
|
|
36
|
-
"test": "node --test \"tests/**/*.cjs\"",
|
|
37
|
+
"test": "node --test --experimental-strip-types \"tests/**/*.cjs\" \"tests/**/*.ts\"",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"codegen:schemas": "node --experimental-strip-types scripts/codegen-schema-types.ts",
|
|
37
40
|
"lint:md": "npx --yes markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.planning\" \"#.claude\" \"#test-fixture/baselines\"",
|
|
38
41
|
"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.
|
|
42
|
+
"validate:schemas": "node --experimental-strip-types scripts/validate-schemas.ts",
|
|
43
|
+
"validate:frontmatter": "node --experimental-strip-types scripts/validate-frontmatter.ts agents/",
|
|
41
44
|
"detect:stale-refs": "node scripts/detect-stale-refs.cjs",
|
|
42
45
|
"scan:injection": "node scripts/run-injection-scanner-ci.cjs",
|
|
43
46
|
"test:size-budget": "node --test tests/agent-size-budget.test.cjs",
|
|
44
47
|
"release:extract-changelog": "node scripts/extract-changelog-section.cjs",
|
|
45
48
|
"verify:version-sync": "node scripts/verify-version-sync.cjs"
|
|
46
49
|
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.0.0",
|
|
52
|
+
"ajv-cli": "^5.0.0",
|
|
53
|
+
"ajv-formats": "^3.0.1",
|
|
54
|
+
"json-schema-to-typescript": "^15.0.0",
|
|
55
|
+
"typescript": "^5.5.0"
|
|
56
|
+
},
|
|
47
57
|
"keywords": [
|
|
48
58
|
"claude",
|
|
49
59
|
"claude-code",
|
|
@@ -89,10 +99,22 @@
|
|
|
89
99
|
"experience-archive",
|
|
90
100
|
"recall",
|
|
91
101
|
"relevance-counter",
|
|
92
|
-
"record-contract"
|
|
102
|
+
"record-contract",
|
|
103
|
+
"first-principles",
|
|
104
|
+
"emotional-design",
|
|
105
|
+
"component-authoring",
|
|
106
|
+
"disney-12-principles",
|
|
107
|
+
"peak-end-rule",
|
|
108
|
+
"loss-aversion",
|
|
109
|
+
"cognitive-load",
|
|
110
|
+
"doherty-threshold",
|
|
111
|
+
"rams-lens"
|
|
93
112
|
],
|
|
94
113
|
"skills": [
|
|
95
114
|
"SKILL.md"
|
|
96
115
|
],
|
|
97
|
-
"hooks": "hooks/hooks.json"
|
|
116
|
+
"hooks": "hooks/hooks.json",
|
|
117
|
+
"dependencies": {
|
|
118
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
119
|
+
}
|
|
98
120
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
>
|
|
5
5
|
> **Anti-slop thesis:** No Dribbble. No Behance. No LinkedIn. No generic trending aggregators. See `.planning/PROJECT.md` and `.planning/phases/13.2-external-authority-watcher/13.2-CONTEXT.md` §D-08.
|
|
6
6
|
|
|
7
|
-
**Last reviewed:** 2026-04-
|
|
8
|
-
**Feed count:**
|
|
7
|
+
**Last reviewed:** 2026-04-24
|
|
8
|
+
**Feed count:** 28 (updated each time a feed is added or removed)
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
- **[Scott Jehl](https://scottjehl.com/)** — `kind: named-practitioner` · `url: https://scottjehl.com/feed/` · `cadence-hint: monthly` · *Progressive enhancement and web performance; long-form durable analysis.*
|
|
47
47
|
- **[Heydon Pickering](https://heydonworks.com/)** — `kind: named-practitioner` · `url: https://heydonworks.com/feed.xml` · `cadence-hint: irregular` · *Accessibility-first component design; Inclusive Components author.*
|
|
48
48
|
- **[Una Kravets](https://una.im/)** — `kind: named-practitioner` · `url: https://una.im/feed.xml` · `cadence-hint: monthly` · *Chrome DevRel on CSS; surfaces and explains new platform capabilities.*
|
|
49
|
+
- **[Don Norman — jnd.org](https://jnd.org/)** — `kind: named-practitioner` · `url: https://jnd.org/feed/` · `cadence-hint: monthly` · *Don Norman's essays on emotional design, affordances, cognitive design, and human-centered AI. Primary source for `reference/emotional-design.md`.*
|
|
50
|
+
- **[Vitsœ — Dieter Rams](https://www.vitsoe.com/gb/about/good-design)** — `kind: named-practitioner` · `url: https://www.vitsoe.com/feed` · `cadence-hint: irregular` · *Canonical source for Rams's 10 Principles of Good Design. Published by Vitsœ, who worked directly with Rams at Braun. Primary source for the Rams Lens in `reference/checklists.md`.*
|
|
49
51
|
|
|
50
52
|
## User-added Are.na channels (user-extensible)
|
|
51
53
|
|
package/reference/checklists.md
CHANGED
|
@@ -162,3 +162,33 @@ Use this checklist after the main design review for pixel-level craft verificati
|
|
|
162
162
|
- [ ] No `transition: all` anywhere (see BAN-12 transition-all)
|
|
163
163
|
- [ ] No `will-change: all` anywhere (see BAN-13 will-change-all)
|
|
164
164
|
- [ ] `prefers-reduced-motion` respected via `MotionConfig` or `useReducedMotion()`
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Rams Lens — 10 Design Questions
|
|
169
|
+
|
|
170
|
+
Dieter Rams's 10 principles of good design (Vitsœ/Braun, 1970s–80s) applied as a self-audit lens. Each question maps to one principle.
|
|
171
|
+
|
|
172
|
+
- [ ] **Innovative** — Does this design solve the problem in a way that was not possible or obvious before?
|
|
173
|
+
- [ ] **Useful** — Does every element serve the primary function? Nothing decorative that doesn't earn its place?
|
|
174
|
+
- [ ] **Aesthetic** — Is the visual appearance the minimum necessary for legibility and emotional resonance?
|
|
175
|
+
- [ ] **Understandable** — Can the user figure out how to use this without reading documentation or a tooltip?
|
|
176
|
+
- [ ] **Unobtrusive** — Does the design stay in the background and let the content or task take focus?
|
|
177
|
+
- [ ] **Honest** — Does the design not imply capabilities, quality, or status that the product doesn't have?
|
|
178
|
+
- [ ] **Long-lasting** — Is this design free of trend-dependent choices (gradients, micro-styles) that will age in 12 months?
|
|
179
|
+
- [ ] **Thorough** — Have edge cases been considered and handled (empty states, error states, loading states, overflow text)?
|
|
180
|
+
- [ ] **Environmentally friendly** — Is the performance footprint minimal? (image sizes, JS bundle, font weight)
|
|
181
|
+
- [ ] **As little design as possible** — If you removed 20% of the design decisions, would the product be worse? If not, remove them.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Sonner / Component-Authoring Lens — 6 Questions
|
|
186
|
+
|
|
187
|
+
Emil Kowalski's component-authoring principles applied as a per-component self-audit. Full reference: `reference/component-authoring.md`.
|
|
188
|
+
|
|
189
|
+
- [ ] **P-01 API surface** — Does this component work correctly in 1 line with zero configuration?
|
|
190
|
+
- [ ] **P-02 Composability** — Does this component compose via slots/children, not via style-configuration props?
|
|
191
|
+
- [ ] **P-03 Defaults** — Are the defaults so sensible that most consumers never need to pass any props?
|
|
192
|
+
- [ ] **P-04 Animation** — Does every animation in this component communicate a state change? No decorative loops?
|
|
193
|
+
- [ ] **P-05 Accessibility** — Does this component have a complete ARIA contract before any visual styling?
|
|
194
|
+
- [ ] **P-06 Edge honesty** — Are known failure modes documented with `// KNOWN:` or `// EDGE:` comments?
|