@hegemonart/get-design-done 1.60.2 → 1.60.3

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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Get Design Done — 5-stage agent-orchestrated design pipeline (Brief → Explore → Plan → Design → Verify) for AI coding agents. 64 agents, 96 skills, 39 connection integrations, two MCP servers, opt-in SQLite state backbone, bidirectional Figma write-back, and a reflector-driven self-improvement loop. Cross-runtime install for Claude Code, Codex, Cursor, OpenCode, Gemini, and more.",
8
- "version": "1.60.2"
8
+ "version": "1.60.3"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "get-design-done",
13
13
  "source": "./",
14
14
  "description": "Agent-orchestrated 5-stage design pipeline (Brief → Explore → Plan → Design → Verify) for AI coding agents. 64 specialized agents, 96 skills, 39 connection integrations (Figma, Refero, Preview, Storybook, Chromatic, Graphify, Linear, Jira, Notion, …), bidirectional Figma write-back, queryable intel store, opt-in SQLite state backbone, and a reflector-driven self-improvement loop. Two MCP servers (gdd-state for typed STATE mutators, gdd-mcp for 13 read-only project-priming tools), tier-aware routing with cost telemetry, and defense-in-depth hooks (protected paths, MCP circuit breaker, injection scanner, budget enforcer). Cross-runtime install for Claude Code, Codex, Cursor, OpenCode, Gemini, Copilot, and more.",
15
- "version": "1.60.2",
15
+ "version": "1.60.3",
16
16
  "author": {
17
17
  "name": "hegemonart"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "get-design-done",
3
3
  "short_name": "gdd",
4
- "version": "1.60.2",
4
+ "version": "1.60.3",
5
5
  "description": "Agent-orchestrated 5-stage design pipeline (Brief → Explore → Plan → Design → Verify) for AI coding agents. 64 specialized agents, 96 skills, 39 connection integrations (Figma, Refero, Preview, Storybook, Chromatic, Graphify, Linear, Jira, Notion, …), bidirectional Figma write-back, queryable intel store for O(1) design-surface lookups, opt-in SQLite state backbone, and a reflector-driven self-improvement loop. Two MCP servers (`gdd-state` for typed STATE mutators, `gdd-mcp` for 13 read-only project-priming tools), tier-aware agent routing with cost telemetry, defense-in-depth hooks (protected paths, MCP circuit breaker, injection scanner, budget enforcer), and a cross-runtime install layer for Claude Code, Codex, Cursor, OpenCode, Gemini, Copilot, and more.",
6
6
  "author": {
7
7
  "name": "hegemonart",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to get-design-done are documented here. Versions follow [sem
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.60.3] - 2026-07-06
8
+
9
+ **Multi-runtime install fix** - the Claude-only `model:` frontmatter directive no longer leaks into the command files generated for non-Claude runtimes. Running any `/gdd-*` command on Kilo Code (Qwen) crashed with `Model not found: inherit/.` because the installer round-tripped `model: inherit` verbatim into the command file and Kilo parsed the value as a literal model id.
10
+
11
+ ### Fixed
12
+
13
+ - **`model:` frontmatter stripped from non-Claude command files** (`scripts/lib/install/converters/shared.cjs`). `buildFrontmatter` now drops the `model:` line when emitting artifacts for the 12 non-Claude command-format runtimes (Kilo, Qwen, OpenCode, Gemini, Cursor, Codex, Copilot, Antigravity, Augment, CodeBuddy, Windsurf, Trae). `model: inherit` is a Claude-Code directive meaning "defer to the session model"; it - and the Claude tier names `opus` / `sonnet` / `haiku` - are not valid model ids on any other runtime, so Kilo read `inherit` as `<provider>/<model>` (`inherit/`) and failed with `Model not found: inherit/.`. Cross-runtime model selection remains the job of `default-tier` / `reasoning-class` + `tier-resolver.cjs`, which round-trip untouched; sibling keys such as `default-tier:` and `model-notes:` are preserved. Agents are Claude-local-only, so no agent artifact was affected. Regression coverage added in `test/suite/converters-wave4.test.cjs` (Kilo / OpenCode / Gemini integration plus a direct `buildFrontmatter` unit).
14
+
15
+ ### Breaking changes
16
+
17
+ None.
18
+
19
+ 5,145/5,145 tests pass.
20
+
21
+ ---
22
+
7
23
  ## [1.60.2] - 2026-06-13
8
24
 
9
25
  **Security & CI hardening** - bring the SAST/dependency-audit gates the project lacked, and close the one untrusted-link gap in the injection scanner, *before* the detection engine lands its large new surface. Sourced from a reconciliation against the upstream framework's recent releases (`.planning/audits/UPSTREAM-GSD-CORE-DIFF-2026-06-13.md`).
@@ -148,11 +148,16 @@ export function stateFileHasPausedBlock(): boolean {
148
148
  }
149
149
 
150
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');
151
+ // mkdir recursive is idempotent — no existsSync check needed.
152
+ mkdirSync(dirname(STATE_PATH), { recursive: true });
153
+ // Seed the header only when the file is new, atomically via the 'wx' flag:
154
+ // an EEXIST means STATE.md already exists, so we skip the header and append
155
+ // to it. This collapses the existsSync→writeFileSync/appendFileSync TOCTOU
156
+ // race that previously sat between the check and the writes.
157
+ try {
158
+ writeFileSync(STATE_PATH, '# Design State\n\n', { encoding: 'utf8', flag: 'wx' });
159
+ } catch (e) {
160
+ if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;
156
161
  }
157
162
  appendFileSync(STATE_PATH, block, 'utf8');
158
163
  }
@@ -197,9 +197,24 @@ function appendJsonl(filePath, row) {
197
197
 
198
198
  function appendStateBlocker(cwd, message) {
199
199
  const statePath = path.join(cwd, '.design', 'STATE.md');
200
- if (!fs.existsSync(statePath)) return; // silent if STATE missing
201
200
  const line = `\n<!-- mcp-circuit-breaker: ${new Date().toISOString()} --> 🛑 BLOCKER: ${message}\n`;
202
- try { fs.appendFileSync(statePath, line, 'utf8'); } catch { /* best-effort */ }
201
+ // Open with 'r+' (no-create) so we append ONLY to an already-existing STATE
202
+ // and never create it — opening fails with ENOENT when STATE is missing,
203
+ // which we swallow as "silent if STATE missing". This collapses the old
204
+ // existsSync→appendFileSync TOCTOU race into a single atomic open.
205
+ let fd;
206
+ try {
207
+ fd = fs.openSync(statePath, 'r+');
208
+ } catch {
209
+ return; // STATE missing (ENOENT) or otherwise unopenable — best-effort, stay silent
210
+ }
211
+ try {
212
+ fs.writeSync(fd, line, fs.fstatSync(fd).size, 'utf8');
213
+ } catch {
214
+ /* best-effort */
215
+ } finally {
216
+ try { fs.closeSync(fd); } catch { /* best-effort */ }
217
+ }
203
218
  }
204
219
 
205
220
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.60.2",
3
+ "version": "1.60.3",
4
4
  "description": "A design-quality pipeline for AI coding agents: brief, explore, plan, design, and verify UI work against your design system.",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -29,7 +29,7 @@
29
29
  //
30
30
  // Default --path is `.design/telemetry/events.jsonl` (relative to cwd).
31
31
 
32
- import { existsSync, statSync, openSync, readSync, closeSync } from 'node:fs';
32
+ import { openSync, readSync, closeSync, fstatSync } from 'node:fs';
33
33
  import { resolve, isAbsolute } from 'node:path';
34
34
  import { pathToFileURL } from 'node:url';
35
35
  import { argv, exit, stdout, stderr } from 'node:process';
@@ -146,23 +146,41 @@ async function cmdTail(parsed) {
146
146
  return 0;
147
147
  }
148
148
  // Follow mode: stream existing content, then poll for appends.
149
+ // Read directly and treat ENOENT as "file not present yet" (offset stays 0,
150
+ // the poll loop below picks it up once it appears) — avoids the
151
+ // existsSync→read/statSync TOCTOU race.
149
152
  let offset = 0;
150
- if (existsSync(path)) {
153
+ try {
151
154
  for await (const ev of readEvents({ path })) {
152
155
  stdout.write(JSON.stringify(ev) + '\n');
153
156
  }
154
- offset = statSync(path).size;
157
+ // Capture the EOF offset from a short-lived fd (open+fstat) instead of
158
+ // statSync(path): keeping every size check on a descriptor — never on the
159
+ // path — leaves no stat→open pair for the poll loop below to race on.
160
+ const fd0 = openSync(path, 'r');
161
+ try { offset = fstatSync(fd0).size; } finally { closeSync(fd0); }
162
+ } catch (e) {
163
+ if (e.code !== 'ENOENT') throw e;
164
+ offset = 0;
155
165
  }
156
166
  // Poll loop. Reads new bytes since last offset, splits on \n, writes each.
157
167
  let buf = '';
158
168
  // eslint-disable-next-line no-constant-condition
159
169
  while (true) {
160
170
  await new Promise((r) => setTimeout(r, 250));
161
- if (!existsSync(path)) continue;
162
- const size = statSync(path).size;
163
- if (size <= offset) continue;
164
- const fd = openSync(path, 'r');
171
+ // Open the file first and fstat the handle — both the size check and the
172
+ // read target the descriptor, not the path, so there is no stat→open TOCTOU
173
+ // window. ENOENT means the file isn't there this tick: skip and re-poll.
174
+ let fd;
175
+ try {
176
+ fd = openSync(path, 'r');
177
+ } catch (e) {
178
+ if (e.code === 'ENOENT') continue;
179
+ throw e;
180
+ }
165
181
  try {
182
+ const size = fstatSync(fd).size;
183
+ if (size <= offset) continue;
166
184
  const need = size - offset;
167
185
  const chunk = Buffer.allocUnsafe(need);
168
186
  const n = readSync(fd, chunk, 0, need, offset);
@@ -202,7 +202,15 @@ function modeForward(check) {
202
202
  }
203
203
 
204
204
  function modeExtract() {
205
- const existing = fs.existsSync(SKILLS_JSON) ? readSkillsJson() : { schema_version: 1, skills: [] };
205
+ // Read directly and treat a missing skills.json as the empty-default seed
206
+ // avoids the existsSync→readFileSync TOCTOU race.
207
+ let existing;
208
+ try {
209
+ existing = readSkillsJson();
210
+ } catch (e) {
211
+ if (e.code !== 'ENOENT') throw e;
212
+ existing = { schema_version: 1, skills: [] };
213
+ }
206
214
  const prevMap = recordMap(existing);
207
215
  const dirs = listSkillDirs();
208
216
  const skills = [];
@@ -23,6 +23,10 @@ const DEFAULT_ARMS_PATH = '.design/telemetry/design-arms.json';
23
23
  // user-outcome data shifts it (D-03 — advisory, never directive).
24
24
  const DESIGN_ARM_PRIOR = Object.freeze({ alpha: 2, beta: 8 });
25
25
 
26
+ // Monotonic per-process counter so two saves within the same millisecond still get
27
+ // distinct tmp names (no `crypto` — this module is "no crypto, no egress").
28
+ let _tmpSeq = 0;
29
+
26
30
  /** Inline FNV-1a (32-bit) hash → 8-char hex. Deterministic, dependency-free. */
27
31
  function fnv1a(str) {
28
32
  let h = 0x811c9dc5;
@@ -62,7 +66,10 @@ function save(store, opts = {}) {
62
66
  fs.mkdirSync(path.dirname(p), { recursive: true });
63
67
  store.schema_version = SCHEMA_VERSION;
64
68
  store.generated_at = new Date().toISOString();
65
- const tmp = p + '.tmp';
69
+ // Unique per-writer tmp name: prevents symlink/race attacks on a static `.tmp`
70
+ // path AND stops concurrent writers from clobbering each other's tmp file before
71
+ // the atomic rename. pid + time + counter — no crypto (see module header).
72
+ const tmp = `${p}.${process.pid}.${Date.now()}.${_tmpSeq++}.tmp`;
66
73
  fs.writeFileSync(tmp, JSON.stringify(store, null, 2));
67
74
  fs.renameSync(tmp, p);
68
75
  }
@@ -316,6 +316,10 @@ function ensureAdapterHeader(body, runtimeDisplay) {
316
316
  * `gsd-` prefix on the existing name to avoid `gdd-gdd-`-style
317
317
  * duplication). All other fields round-trip verbatim — we do NOT
318
318
  * parse YAML; we operate on the raw text with a line-by-line scan.
319
+ * - EXCEPTION: the Claude-Code-only `model:` line is dropped. Its
320
+ * values (`inherit` / a Claude tier name) are invalid model ids on
321
+ * non-Claude runtimes and crash their command loaders (Kilo:
322
+ * `Model not found: inherit/.`). See the inline note below.
319
323
  * - If the original has no `name:` field, prepend one.
320
324
  *
321
325
  * Returns a complete frontmatter string with leading/trailing `---`
@@ -339,8 +343,24 @@ function buildFrontmatter(originalFrontmatter, skillName, runtimePrefix) {
339
343
  }
340
344
 
341
345
  // Line-by-line rewrite of the `name:` field. We never touch description,
342
- // tools, or any other field — they round-trip verbatim.
343
- const lines = originalFrontmatter.split(/\r?\n/);
346
+ // tools, or any other field — they round-trip verbatim, with ONE
347
+ // exception: the Claude-Code-only `model:` directive is dropped.
348
+ //
349
+ // `model:` is a Claude harness directive ("defer to the session model" for
350
+ // `inherit`, or pin a Claude tier for `opus`/`sonnet`/`haiku`). None of
351
+ // those values are valid model ids on the non-Claude runtimes this
352
+ // converter targets. Kilo (and other command-runners) read the `model:`
353
+ // frontmatter and parse the value as a literal `<provider>/<model>` — so
354
+ // `model: inherit` becomes `inherit/` (empty model) and the command dies
355
+ // with `Model not found: inherit/.`. Cross-runtime model selection is the
356
+ // job of `default-tier`/`reasoning-class` + tier-resolver.cjs, NOT this
357
+ // per-command harness directive, so we strip the whole line here.
358
+ //
359
+ // The `^\s*model\s*:` anchor matches only the exact `model` key — sibling
360
+ // keys like `model-notes:` or `default-tier:` are preserved.
361
+ const lines = originalFrontmatter
362
+ .split(/\r?\n/)
363
+ .filter((line) => !/^\s*model\s*:/.test(line));
344
364
  let nameSeen = false;
345
365
  for (let i = 0; i < lines.length; i++) {
346
366
  const m = lines[i].match(/^(\s*name\s*:\s*)(.*)$/);
@@ -31,20 +31,29 @@ function load(name, opts) {
31
31
  const fallback = Object.prototype.hasOwnProperty.call(o, 'fallback') ? o.fallback : {};
32
32
  const abs = path.join(dir, `${name}.json`);
33
33
 
34
- let stat;
35
- try { stat = fs.statSync(abs); } catch {
34
+ // Open once and operate on the file descriptor (fstat for the mtime-cache key,
35
+ // then read from the same fd). A single handle resolves the path exactly once,
36
+ // at open, so there is no statSync→readFileSync TOCTOU window.
37
+ let fd;
38
+ try { fd = fs.openSync(abs, 'r'); } catch {
36
39
  if (!o.quiet) process.stderr.write(`manifest: ${name}.json not found — using empty fallback (a consumer phase may not have shipped its data yet)\n`);
37
40
  return fallback;
38
41
  }
39
- const cached = _cache.get(abs);
40
- if (cached && cached.mtimeMs === stat.mtimeMs) return cached.data;
41
42
  try {
42
- const data = JSON.parse(fs.readFileSync(abs, 'utf8'));
43
- _cache.set(abs, { mtimeMs: stat.mtimeMs, data });
44
- return data;
45
- } catch (e) {
46
- if (!o.quiet) process.stderr.write(`manifest: ${name}.json parse error (${e.message}) — using empty fallback\n`);
47
- return fallback;
43
+ const stat = fs.fstatSync(fd);
44
+ const cached = _cache.get(abs);
45
+ if (cached && cached.mtimeMs === stat.mtimeMs) return cached.data;
46
+ const raw = fs.readFileSync(fd, 'utf8');
47
+ try {
48
+ const data = JSON.parse(raw);
49
+ _cache.set(abs, { mtimeMs: stat.mtimeMs, data });
50
+ return data;
51
+ } catch (e) {
52
+ if (!o.quiet) process.stderr.write(`manifest: ${name}.json parse error (${e.message}) — using empty fallback\n`);
53
+ return fallback;
54
+ }
55
+ } finally {
56
+ fs.closeSync(fd);
48
57
  }
49
58
  }
50
59
 
@@ -425,11 +425,16 @@ function applyReject(draftPath, _options) {
425
425
  */
426
426
  function applyDefer(draftPath, options) {
427
427
  const opts = options || {};
428
- if (!fs.existsSync(draftPath)) {
429
- throw new Error(`KFM draft not found: ${draftPath}`);
430
- }
431
428
  const deferredUntil = opts.deferredUntil || new Date(Date.now() + 30 * 86_400_000).toISOString().slice(0, 10);
432
- const orig = fs.readFileSync(draftPath, 'utf8');
429
+ // Read directly and treat ENOENT as "draft not found" — avoids the
430
+ // existsSync→readFileSync TOCTOU race.
431
+ let orig;
432
+ try {
433
+ orig = fs.readFileSync(draftPath, 'utf8');
434
+ } catch (e) {
435
+ if (e.code === 'ENOENT') throw new Error(`KFM draft not found: ${draftPath}`);
436
+ throw e;
437
+ }
433
438
  let updated;
434
439
  if (/^deferred_until:/m.test(orig)) {
435
440
  updated = orig.replace(/^deferred_until:.*$/m, `deferred_until: ${deferredUntil}`);
@@ -73,7 +73,7 @@ const FILE_SENSITIVITY = Object.freeze([
73
73
  // De-risking: tests + fixtures are low-stakes.
74
74
  { test: /(^|\/)(tests?|fixtures?|__tests__|__fixtures__)\//i, mult: 0.6, add: 0, label: 'test-or-fixture' },
75
75
  // De-risking: docs / markdown.
76
- { test: /(^|\/)docs?\/|\.mdx?$/i, mult: 0.5, add: 0, label: 'docs' },
76
+ { test: /(?:(?:^|\/)docs?\/)|(?:\.mdx?$)/i, mult: 0.5, add: 0, label: 'docs' },
77
77
  ]);
78
78
 
79
79
  // ── Severity -> addend for destructive bash (via dangerous-patterns.cjs) ────
package/sdk/cli/index.js CHANGED
@@ -5,11 +5,20 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
10
15
  };
11
16
  var __commonJS = (cb, mod) => function __require() {
12
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ try {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ } catch (e) {
20
+ throw mod = 0, e;
21
+ }
13
22
  };
14
23
  var __export = (target, all) => {
15
24
  for (var name in all)
@@ -7,7 +7,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
9
  var __commonJS = (cb, mod) => function __require() {
10
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ try {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ } catch (e) {
13
+ throw mod = 0, e;
14
+ }
11
15
  };
12
16
  var __export = (target, all) => {
13
17
  for (var name14 in all)
@@ -428,24 +432,30 @@ var require_loader = __commonJS({
428
432
  const dir = o.dir || MANIFEST_DIR;
429
433
  const fallback = Object.prototype.hasOwnProperty.call(o, "fallback") ? o.fallback : {};
430
434
  const abs = path.join(dir, `${name14}.json`);
431
- let stat;
435
+ let fd;
432
436
  try {
433
- stat = fs.statSync(abs);
437
+ fd = fs.openSync(abs, "r");
434
438
  } catch {
435
439
  if (!o.quiet) process.stderr.write(`manifest: ${name14}.json not found \u2014 using empty fallback (a consumer phase may not have shipped its data yet)
436
440
  `);
437
441
  return fallback;
438
442
  }
439
- const cached = _cache.get(abs);
440
- if (cached && cached.mtimeMs === stat.mtimeMs) return cached.data;
441
443
  try {
442
- const data = JSON.parse(fs.readFileSync(abs, "utf8"));
443
- _cache.set(abs, { mtimeMs: stat.mtimeMs, data });
444
- return data;
445
- } catch (e) {
446
- if (!o.quiet) process.stderr.write(`manifest: ${name14}.json parse error (${e.message}) \u2014 using empty fallback
444
+ const stat = fs.fstatSync(fd);
445
+ const cached = _cache.get(abs);
446
+ if (cached && cached.mtimeMs === stat.mtimeMs) return cached.data;
447
+ const raw = fs.readFileSync(fd, "utf8");
448
+ try {
449
+ const data = JSON.parse(raw);
450
+ _cache.set(abs, { mtimeMs: stat.mtimeMs, data });
451
+ return data;
452
+ } catch (e) {
453
+ if (!o.quiet) process.stderr.write(`manifest: ${name14}.json parse error (${e.message}) \u2014 using empty fallback
447
454
  `);
448
- return fallback;
455
+ return fallback;
456
+ }
457
+ } finally {
458
+ fs.closeSync(fd);
449
459
  }
450
460
  }
451
461
  module2.exports = { load: load2, reset, MANIFEST_DIR };