@hegemonart/get-design-done 1.30.6 → 1.31.5
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 +6 -3
- package/.claude-plugin/plugin.json +5 -2
- package/CHANGELOG.md +105 -0
- package/NOTICE +224 -0
- package/README.md +22 -1
- package/SKILL.md +1 -0
- package/agents/design-authority-watcher.md +1 -1
- package/agents/perf-analyzer.md +2 -2
- package/bin/gdd-mcp +78 -0
- package/bin/gdd-sdk +34 -24
- package/bin/gdd-state-mcp +78 -0
- package/{README.de.md → docs/i18n/README.de.md} +1 -1
- package/{README.fr.md → docs/i18n/README.fr.md} +1 -1
- package/{README.it.md → docs/i18n/README.it.md} +1 -1
- package/{README.ja.md → docs/i18n/README.ja.md} +1 -1
- package/{README.ko.md → docs/i18n/README.ko.md} +1 -1
- package/{README.zh-CN.md → docs/i18n/README.zh-CN.md} +1 -1
- package/hooks/_hook-emit.js +1 -1
- package/hooks/budget-enforcer.ts +5 -5
- package/hooks/context-exhaustion.ts +2 -2
- package/hooks/gdd-precompact-snapshot.js +3 -3
- package/hooks/gdd-read-injection-scanner.ts +2 -2
- package/hooks/gdd-sessionstart-recap.js +1 -1
- package/hooks/gdd-turn-closeout.js +1 -1
- package/package.json +24 -10
- package/recipes/.gitkeep +0 -0
- package/reference/schemas/recipe.schema.json +33 -0
- package/scripts/cli/gdd-events.mjs +5 -5
- package/scripts/lib/cache/gdd-cache-manager.cjs +1 -1
- package/scripts/lib/cli/index.ts +22 -160
- package/scripts/lib/connection-probe/index.cjs +1 -1
- package/scripts/lib/discuss-parallel-runner/aggregator.ts +1 -1
- package/scripts/lib/discuss-parallel-runner/index.ts +1 -1
- package/scripts/lib/error-classifier.cjs +24 -227
- package/scripts/lib/event-stream/index.ts +25 -193
- package/scripts/lib/figma-extract/digest.cjs +430 -0
- package/scripts/lib/figma-extract/parse-url.cjs +87 -0
- package/scripts/lib/figma-extract/payload-schema.json +108 -0
- package/scripts/lib/figma-extract/pull.cjs +394 -0
- package/scripts/lib/figma-extract/receiver.cjs +273 -0
- package/scripts/lib/figma-extract/render-md.cjs +143 -0
- package/scripts/lib/figma-extract/styles-resolver.cjs +147 -0
- package/scripts/lib/figma-extract/walk.cjs +100 -0
- package/scripts/lib/gdd-errors/index.ts +24 -213
- package/scripts/lib/gdd-state/index.ts +23 -161
- package/scripts/lib/health-mirror/index.cjs +88 -1
- package/scripts/lib/iteration-budget.cjs +23 -199
- package/scripts/lib/jittered-backoff.cjs +24 -107
- package/scripts/lib/lockfile.cjs +23 -195
- package/scripts/lib/logger/index.ts +1 -1
- package/scripts/lib/parallelism-engine/concurrency-tuner.cjs +1 -1
- package/scripts/lib/perf-analyzer/index.cjs +1 -1
- package/scripts/lib/pipeline-runner/index.ts +4 -4
- package/scripts/lib/pipeline-runner/state-machine.ts +1 -1
- package/scripts/lib/prompt-dedup/index.cjs +1 -1
- package/scripts/lib/rate-guard.cjs +2 -2
- package/scripts/lib/recipe-loader.cjs +142 -0
- package/scripts/lib/session-runner/errors.ts +3 -3
- package/scripts/lib/session-runner/index.ts +3 -3
- package/scripts/lib/session-runner/transcript.ts +1 -1
- package/scripts/lib/tool-scoping/index.ts +1 -1
- package/scripts/mcp-servers/gdd-mcp/server.ts +29 -311
- package/scripts/mcp-servers/gdd-state/server.ts +28 -282
- package/sdk/README.md +45 -0
- package/{scripts/lib → sdk}/cli/commands/audit.ts +3 -3
- package/{scripts/lib → sdk}/cli/commands/init.ts +3 -3
- package/{scripts/lib → sdk}/cli/commands/query.ts +4 -4
- package/{scripts/lib → sdk}/cli/commands/run.ts +5 -5
- package/{scripts/lib → sdk}/cli/commands/stage.ts +5 -5
- package/sdk/cli/index.js +8091 -0
- package/sdk/cli/index.ts +172 -0
- package/{scripts/lib → sdk}/cli/parse-args.ts +2 -2
- package/{scripts/lib/gdd-errors → sdk/errors}/classification.ts +1 -1
- package/sdk/errors/index.ts +218 -0
- package/{scripts/lib → sdk}/event-stream/emitter.ts +1 -1
- package/sdk/event-stream/index.ts +197 -0
- package/{scripts/lib → sdk}/event-stream/reader.ts +1 -1
- package/{scripts/lib → sdk}/event-stream/types.ts +2 -2
- package/{scripts/lib → sdk}/event-stream/writer.ts +1 -1
- package/sdk/index.ts +19 -0
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/README.md +3 -3
- package/sdk/mcp/gdd-mcp/server.js +1924 -0
- package/sdk/mcp/gdd-mcp/server.ts +325 -0
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_cycle_recap.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_decisions_list.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_events_tail.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_health.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_intel_get.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_learnings_digest.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phase_current.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phases_list.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_plans_list.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_reflections_latest.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_status.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_telemetry_query.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/index.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/shared.ts +3 -3
- package/sdk/mcp/gdd-state/server.js +2790 -0
- package/sdk/mcp/gdd-state/server.ts +294 -0
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_blocker.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_decision.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_must_have.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/checkpoint.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/frontmatter_update.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/get.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/index.ts +1 -1
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/probe_connections.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/resolve_blocker.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/set_status.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/shared.ts +8 -8
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/transition_stage.ts +4 -4
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/update_progress.ts +2 -2
- package/sdk/primitives/error-classifier.cjs +232 -0
- package/sdk/primitives/iteration-budget.cjs +205 -0
- package/sdk/primitives/jittered-backoff.cjs +112 -0
- package/sdk/primitives/lockfile.cjs +201 -0
- package/{scripts/lib/gdd-state → sdk/state}/gates.ts +1 -1
- package/sdk/state/index.ts +167 -0
- package/{scripts/lib/gdd-state → sdk/state}/lockfile.ts +1 -1
- package/{scripts/lib/gdd-state → sdk/state}/mutator.ts +1 -1
- package/{scripts/lib/gdd-state → sdk/state}/parser.ts +1 -1
- package/{scripts/lib/gdd-state → sdk/state}/types.ts +4 -4
- package/skills/figma-extract/SKILL.md +64 -0
- package/skills/health/SKILL.md +10 -0
- package/skills/quality-gate/SKILL.md +2 -2
- package/scripts/aggregate-agent-metrics.ts +0 -282
- package/scripts/bootstrap-manifest.txt +0 -3
- package/scripts/bootstrap.sh +0 -80
- package/scripts/build-distribution-bundles.cjs +0 -549
- package/scripts/build-intel.cjs +0 -486
- package/scripts/codegen-schema-types.ts +0 -149
- package/scripts/detect-stale-refs.cjs +0 -107
- package/scripts/e2e/run-headless.ts +0 -514
- package/scripts/extract-changelog-section.cjs +0 -58
- package/scripts/gsd-cleanup-incubator.cjs +0 -367
- package/scripts/injection-patterns.cjs +0 -58
- package/scripts/lint-agentskills-spec.cjs +0 -457
- package/scripts/release-smoke-test.cjs +0 -200
- package/scripts/rollback-release.sh +0 -42
- package/scripts/run-injection-scanner-ci.cjs +0 -83
- package/scripts/tests/test-authority-rejected-kinds.sh +0 -58
- package/scripts/tests/test-authority-watcher-diff.sh +0 -113
- package/scripts/tests/test-motion-provenance.sh +0 -64
- package/scripts/validate-frontmatter.ts +0 -409
- package/scripts/validate-incubator-scope.cjs +0 -133
- package/scripts/validate-schemas.ts +0 -401
- package/scripts/validate-skill-length.cjs +0 -283
- package/scripts/verify-version-sync.cjs +0 -30
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_cycle_recap.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_decisions_list.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_events_tail.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_health.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_intel_get.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_learnings_digest.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phase_current.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phases_list.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_plans_list.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_reflections_latest.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_status.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_telemetry_query.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_blocker.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_decision.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_must_have.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/checkpoint.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/frontmatter_update.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/get.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/probe_connections.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/resolve_blocker.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/set_status.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/transition_stage.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/update_progress.schema.json +0 -0
- /package/{scripts/lib → sdk/primitives}/error-classifier.d.cts +0 -0
- /package/{scripts/lib → sdk/primitives}/iteration-budget.d.cts +0 -0
- /package/{scripts/lib → sdk/primitives}/jittered-backoff.d.cts +0 -0
- /package/{scripts/lib → sdk/primitives}/lockfile.d.cts +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// scripts/lib/lockfile.cjs
|
|
2
|
+
//
|
|
3
|
+
// Plan 20-14 — PID+timestamp sibling lockfile for `.cjs` consumers.
|
|
4
|
+
//
|
|
5
|
+
// Algorithm mirrors sdk/state/lockfile.ts (Plan 20-01):
|
|
6
|
+
// Lock path: `${target}.lock`
|
|
7
|
+
// Payload: { pid: number, host: string, acquired_at: ISO8601 }
|
|
8
|
+
// Acquire: atomic `writeFileSync(..., { flag: 'wx' })`
|
|
9
|
+
// Stale rule: pid dead (ESRCH via `kill(pid, 0)`) OR `acquired_at` older
|
|
10
|
+
// than `staleMs` OR unparseable payload
|
|
11
|
+
// Release: unlink; ENOENT is not an error; idempotent
|
|
12
|
+
//
|
|
13
|
+
// Windows: AV scanners and file-indexers can hold a file briefly after
|
|
14
|
+
// close. `wx` create may fail with EPERM/EBUSY even when the target is
|
|
15
|
+
// free; we treat these as transient and loop (same code path as EEXIST).
|
|
16
|
+
//
|
|
17
|
+
// Dependency-cycle note: Plan 20-14's rate-guard + iteration-budget
|
|
18
|
+
// consume this module, and both are required to stay dependency-light so
|
|
19
|
+
// that hooks/budget-enforcer.ts can import them without dragging the
|
|
20
|
+
// gdd-state MCP graph along. Hence this standalone .cjs port instead of
|
|
21
|
+
// calling the .ts version.
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const os = require('node:os');
|
|
27
|
+
|
|
28
|
+
const DEFAULT_STALE_MS = 60_000;
|
|
29
|
+
const DEFAULT_MAX_WAIT_MS = 5_000;
|
|
30
|
+
const DEFAULT_POLL_MS = 50;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Acquire an advisory lock at `${path}.lock`. Returns an idempotent
|
|
34
|
+
* async release function.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} path path being locked (we append `.lock`)
|
|
37
|
+
* @param {object} [opts]
|
|
38
|
+
* @param {number} [opts.staleMs] ms after which an existing lock is stale. Default 60_000.
|
|
39
|
+
* @param {number} [opts.maxWaitMs] total ms to wait before throwing. Default 5_000.
|
|
40
|
+
* @param {number} [opts.pollMs] ms between retry attempts. Default 50.
|
|
41
|
+
* @returns {Promise<() => Promise<void>>} release function
|
|
42
|
+
* @throws {Error} with name === 'LockAcquisitionError' when maxWaitMs elapses
|
|
43
|
+
*/
|
|
44
|
+
async function acquire(path, opts) {
|
|
45
|
+
const o = opts || {};
|
|
46
|
+
const staleMs = Number.isFinite(o.staleMs) ? o.staleMs : DEFAULT_STALE_MS;
|
|
47
|
+
const maxWaitMs = Number.isFinite(o.maxWaitMs) ? o.maxWaitMs : DEFAULT_MAX_WAIT_MS;
|
|
48
|
+
const pollMs = Number.isFinite(o.pollMs) ? o.pollMs : DEFAULT_POLL_MS;
|
|
49
|
+
|
|
50
|
+
if (staleMs < 0 || maxWaitMs < 0 || pollMs < 0) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`lockfile.acquire: invalid options (staleMs=${staleMs}, maxWaitMs=${maxWaitMs}, pollMs=${pollMs})`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const lockPath = `${path}.lock`;
|
|
57
|
+
const payload = JSON.stringify({
|
|
58
|
+
pid: process.pid,
|
|
59
|
+
host: os.hostname(),
|
|
60
|
+
acquired_at: new Date().toISOString(),
|
|
61
|
+
});
|
|
62
|
+
const startedAt = Date.now();
|
|
63
|
+
|
|
64
|
+
while (true) {
|
|
65
|
+
try {
|
|
66
|
+
fs.writeFileSync(lockPath, payload, { flag: 'wx', encoding: 'utf8' });
|
|
67
|
+
return makeRelease(lockPath);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
70
|
+
if (code !== 'EEXIST' && code !== 'EPERM' && code !== 'EBUSY') {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
// Try to read the current holder; if it vanished between EEXIST and
|
|
74
|
+
// read, loop immediately.
|
|
75
|
+
const existing = readLockSafe(lockPath);
|
|
76
|
+
if (existing === null) continue;
|
|
77
|
+
|
|
78
|
+
const parsed = parseLock(existing);
|
|
79
|
+
// Only clear when we're confident the lock is stale: the payload
|
|
80
|
+
// parses AND the PID/age check says so. An unparseable payload is
|
|
81
|
+
// treated as fresh — on Windows, AV/indexer can transiently deny
|
|
82
|
+
// reads (EACCES/EPERM/EBUSY), and clearing under that condition
|
|
83
|
+
// would let two writers race and lose increments.
|
|
84
|
+
if (parsed !== null && isStale(parsed, staleMs)) {
|
|
85
|
+
// Clear stale lock; race-tolerant — if it's already gone we get
|
|
86
|
+
// ENOENT, no-op.
|
|
87
|
+
try { fs.unlinkSync(lockPath); } catch { /* ignore */ }
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (Date.now() - startedAt >= maxWaitMs) {
|
|
92
|
+
const e = new Error(
|
|
93
|
+
`lockfile: failed to acquire ${lockPath} within ${maxWaitMs}ms (held by ${existing})`,
|
|
94
|
+
);
|
|
95
|
+
e.name = 'LockAcquisitionError';
|
|
96
|
+
e.lockPath = lockPath;
|
|
97
|
+
e.holder = existing;
|
|
98
|
+
e.waitedMs = Date.now() - startedAt;
|
|
99
|
+
throw e;
|
|
100
|
+
}
|
|
101
|
+
await sleep(pollMs);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function makeRelease(lockPath) {
|
|
107
|
+
let released = false;
|
|
108
|
+
return async function release() {
|
|
109
|
+
if (released) return;
|
|
110
|
+
released = true;
|
|
111
|
+
try {
|
|
112
|
+
fs.unlinkSync(lockPath);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
115
|
+
if (code === 'ENOENT') return; // idempotent — already gone
|
|
116
|
+
if (code === 'EPERM' || code === 'EBUSY') {
|
|
117
|
+
// Windows AV/indexer: retry once.
|
|
118
|
+
await sleep(50);
|
|
119
|
+
try {
|
|
120
|
+
if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath);
|
|
121
|
+
} catch { /* give up; stale-detection will reclaim */ }
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Any other errno: swallow. Best-effort cleanup; stale-age check
|
|
125
|
+
// will eventually reclaim the lock.
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readLockSafe(p) {
|
|
131
|
+
try {
|
|
132
|
+
return fs.readFileSync(p, 'utf8');
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
135
|
+
if (code === 'ENOENT') return null;
|
|
136
|
+
return '<unreadable>';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseLock(raw) {
|
|
141
|
+
try {
|
|
142
|
+
const obj = JSON.parse(raw);
|
|
143
|
+
if (
|
|
144
|
+
obj && typeof obj === 'object' &&
|
|
145
|
+
typeof obj.pid === 'number' &&
|
|
146
|
+
typeof obj.host === 'string' &&
|
|
147
|
+
typeof obj.acquired_at === 'string'
|
|
148
|
+
) {
|
|
149
|
+
return obj;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isStale(payload, staleMs) {
|
|
158
|
+
if (!isPidAlive(payload.pid, payload.host)) return true;
|
|
159
|
+
const t = Date.parse(payload.acquired_at);
|
|
160
|
+
if (!Number.isFinite(t)) return true;
|
|
161
|
+
return Date.now() - t > staleMs;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isPidAlive(pid, host) {
|
|
165
|
+
if (host !== os.hostname()) return true; // can't introspect other hosts
|
|
166
|
+
if (pid === process.pid) return true;
|
|
167
|
+
try {
|
|
168
|
+
process.kill(pid, 0); // signal 0 = validate, don't deliver
|
|
169
|
+
return true;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
172
|
+
if (code === 'ESRCH') return false;
|
|
173
|
+
// EPERM / EACCES: process exists but is unsignalable; treat as alive.
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sleep(ms) {
|
|
179
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* `fs.renameSync` wrapper that retries once on Windows EPERM/EBUSY/EACCES.
|
|
184
|
+
* AV scanners and the file-indexer can briefly hold a destination open
|
|
185
|
+
* after another process closed it, causing rename to fail even when the
|
|
186
|
+
* advisory lock is correctly held.
|
|
187
|
+
*
|
|
188
|
+
* Mirrors the inline retry in sdk/state/index.ts mutate().
|
|
189
|
+
*/
|
|
190
|
+
async function renameWithRetry(from, to) {
|
|
191
|
+
try {
|
|
192
|
+
fs.renameSync(from, to);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
const code = err && typeof err === 'object' ? err.code : undefined;
|
|
195
|
+
if (code !== 'EPERM' && code !== 'EBUSY' && code !== 'EACCES') throw err;
|
|
196
|
+
await sleep(50);
|
|
197
|
+
fs.renameSync(from, to);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { acquire, renameWithRetry };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// sdk/state/gates.ts — pure transition-gate functions.
|
|
2
2
|
//
|
|
3
3
|
// Plan 20-02 (SDK-03): the typed, single-source-of-truth implementation
|
|
4
4
|
// of "can this pipeline advance?" that replaces prose-encoded guards in
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// sdk/state/index.ts — public API for the gdd-state module.
|
|
2
|
+
//
|
|
3
|
+
// This is the ONLY file consumers should import from. The module exposes
|
|
4
|
+
// exactly five surface-level names:
|
|
5
|
+
// * read(path) — parse STATE.md from disk
|
|
6
|
+
// * mutate(path, fn) — atomic read-modify-write under a lock
|
|
7
|
+
// * transition(path, toStage) — gate + stage-advance helper
|
|
8
|
+
// * ParsedState (type) — consumer-visible shape
|
|
9
|
+
// * Stage (type) — stage enum
|
|
10
|
+
//
|
|
11
|
+
// Plan 20-02 wired the real transition gates in via `gateFor(from, to)`
|
|
12
|
+
// imported from `./gates.ts`. Plan 20-04 migrated the error classes
|
|
13
|
+
// (TransitionGateFailed, LockAcquisitionError, ParseError) to the
|
|
14
|
+
// unified `gdd-errors` taxonomy — `types.ts` re-exports them verbatim
|
|
15
|
+
// so consumers of `gdd-state` need no changes.
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync } from 'node:fs';
|
|
18
|
+
|
|
19
|
+
import { acquire } from './lockfile.ts';
|
|
20
|
+
import { parse } from './parser.ts';
|
|
21
|
+
import { serialize } from './mutator.ts';
|
|
22
|
+
import { gateFor } from './gates.ts';
|
|
23
|
+
import {
|
|
24
|
+
TransitionGateFailed,
|
|
25
|
+
isStage,
|
|
26
|
+
type ParsedState,
|
|
27
|
+
type Stage,
|
|
28
|
+
type TransitionResult,
|
|
29
|
+
} from './types.ts';
|
|
30
|
+
|
|
31
|
+
export type { ParsedState, Stage } from './types.ts';
|
|
32
|
+
export { TransitionGateFailed, LockAcquisitionError, ParseError } from './types.ts';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read STATE.md from disk and return the parsed state.
|
|
36
|
+
*
|
|
37
|
+
* Shared-read: no lock is taken. Reads are snapshot-safe for markdown
|
|
38
|
+
* (the OS guarantees a coherent view even if a writer is mid-rename —
|
|
39
|
+
* we either see the old file or the new file, never a torn write,
|
|
40
|
+
* because `mutate()` uses atomic rename).
|
|
41
|
+
*/
|
|
42
|
+
export async function read(path: string): Promise<ParsedState> {
|
|
43
|
+
const raw: string = readFileSync(path, 'utf8');
|
|
44
|
+
return parse(raw).state;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Atomic read-modify-write on STATE.md.
|
|
49
|
+
*
|
|
50
|
+
* Flow:
|
|
51
|
+
* 1. Acquire sibling `.lock` file (PID+timestamp advisory lock).
|
|
52
|
+
* 2. Read current contents.
|
|
53
|
+
* 3. Apply `fn`.
|
|
54
|
+
* 4. Serialize to a `.tmp` file next to `path`.
|
|
55
|
+
* 5. `renameSync(.tmp, path)` — POSIX-atomic; on Windows EPERM means
|
|
56
|
+
* a scanner held it briefly, retry once.
|
|
57
|
+
* 6. Release the lock (in `finally` — released even on mid-fn throw).
|
|
58
|
+
*
|
|
59
|
+
* Crash between write and rename is benign: STATE.md is untouched; the
|
|
60
|
+
* `.tmp` file is orphaned (cleaned up on the next acquire by the caller).
|
|
61
|
+
*/
|
|
62
|
+
export async function mutate(
|
|
63
|
+
path: string,
|
|
64
|
+
fn: (s: ParsedState) => ParsedState,
|
|
65
|
+
): Promise<ParsedState> {
|
|
66
|
+
const release = await acquire(path);
|
|
67
|
+
const tmpPath: string = `${path}.tmp`;
|
|
68
|
+
try {
|
|
69
|
+
const raw: string = readFileSync(path, 'utf8');
|
|
70
|
+
const { state, raw_bodies, raw_frontmatter, block_gaps, line_ending } =
|
|
71
|
+
parse(raw);
|
|
72
|
+
// Deep-clone so the consumer's fn cannot mutate the state we just
|
|
73
|
+
// parsed (defensive — apply() does this too for pure callers).
|
|
74
|
+
const clone = structuredClone(state);
|
|
75
|
+
const next = fn(clone);
|
|
76
|
+
const out = serialize(next, {
|
|
77
|
+
raw_frontmatter,
|
|
78
|
+
raw_bodies,
|
|
79
|
+
block_gaps,
|
|
80
|
+
line_ending,
|
|
81
|
+
});
|
|
82
|
+
writeFileSync(tmpPath, out, 'utf8');
|
|
83
|
+
try {
|
|
84
|
+
renameSync(tmpPath, path);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// Windows EPERM retry — AV / indexer holding STATE.md briefly.
|
|
87
|
+
const code =
|
|
88
|
+
typeof err === 'object' && err !== null && 'code' in err
|
|
89
|
+
? (err as { code?: unknown }).code
|
|
90
|
+
: undefined;
|
|
91
|
+
if (code === 'EPERM' || code === 'EBUSY') {
|
|
92
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
93
|
+
renameSync(tmpPath, path);
|
|
94
|
+
} else {
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return next;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// Clean up the orphaned tmp file on failure so we don't pollute.
|
|
101
|
+
try {
|
|
102
|
+
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
103
|
+
} catch {
|
|
104
|
+
// best-effort; a leftover tmp file does not corrupt STATE.md.
|
|
105
|
+
}
|
|
106
|
+
throw err;
|
|
107
|
+
} finally {
|
|
108
|
+
await release();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Advance to `toStage` under the locked RMW protocol.
|
|
114
|
+
*
|
|
115
|
+
* Steps:
|
|
116
|
+
* 1. Read current state (outside the lock) to pass to the gate.
|
|
117
|
+
* 2. Resolve the gate via `gateFor(position.stage, toStage)`.
|
|
118
|
+
* - `null` → TransitionGateFailed "Invalid transition" (skip-stage,
|
|
119
|
+
* backward, same-stage, or from outside the Stage union).
|
|
120
|
+
* 3. Invoke the gate. If `pass: false`, throw TransitionGateFailed with
|
|
121
|
+
* the gate's blockers verbatim.
|
|
122
|
+
* 4. If `pass: true`, mutate STATE.md under the lock:
|
|
123
|
+
* - frontmatter.stage = toStage
|
|
124
|
+
* - position.stage = toStage
|
|
125
|
+
* - frontmatter.last_checkpoint = now (ISO)
|
|
126
|
+
* - timestamps[`${toStage}_started_at`] = now (ISO)
|
|
127
|
+
*
|
|
128
|
+
* Returns the updated state plus the gate response (for callers that
|
|
129
|
+
* want to log blockers — on pass, `blockers` is always `[]`).
|
|
130
|
+
*/
|
|
131
|
+
export async function transition(
|
|
132
|
+
path: string,
|
|
133
|
+
toStage: Stage,
|
|
134
|
+
): Promise<TransitionResult> {
|
|
135
|
+
// Read (outside the lock) to pass current state to the gate — the
|
|
136
|
+
// mutate() below will re-read under the lock before applying changes.
|
|
137
|
+
// This two-phase pattern matches the GSD reference implementation.
|
|
138
|
+
const beforeMutate = await read(path);
|
|
139
|
+
const from: string = beforeMutate.position.stage;
|
|
140
|
+
// `position.stage` is typed as `string` in ParsedState (parser tolerates
|
|
141
|
+
// `scan` and other pre-brief values). Narrow it to `Stage` before asking
|
|
142
|
+
// the gate registry — anything outside the union is an invalid FROM.
|
|
143
|
+
if (!isStage(from)) {
|
|
144
|
+
throw new TransitionGateFailed(toStage, [
|
|
145
|
+
`Invalid transition: from="${from}" is not a recognized Stage`,
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
const gate = gateFor(from, toStage);
|
|
149
|
+
if (gate === null) {
|
|
150
|
+
throw new TransitionGateFailed(toStage, [
|
|
151
|
+
`Invalid transition: ${from} → ${toStage}`,
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
const gateResult = gate(beforeMutate);
|
|
155
|
+
if (!gateResult.pass) {
|
|
156
|
+
throw new TransitionGateFailed(toStage, gateResult.blockers);
|
|
157
|
+
}
|
|
158
|
+
const nowIso: string = new Date().toISOString();
|
|
159
|
+
const nextState = await mutate(path, (s): ParsedState => {
|
|
160
|
+
s.frontmatter.stage = toStage;
|
|
161
|
+
s.frontmatter.last_checkpoint = nowIso;
|
|
162
|
+
s.position.stage = toStage;
|
|
163
|
+
s.timestamps[`${toStage}_started_at`] = nowIso;
|
|
164
|
+
return s;
|
|
165
|
+
});
|
|
166
|
+
return { pass: true, blockers: gateResult.blockers, state: nextState };
|
|
167
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// sdk/state/types.ts — typed shape of a parsed STATE.md.
|
|
2
2
|
//
|
|
3
3
|
// Plan 20-01 (SDK-01/02): canonical type surface consumed by the parser,
|
|
4
4
|
// mutator, and public read/mutate/transition API. Everything here is
|
|
@@ -241,18 +241,18 @@ export interface TransitionResult extends GateResult {
|
|
|
241
241
|
|
|
242
242
|
// Error classes migrated to the unified GDDError taxonomy in Plan 20-04.
|
|
243
243
|
// Re-exported here so existing consumers (tests, downstream modules) keep
|
|
244
|
-
// importing from `
|
|
244
|
+
// importing from `sdk/state/types.ts` unchanged.
|
|
245
245
|
//
|
|
246
246
|
// * TransitionGateFailed — StateConflictError subclass; retryable
|
|
247
247
|
// * LockAcquisitionError — StateConflictError subclass; retryable
|
|
248
248
|
// * ParseError — ValidationError subclass; fix your STATE.md
|
|
249
249
|
//
|
|
250
|
-
// See `
|
|
250
|
+
// See `sdk/errors/index.ts` for the taxonomy definition.
|
|
251
251
|
export {
|
|
252
252
|
TransitionGateFailed,
|
|
253
253
|
LockAcquisitionError,
|
|
254
254
|
ParseError,
|
|
255
|
-
} from '../
|
|
255
|
+
} from '../errors/index.ts';
|
|
256
256
|
|
|
257
257
|
/** Type-guard for `Stage`. */
|
|
258
258
|
export function isStage(value: unknown): value is Stage {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gdd-figma-extract
|
|
3
|
+
description: Off-context Figma design-system extraction into a compact local digest (DESIGN.md + tokens.json + components.json). Pulls the file via the Figma REST API and digests it without the raw JSON ever entering the model context.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# gdd-figma-extract
|
|
7
|
+
|
|
8
|
+
Pull a whole Figma design system into a compact, queryable local digest — **without** the raw JSON ever entering Claude context. The heavy lifting runs in tested `.cjs` tools; the model reads only the digest outputs.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
/gdd:figma-extract <file-key-or-url> # full design-system digest
|
|
14
|
+
/gdd:figma-extract <file-key-or-url> --component Button # ~500-token single-component slice
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`<file-key-or-url>` is a Figma file URL (`https://www.figma.com/file/<key>/…` or `/design/<key>/…`) or a bare file key.
|
|
18
|
+
|
|
19
|
+
## Behavior
|
|
20
|
+
|
|
21
|
+
1. **Preflight (D-10).** Confirm `FIGMA_TOKEN` is set in the environment:
|
|
22
|
+
```
|
|
23
|
+
node -e "process.exit(process.env.FIGMA_TOKEN||process.env.FIGMA_PERSONAL_ACCESS_TOKEN?0:1)"
|
|
24
|
+
```
|
|
25
|
+
If unset, tell the user to `export FIGMA_TOKEN=figd_…` (from https://www.figma.com/developers/api#access-tokens). The token comes from the environment **only** — never ask the user to paste it into a file or the chat, and never echo it back.
|
|
26
|
+
|
|
27
|
+
2. **Stage 1 — pull.** Pull the file's REST endpoints into the gitignored raw cache (D-09):
|
|
28
|
+
```
|
|
29
|
+
node scripts/lib/figma-extract/pull.cjs "<file-key-or-url>"
|
|
30
|
+
```
|
|
31
|
+
This caches to `.figma-extract-cache/raw/<file-key>/` and skips re-pulling when Figma's `version` is unchanged (D-11). Add `--force` to bypass the cache, `--out <dir>` to relocate the cache. The tool prints a JSON summary (endpoints, bytes, cached) on stdout — the raw bodies stay on disk.
|
|
32
|
+
|
|
33
|
+
3. **Stage 2 — plugin sync (OPTIONAL, Path C).** Only when the design tokens live in Figma Variables that the REST API cannot return (non-Enterprise plans → the pull summary shows `variables` skipped) and the user wants token coverage:
|
|
34
|
+
```
|
|
35
|
+
node scripts/lib/figma-extract/receiver.cjs --out .figma-extract-cache/raw/<file-key>
|
|
36
|
+
```
|
|
37
|
+
This binds `127.0.0.1:5179` (D-06). Tell the user to run the dev-installed **"GDD Sync"** plugin in Figma and click **"Export to GDD"** — see `figma-plugin/README.md` for the one-time dev-install. The receiver writes `variables.json` into the cache and exits on receipt or timeout. Skip this stage entirely for design systems whose tokens already come through the REST pull.
|
|
38
|
+
|
|
39
|
+
4. **Stage 3 — digest.** Transform the cache into the compact digest:
|
|
40
|
+
```
|
|
41
|
+
node scripts/lib/figma-extract/digest.cjs --raw .figma-extract-cache/raw/<file-key> --out .figma-extract-cache/digest
|
|
42
|
+
```
|
|
43
|
+
This writes `DESIGN.md`, `tokens.json`, and `components.json`. Add `--prefer-styles` to invert the token priority to styles-first (D-04). Add `--component <name>` (D-08) to emit a single-component slice instead of the full digest.
|
|
44
|
+
|
|
45
|
+
5. **Read the digest.** Open **only** `.figma-extract-cache/digest/DESIGN.md` (plus `tokens.json` / `components.json` when you need structured data). For a single component, pass `--component <name>` in Stage 3 and read the ~500-token slice instead of the full ~16K-token spec.
|
|
46
|
+
|
|
47
|
+
## Required Reading
|
|
48
|
+
|
|
49
|
+
- `.figma-extract-cache/digest/DESIGN.md` — the compact human/LLM-readable spec
|
|
50
|
+
- `.figma-extract-cache/digest/tokens.json` — resolved design tokens (when structured token data is needed)
|
|
51
|
+
- `.figma-extract-cache/digest/components.json` — components with variants/props/defaults (when structured component data is needed)
|
|
52
|
+
|
|
53
|
+
## Notes
|
|
54
|
+
|
|
55
|
+
- Two-stage pipeline (D-01): re-run Stage 3 against an existing cache without re-pulling. The digest does zero network calls.
|
|
56
|
+
- The spike proved **0 Claude tokens** during extraction (898× compression, 223 MB → 254 KB). That property holds only because this skill never surfaces the raw cache.
|
|
57
|
+
- Figma MCP remains the right tool for spot questions on a single live component; this skill is the cheaper path for whole-design-system workflows.
|
|
58
|
+
|
|
59
|
+
## Do Not
|
|
60
|
+
|
|
61
|
+
- **Do NOT read or `cat` the `raw/*.json` cache** (e.g. `.figma-extract-cache/raw/<file-key>/file.json`). It is tool-internal and often 100+ MB; loading it into context defeats the off-context guarantee (D-12). Read only the digest outputs above.
|
|
62
|
+
- **Do not persist, log, echo, or print `FIGMA_TOKEN`** (D-10). It belongs in the environment only — never write it to a file, a commit, or chat output.
|
|
63
|
+
|
|
64
|
+
## FIGMA-EXTRACT COMPLETE
|
package/skills/health/SKILL.md
CHANGED
|
@@ -53,6 +53,16 @@ Health: 5 / 6 checks passing.
|
|
|
53
53
|
━━━━━━━━━━━━━━━━━━━━━
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## Figma-extract readiness (figma_extract)
|
|
57
|
+
|
|
58
|
+
After the health table, the `gdd_health` MCP surface (`scripts/lib/health-mirror/index.cjs`) reports a `figma_extract` check so a user knows whether figma-extract is usable. The detail is one of three exact strings:
|
|
59
|
+
|
|
60
|
+
- `figma extract: ready (token set)` — `FIGMA_TOKEN` (or `FIGMA_PERSONAL_ACCESS_TOKEN`) is present (status `ok`).
|
|
61
|
+
- `figma extract: token missing` — no token env is set (status `warn`).
|
|
62
|
+
- `figma extract: plugin sync needed for variables (Free tier detected)` — token present but a prior pull recorded a 403/skip on the Variables REST path, so run the plugin-sync step (status `warn`).
|
|
63
|
+
|
|
64
|
+
Token PRESENCE only is detected (D-10) — the token value is never read, logged, or shown. The Free-tier signal is read from the local raw-pull cache only; no network call is made.
|
|
65
|
+
|
|
56
66
|
## Check MCP registration (gdd-mcp)
|
|
57
67
|
|
|
58
68
|
After the health table, inspect whether `gdd-mcp` (Phase 27.7+) is registered with any installed harness and render a one-line status row. Dismissable via `.design/config.json#mcp_nudge=false`. Non-blocking: failure paths render `MCP server: unknown` rather than crash. Full detection procedure (dismissal check, detection via `scripts/lib/install/mcp-register.cjs`, row rendering for claude/codex/both/neither, fallback) lives in `./health-mcp-detection.md`.
|
|
@@ -58,11 +58,11 @@ Else: increment `iteration`, emit `quality_gate_iteration`, spawn `design-fixer`
|
|
|
58
58
|
|
|
59
59
|
## Step 5 — STATE write
|
|
60
60
|
|
|
61
|
-
Mutate `state.quality_gate.run` to `{started_at, completed_at, status, iteration, commands_run, extra_attrs:{}}`. Persist via `mcp__gdd_state__set_quality_gate` or `apply()` mutator from `
|
|
61
|
+
Mutate `state.quality_gate.run` to `{started_at, completed_at, status, iteration, commands_run, extra_attrs:{}}`. Persist via `mcp__gdd_state__set_quality_gate` or `apply()` mutator from `sdk/state/mutator.ts` — identical on-disk shape.
|
|
62
62
|
|
|
63
63
|
## Step 6 — Event emission (D-09)
|
|
64
64
|
|
|
65
|
-
Use `appendEvent` from `
|
|
65
|
+
Use `appendEvent` from `sdk/event-stream/index.ts` — persist-first / broadcast-second; never throws on persist path. `ts` / `cycle` / `stage` are stamped by the writer. Six event types (one per lifecycle position):
|
|
66
66
|
|
|
67
67
|
| Event | When | Payload |
|
|
68
68
|
|-------|------|---------|
|