@hegemonart/get-design-done 1.19.6 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +60 -0
- package/README.md +12 -0
- package/agents/design-reflector.md +13 -0
- package/connections/connections.md +3 -0
- package/connections/figma.md +2 -0
- package/connections/gdd-state.md +186 -0
- package/hooks/budget-enforcer.ts +716 -0
- package/hooks/context-exhaustion.ts +251 -0
- package/hooks/gdd-read-injection-scanner.ts +172 -0
- package/hooks/hooks.json +3 -3
- package/package.json +19 -6
- package/reference/config-schema.md +2 -2
- package/reference/error-recovery.md +58 -0
- package/reference/registry.json +7 -0
- package/reference/schemas/budget.schema.json +42 -0
- package/reference/schemas/events.schema.json +55 -0
- package/reference/schemas/generated.d.ts +419 -0
- package/reference/schemas/iteration-budget.schema.json +36 -0
- package/reference/schemas/mcp-gdd-state-tools.schema.json +89 -0
- package/reference/schemas/rate-limits.schema.json +31 -0
- package/scripts/aggregate-agent-metrics.ts +282 -0
- package/scripts/codegen-schema-types.ts +149 -0
- package/scripts/lib/error-classifier.cjs +232 -0
- package/scripts/lib/error-classifier.d.cts +44 -0
- package/scripts/lib/event-stream/emitter.ts +88 -0
- package/scripts/lib/event-stream/index.ts +154 -0
- package/scripts/lib/event-stream/types.ts +127 -0
- package/scripts/lib/event-stream/writer.ts +154 -0
- package/scripts/lib/gdd-errors/classification.ts +124 -0
- package/scripts/lib/gdd-errors/index.ts +218 -0
- package/scripts/lib/gdd-state/gates.ts +216 -0
- package/scripts/lib/gdd-state/index.ts +167 -0
- package/scripts/lib/gdd-state/lockfile.ts +232 -0
- package/scripts/lib/gdd-state/mutator.ts +574 -0
- package/scripts/lib/gdd-state/parser.ts +523 -0
- package/scripts/lib/gdd-state/types.ts +179 -0
- package/scripts/lib/iteration-budget.cjs +205 -0
- package/scripts/lib/iteration-budget.d.cts +32 -0
- package/scripts/lib/jittered-backoff.cjs +112 -0
- package/scripts/lib/jittered-backoff.d.cts +38 -0
- package/scripts/lib/lockfile.cjs +177 -0
- package/scripts/lib/lockfile.d.cts +21 -0
- package/scripts/lib/prompt-sanitizer/index.ts +435 -0
- package/scripts/lib/prompt-sanitizer/patterns.ts +173 -0
- package/scripts/lib/rate-guard.cjs +365 -0
- package/scripts/lib/rate-guard.d.cts +38 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_blocker.schema.json +67 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_decision.schema.json +68 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_must_have.schema.json +68 -0
- package/scripts/mcp-servers/gdd-state/schemas/checkpoint.schema.json +51 -0
- package/scripts/mcp-servers/gdd-state/schemas/frontmatter_update.schema.json +62 -0
- package/scripts/mcp-servers/gdd-state/schemas/get.schema.json +51 -0
- package/scripts/mcp-servers/gdd-state/schemas/probe_connections.schema.json +75 -0
- package/scripts/mcp-servers/gdd-state/schemas/resolve_blocker.schema.json +66 -0
- package/scripts/mcp-servers/gdd-state/schemas/set_status.schema.json +47 -0
- package/scripts/mcp-servers/gdd-state/schemas/transition_stage.schema.json +70 -0
- package/scripts/mcp-servers/gdd-state/schemas/update_progress.schema.json +58 -0
- package/scripts/mcp-servers/gdd-state/server.ts +288 -0
- package/scripts/mcp-servers/gdd-state/tools/add_blocker.ts +72 -0
- package/scripts/mcp-servers/gdd-state/tools/add_decision.ts +89 -0
- package/scripts/mcp-servers/gdd-state/tools/add_must_have.ts +113 -0
- package/scripts/mcp-servers/gdd-state/tools/checkpoint.ts +60 -0
- package/scripts/mcp-servers/gdd-state/tools/frontmatter_update.ts +91 -0
- package/scripts/mcp-servers/gdd-state/tools/get.ts +51 -0
- package/scripts/mcp-servers/gdd-state/tools/index.ts +51 -0
- package/scripts/mcp-servers/gdd-state/tools/probe_connections.ts +73 -0
- package/scripts/mcp-servers/gdd-state/tools/resolve_blocker.ts +84 -0
- package/scripts/mcp-servers/gdd-state/tools/set_status.ts +54 -0
- package/scripts/mcp-servers/gdd-state/tools/shared.ts +194 -0
- package/scripts/mcp-servers/gdd-state/tools/transition_stage.ts +80 -0
- package/scripts/mcp-servers/gdd-state/tools/update_progress.ts +81 -0
- package/scripts/validate-frontmatter.ts +114 -0
- package/scripts/validate-schemas.ts +401 -0
- package/skills/brief/SKILL.md +15 -6
- package/skills/design/SKILL.md +31 -13
- package/skills/explore/SKILL.md +41 -17
- package/skills/health/SKILL.md +15 -4
- package/skills/optimize/SKILL.md +3 -3
- package/skills/pause/SKILL.md +16 -10
- package/skills/plan/SKILL.md +33 -17
- package/skills/progress/SKILL.md +15 -11
- package/skills/resume/SKILL.md +19 -10
- package/skills/settings/SKILL.md +11 -3
- package/skills/todo/SKILL.md +12 -3
- package/skills/verify/SKILL.md +65 -29
- package/hooks/budget-enforcer.js +0 -329
- package/hooks/context-exhaustion.js +0 -127
- package/hooks/gdd-read-injection-scanner.js +0 -39
- package/scripts/aggregate-agent-metrics.js +0 -173
- package/scripts/validate-frontmatter.cjs +0 -68
- package/scripts/validate-schemas.cjs +0 -242
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// scripts/lib/gdd-state/lockfile.ts — PID+timestamp sibling lockfile.
|
|
2
|
+
//
|
|
3
|
+
// Pattern (from GSD state-mutation.ts D2):
|
|
4
|
+
// Lock file at `${target}.lock` holds JSON
|
|
5
|
+
// { pid: <number>, host: <string>, acquired_at: <ISO 8601> }
|
|
6
|
+
// Acquire = atomic `writeFileSync(..., { flag: 'wx' })`.
|
|
7
|
+
// On EEXIST: check staleness, retry after pollMs, fail after maxWaitMs.
|
|
8
|
+
// Stale = pid dead (ESRCH via process.kill(pid, 0)) OR acquired_at older
|
|
9
|
+
// than staleMs.
|
|
10
|
+
// Release = unlink(path); ENOENT is not an error.
|
|
11
|
+
//
|
|
12
|
+
// Windows note: on Windows, AV scanners and file-indexers can hold a file
|
|
13
|
+
// briefly after close. A writeFileSync with 'wx' can fail with EPERM or
|
|
14
|
+
// EBUSY even when the target is free; we treat these as transient and
|
|
15
|
+
// retry (same code path as EEXIST).
|
|
16
|
+
|
|
17
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'node:fs';
|
|
18
|
+
import { hostname } from 'node:os';
|
|
19
|
+
|
|
20
|
+
import { LockAcquisitionError } from './types.ts';
|
|
21
|
+
|
|
22
|
+
export interface AcquireOptions {
|
|
23
|
+
/** ms after which an existing lock is considered stale. Default 60_000. */
|
|
24
|
+
staleMs?: number;
|
|
25
|
+
/** total ms to wait before throwing LockAcquisitionError. Default 5_000. */
|
|
26
|
+
maxWaitMs?: number;
|
|
27
|
+
/** ms between retry attempts. Default 50. */
|
|
28
|
+
pollMs?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Release function returned by `acquire()`. Idempotent. */
|
|
32
|
+
export type LockRelease = () => Promise<void>;
|
|
33
|
+
|
|
34
|
+
interface LockPayload {
|
|
35
|
+
pid: number;
|
|
36
|
+
host: string;
|
|
37
|
+
acquired_at: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEFAULTS = {
|
|
41
|
+
staleMs: 60_000,
|
|
42
|
+
maxWaitMs: 5_000,
|
|
43
|
+
pollMs: 50,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Acquire an advisory lock at `${path}.lock`. Returns a release function.
|
|
48
|
+
*
|
|
49
|
+
* @throws LockAcquisitionError when `maxWaitMs` elapses without acquiring.
|
|
50
|
+
*/
|
|
51
|
+
export async function acquire(
|
|
52
|
+
path: string,
|
|
53
|
+
opts: AcquireOptions = {},
|
|
54
|
+
): Promise<LockRelease> {
|
|
55
|
+
const staleMs: number = opts.staleMs ?? DEFAULTS.staleMs;
|
|
56
|
+
const maxWaitMs: number = opts.maxWaitMs ?? DEFAULTS.maxWaitMs;
|
|
57
|
+
const pollMs: number = opts.pollMs ?? DEFAULTS.pollMs;
|
|
58
|
+
const lockPath: string = `${path}.lock`;
|
|
59
|
+
const startedAt: number = Date.now();
|
|
60
|
+
|
|
61
|
+
const payload: LockPayload = {
|
|
62
|
+
pid: process.pid,
|
|
63
|
+
host: hostname(),
|
|
64
|
+
acquired_at: new Date().toISOString(),
|
|
65
|
+
};
|
|
66
|
+
const payloadText: string = JSON.stringify(payload);
|
|
67
|
+
|
|
68
|
+
// Guard against absurd configs that would make the function never run.
|
|
69
|
+
if (maxWaitMs < 0 || pollMs < 0 || staleMs < 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`invalid AcquireOptions: staleMs=${staleMs}, maxWaitMs=${maxWaitMs}, pollMs=${pollMs}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
while (true) {
|
|
76
|
+
try {
|
|
77
|
+
// Atomic create — fails if lock exists. 'wx' = O_CREAT | O_EXCL.
|
|
78
|
+
writeFileSync(lockPath, payloadText, { flag: 'wx', encoding: 'utf8' });
|
|
79
|
+
// Got it. Return idempotent release.
|
|
80
|
+
return makeRelease(lockPath);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const code = getErrnoCode(err);
|
|
83
|
+
// EEXIST: another holder present — check staleness.
|
|
84
|
+
// EPERM / EBUSY on Windows: transient AV / indexer — same retry path.
|
|
85
|
+
if (code !== 'EEXIST' && code !== 'EPERM' && code !== 'EBUSY') {
|
|
86
|
+
// Unexpected error (e.g., ENOENT if the parent dir doesn't exist,
|
|
87
|
+
// EACCES, ENOSPC). Rethrow untouched — callers should see the
|
|
88
|
+
// OS-level reason.
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Try to read current holder. Best-effort — if the file just
|
|
93
|
+
// vanished between EEXIST and read, loop immediately.
|
|
94
|
+
const existing: string | null = readLockSafe(lockPath);
|
|
95
|
+
if (existing === null) {
|
|
96
|
+
// vanished; retry without sleeping.
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parsed: LockPayload | null = parseLock(existing);
|
|
101
|
+
if (parsed !== null && isStale(parsed, staleMs)) {
|
|
102
|
+
// Clear stale lock and retry.
|
|
103
|
+
try {
|
|
104
|
+
unlinkSync(lockPath);
|
|
105
|
+
} catch (delErr) {
|
|
106
|
+
const delCode = getErrnoCode(delErr);
|
|
107
|
+
if (delCode !== 'ENOENT') {
|
|
108
|
+
// Someone else cleared it first; fall through to retry.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Fresh (or unparseable — treat as fresh to avoid clobbering a
|
|
115
|
+
// different writer). Wait and retry.
|
|
116
|
+
if (Date.now() - startedAt >= maxWaitMs) {
|
|
117
|
+
throw new LockAcquisitionError(lockPath, existing, Date.now() - startedAt);
|
|
118
|
+
}
|
|
119
|
+
await sleep(pollMs);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** --- helpers --- */
|
|
125
|
+
|
|
126
|
+
function makeRelease(lockPath: string): LockRelease {
|
|
127
|
+
let released = false;
|
|
128
|
+
return async () => {
|
|
129
|
+
if (released) return;
|
|
130
|
+
released = true;
|
|
131
|
+
try {
|
|
132
|
+
unlinkSync(lockPath);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const code = getErrnoCode(err);
|
|
135
|
+
if (code !== 'ENOENT') {
|
|
136
|
+
// EPERM on Windows from AV: retry once. If still bad, swallow —
|
|
137
|
+
// idempotent release should not throw on best-effort cleanup.
|
|
138
|
+
if (code === 'EPERM' || code === 'EBUSY') {
|
|
139
|
+
await sleep(50);
|
|
140
|
+
try {
|
|
141
|
+
if (existsSync(lockPath)) unlinkSync(lockPath);
|
|
142
|
+
} catch {
|
|
143
|
+
// give up silently — lock will be considered stale next acquire.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// For any other code, silently ignore; the next acquirer will
|
|
147
|
+
// either succeed (file is gone) or treat it as stale.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readLockSafe(path: string): string | null {
|
|
154
|
+
try {
|
|
155
|
+
return readFileSync(path, 'utf8');
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const code = getErrnoCode(err);
|
|
158
|
+
if (code === 'ENOENT') return null;
|
|
159
|
+
// EPERM/EBUSY on Windows: treat as still-locked; return a placeholder
|
|
160
|
+
// so the caller can surface it.
|
|
161
|
+
return '<unreadable>';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseLock(raw: string): LockPayload | null {
|
|
166
|
+
try {
|
|
167
|
+
const obj = JSON.parse(raw) as unknown;
|
|
168
|
+
if (
|
|
169
|
+
typeof obj === 'object' &&
|
|
170
|
+
obj !== null &&
|
|
171
|
+
typeof (obj as Record<string, unknown>)['pid'] === 'number' &&
|
|
172
|
+
typeof (obj as Record<string, unknown>)['host'] === 'string' &&
|
|
173
|
+
typeof (obj as Record<string, unknown>)['acquired_at'] === 'string'
|
|
174
|
+
) {
|
|
175
|
+
return obj as LockPayload;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isStale(payload: LockPayload, staleMs: number): boolean {
|
|
184
|
+
// 1) PID check — if the process is dead, the lock is stale.
|
|
185
|
+
if (!isPidAlive(payload.pid, payload.host)) return true;
|
|
186
|
+
// 2) Age check — acquired_at older than staleMs is stale even if the
|
|
187
|
+
// PID is reused by something else.
|
|
188
|
+
const acquiredAt = Date.parse(payload.acquired_at);
|
|
189
|
+
if (!Number.isFinite(acquiredAt)) return true; // garbage timestamp
|
|
190
|
+
return Date.now() - acquiredAt > staleMs;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check whether a PID is alive *on this host*. Returns true (treat as
|
|
195
|
+
* alive) when the holder is on a different host — cross-host reasoning
|
|
196
|
+
* requires coordination we don't have, so we fall back to the age-based
|
|
197
|
+
* staleness check.
|
|
198
|
+
*/
|
|
199
|
+
function isPidAlive(pid: number, host: string): boolean {
|
|
200
|
+
if (host !== hostname()) {
|
|
201
|
+
// Can't introspect another host's process table — assume alive.
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (pid === process.pid) {
|
|
205
|
+
// Own pid — alive.
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
// Signal 0 just validates the pid; doesn't deliver a signal.
|
|
210
|
+
process.kill(pid, 0);
|
|
211
|
+
return true;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const code = getErrnoCode(err);
|
|
214
|
+
if (code === 'ESRCH') return false; // no such process → dead
|
|
215
|
+
// EPERM on Windows / POSIX: process exists but we can't signal it
|
|
216
|
+
// (different user or privilege). Treat as alive — safer than
|
|
217
|
+
// accidentally stealing someone else's lock.
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getErrnoCode(err: unknown): string | undefined {
|
|
223
|
+
if (typeof err === 'object' && err !== null && 'code' in err) {
|
|
224
|
+
const code = (err as { code?: unknown }).code;
|
|
225
|
+
if (typeof code === 'string') return code;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sleep(ms: number): Promise<void> {
|
|
231
|
+
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
232
|
+
}
|