@hegemonart/get-design-done 1.60.2 → 1.60.4
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +32 -0
- package/hooks/context-exhaustion.ts +10 -5
- package/hooks/gdd-mcp-circuit-breaker.js +17 -2
- package/package.json +1 -1
- package/scripts/cli/gdd-events.mjs +25 -7
- package/scripts/generate-skill-frontmatter.cjs +9 -1
- package/scripts/lib/ds-arms/design-arms-store.cjs +8 -1
- package/scripts/lib/install/converters/codex-plugin.cjs +18 -1
- package/scripts/lib/install/converters/cursor-marketplace.cjs +29 -8
- package/scripts/lib/install/converters/shared.cjs +74 -2
- package/scripts/lib/manifest/loader.cjs +19 -10
- package/scripts/lib/reflector-kfm-proposer.cjs +9 -4
- package/scripts/lib/risk/tables.cjs +1 -1
- package/sdk/cli/index.js +12 -3
- package/sdk/mcp/gdd-mcp/server.js +21 -11
|
@@ -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.
|
|
8
|
+
"version": "1.60.4"
|
|
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.
|
|
15
|
+
"version": "1.60.4",
|
|
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.
|
|
4
|
+
"version": "1.60.4",
|
|
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,38 @@ All notable changes to get-design-done are documented here. Versions follow [sem
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [1.60.4] - 2026-07-06
|
|
8
|
+
|
|
9
|
+
**Multi-runtime install fix (Tier-2 bundles)** - follow-up to 1.60.3. The same Claude-only `model:` frontmatter directive is now stripped from the Tier-2 marketplace distribution bundles, not just the Tier-1 file-drop command files.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`model:` frontmatter stripped from Tier-2 marketplace bundles** (`scripts/lib/install/converters/codex-plugin.cjs`, `scripts/lib/install/converters/cursor-marketplace.cjs`). These converters copied the `skills/` tree verbatim, so `model: inherit` still leaked into the Codex-plugin and Cursor-marketplace bundles and would crash non-Claude consumers exactly as the file-drop path did before 1.60.3 (`Model not found: inherit/.`). A new shared helper `stripModelFromFrontmatter` drops the `model:` line from each `SKILL.md` frontmatter during the copy; files that carry no `model:` line are still copied byte-for-byte, so the verbatim-copy guarantee holds for everything else. `buildFrontmatter` and the new helper share a single `MODEL_FIELD_RE` so the Tier-1 and Tier-2 paths strip identically. Regression coverage in `test/suite/converters/codex-plugin.test.cjs`, `test/suite/converters/cursor-marketplace.test.cjs`, and a `stripModelFromFrontmatter` unit in `test/suite/converters-wave4.test.cjs`.
|
|
14
|
+
|
|
15
|
+
### Breaking changes
|
|
16
|
+
|
|
17
|
+
None.
|
|
18
|
+
|
|
19
|
+
5,148/5,148 tests pass.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [1.60.3] - 2026-07-06
|
|
24
|
+
|
|
25
|
+
**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.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **`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).
|
|
30
|
+
|
|
31
|
+
### Breaking changes
|
|
32
|
+
|
|
33
|
+
None.
|
|
34
|
+
|
|
35
|
+
5,145/5,145 tests pass.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
7
39
|
## [1.60.2] - 2026-06-13
|
|
8
40
|
|
|
9
41
|
**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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.60.4",
|
|
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 {
|
|
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
|
-
|
|
153
|
+
try {
|
|
151
154
|
for await (const ev of readEvents({ path })) {
|
|
152
155
|
stdout.write(JSON.stringify(ev) + '\n');
|
|
153
156
|
}
|
|
154
|
-
offset
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
const fs = require('node:fs');
|
|
46
46
|
const path = require('node:path');
|
|
47
|
+
const shared = require('./shared.cjs');
|
|
47
48
|
|
|
48
49
|
// Per research § Top-level fields: name, version, description are the only
|
|
49
50
|
// strictly-required spec fields. All other manifest fields are optional.
|
|
@@ -112,6 +113,12 @@ function curateKeywords(arr) {
|
|
|
112
113
|
/**
|
|
113
114
|
* Copy a directory tree recursively. Vanilla fs only — no deps. Mirrors
|
|
114
115
|
* the helper used by cursor-marketplace.cjs (Plan 28-8-B1).
|
|
116
|
+
*
|
|
117
|
+
* `SKILL.md` files are sanitized in transit: the Claude-only `model:`
|
|
118
|
+
* frontmatter directive is stripped so non-Claude marketplace consumers do
|
|
119
|
+
* not choke on `model: inherit` (Kilo: `Model not found: inherit/.`). Files
|
|
120
|
+
* that carry no `model:` line are copied byte-for-byte (copyFileSync), so the
|
|
121
|
+
* verbatim-copy guarantee still holds for everything else.
|
|
115
122
|
*/
|
|
116
123
|
function copyDirRecursive(src, dest) {
|
|
117
124
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -121,7 +128,17 @@ function copyDirRecursive(src, dest) {
|
|
|
121
128
|
if (entry.isDirectory()) {
|
|
122
129
|
copyDirRecursive(srcPath, destPath);
|
|
123
130
|
} else if (entry.isFile()) {
|
|
124
|
-
|
|
131
|
+
if (entry.name === 'SKILL.md') {
|
|
132
|
+
const original = fs.readFileSync(srcPath, 'utf8');
|
|
133
|
+
const stripped = shared.stripModelFromFrontmatter(original);
|
|
134
|
+
if (stripped === original) {
|
|
135
|
+
fs.copyFileSync(srcPath, destPath); // byte-exact, no re-encode
|
|
136
|
+
} else {
|
|
137
|
+
fs.writeFileSync(destPath, stripped, 'utf8');
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
fs.copyFileSync(srcPath, destPath);
|
|
141
|
+
}
|
|
125
142
|
}
|
|
126
143
|
// symlinks + other: ignored (skills tree is regular files only).
|
|
127
144
|
}
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
const fs = require('node:fs');
|
|
36
36
|
const path = require('node:path');
|
|
37
|
+
const shared = require('./shared.cjs');
|
|
37
38
|
|
|
38
39
|
// Curated keyword subset for Cursor's marketplace card display.
|
|
39
40
|
// Per Wave A research § Schema Mapping `keywords` row: marketplace card
|
|
@@ -201,6 +202,12 @@ function buildManifest(sources, opts) {
|
|
|
201
202
|
/**
|
|
202
203
|
* Copy a directory tree recursively. Vanilla fs only — no deps.
|
|
203
204
|
* Returns the list of relative paths written (relative to `dest`).
|
|
205
|
+
*
|
|
206
|
+
* `SKILL.md` files are sanitized in transit: the Claude-only `model:`
|
|
207
|
+
* frontmatter directive is stripped so non-Claude marketplace consumers do
|
|
208
|
+
* not choke on `model: inherit` (Kilo: `Model not found: inherit/.`). Files
|
|
209
|
+
* that carry no `model:` line are copied byte-for-byte (copyFileSync), so the
|
|
210
|
+
* verbatim-copy guarantee still holds for everything else.
|
|
204
211
|
*/
|
|
205
212
|
function copyDirRecursive(src, dest, relPrefix) {
|
|
206
213
|
const written = [];
|
|
@@ -208,16 +215,30 @@ function copyDirRecursive(src, dest, relPrefix) {
|
|
|
208
215
|
while (stack.length > 0) {
|
|
209
216
|
const { s, d, rel } = stack.pop();
|
|
210
217
|
fs.mkdirSync(d, { recursive: true });
|
|
211
|
-
|
|
218
|
+
// Use withFileTypes so dir/file classification comes from the single
|
|
219
|
+
// readdir syscall (the dirent), not a follow-up statSync — this collapses
|
|
220
|
+
// the check-then-use file race (CodeQL js/file-system-race) between the
|
|
221
|
+
// stat and the readFileSync/copyFileSync. Mirrors codex-plugin.cjs.
|
|
222
|
+
const entries = fs.readdirSync(s, { withFileTypes: true });
|
|
212
223
|
for (const entry of entries) {
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
224
|
+
const name = entry.name;
|
|
225
|
+
const sp = path.join(s, name);
|
|
226
|
+
const dp = path.join(d, name);
|
|
227
|
+
const relPath = rel ? `${rel}/${name}` : name;
|
|
228
|
+
if (entry.isDirectory()) {
|
|
218
229
|
stack.push({ s: sp, d: dp, rel: relPath });
|
|
219
|
-
} else if (
|
|
220
|
-
|
|
230
|
+
} else if (entry.isFile()) {
|
|
231
|
+
if (name === 'SKILL.md') {
|
|
232
|
+
const original = fs.readFileSync(sp, 'utf8');
|
|
233
|
+
const stripped = shared.stripModelFromFrontmatter(original);
|
|
234
|
+
if (stripped === original) {
|
|
235
|
+
fs.copyFileSync(sp, dp); // byte-exact, no re-encode
|
|
236
|
+
} else {
|
|
237
|
+
fs.writeFileSync(dp, stripped, 'utf8');
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
fs.copyFileSync(sp, dp);
|
|
241
|
+
}
|
|
221
242
|
written.push(relPath);
|
|
222
243
|
}
|
|
223
244
|
// symlinks + other: ignored (skills tree is regular files only)
|
|
@@ -101,6 +101,21 @@ const CODEX_TOOL_MAP = Object.freeze({
|
|
|
101
101
|
WebFetch: 'shell',
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// MODEL_FIELD_RE — the Claude-only `model:` frontmatter key
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Matches a `model:` frontmatter line (optional leading whitespace, exact key).
|
|
110
|
+
* The `model:` directive is Claude-Code-only: its values (`inherit`, or a
|
|
111
|
+
* Claude tier name `opus`/`sonnet`/`haiku`) are not valid model ids on any
|
|
112
|
+
* non-Claude runtime, whose command loaders parse the value as a literal
|
|
113
|
+
* `<provider>/<model>` and fail (Kilo: `Model not found: inherit/.`). Every
|
|
114
|
+
* converter that emits artifacts for a non-Claude consumer strips this line.
|
|
115
|
+
* Anchored to the exact key so `default-tier:` / `model-notes:` never match.
|
|
116
|
+
*/
|
|
117
|
+
const MODEL_FIELD_RE = /^\s*model\s*:/;
|
|
118
|
+
|
|
104
119
|
// ---------------------------------------------------------------------------
|
|
105
120
|
// extractFrontmatterAndBody — YAML frontmatter parser
|
|
106
121
|
// ---------------------------------------------------------------------------
|
|
@@ -316,6 +331,10 @@ function ensureAdapterHeader(body, runtimeDisplay) {
|
|
|
316
331
|
* `gsd-` prefix on the existing name to avoid `gdd-gdd-`-style
|
|
317
332
|
* duplication). All other fields round-trip verbatim — we do NOT
|
|
318
333
|
* parse YAML; we operate on the raw text with a line-by-line scan.
|
|
334
|
+
* - EXCEPTION: the Claude-Code-only `model:` line is dropped. Its
|
|
335
|
+
* values (`inherit` / a Claude tier name) are invalid model ids on
|
|
336
|
+
* non-Claude runtimes and crash their command loaders (Kilo:
|
|
337
|
+
* `Model not found: inherit/.`). See the inline note below.
|
|
319
338
|
* - If the original has no `name:` field, prepend one.
|
|
320
339
|
*
|
|
321
340
|
* Returns a complete frontmatter string with leading/trailing `---`
|
|
@@ -339,8 +358,24 @@ function buildFrontmatter(originalFrontmatter, skillName, runtimePrefix) {
|
|
|
339
358
|
}
|
|
340
359
|
|
|
341
360
|
// Line-by-line rewrite of the `name:` field. We never touch description,
|
|
342
|
-
// tools, or any other field — they round-trip verbatim
|
|
343
|
-
|
|
361
|
+
// tools, or any other field — they round-trip verbatim, with ONE
|
|
362
|
+
// exception: the Claude-Code-only `model:` directive is dropped.
|
|
363
|
+
//
|
|
364
|
+
// `model:` is a Claude harness directive ("defer to the session model" for
|
|
365
|
+
// `inherit`, or pin a Claude tier for `opus`/`sonnet`/`haiku`). None of
|
|
366
|
+
// those values are valid model ids on the non-Claude runtimes this
|
|
367
|
+
// converter targets. Kilo (and other command-runners) read the `model:`
|
|
368
|
+
// frontmatter and parse the value as a literal `<provider>/<model>` — so
|
|
369
|
+
// `model: inherit` becomes `inherit/` (empty model) and the command dies
|
|
370
|
+
// with `Model not found: inherit/.`. Cross-runtime model selection is the
|
|
371
|
+
// job of `default-tier`/`reasoning-class` + tier-resolver.cjs, NOT this
|
|
372
|
+
// per-command harness directive, so we strip the whole line here.
|
|
373
|
+
//
|
|
374
|
+
// `MODEL_FIELD_RE` matches only the exact `model` key — sibling keys like
|
|
375
|
+
// `model-notes:` or `default-tier:` are preserved.
|
|
376
|
+
const lines = originalFrontmatter
|
|
377
|
+
.split(/\r?\n/)
|
|
378
|
+
.filter((line) => !MODEL_FIELD_RE.test(line));
|
|
344
379
|
let nameSeen = false;
|
|
345
380
|
for (let i = 0; i < lines.length; i++) {
|
|
346
381
|
const m = lines[i].match(/^(\s*name\s*:\s*)(.*)$/);
|
|
@@ -363,6 +398,41 @@ function buildFrontmatter(originalFrontmatter, skillName, runtimePrefix) {
|
|
|
363
398
|
return '---\n' + lines.join('\n') + '\n---\n';
|
|
364
399
|
}
|
|
365
400
|
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// stripModelFromFrontmatter — drop the Claude-only `model:` line, keep body
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Strip the Claude-only `model:` directive from a full SKILL.md string's
|
|
407
|
+
* frontmatter, leaving the body untouched.
|
|
408
|
+
*
|
|
409
|
+
* Used by the Tier-2 marketplace bundle emitters (codex-plugin.cjs,
|
|
410
|
+
* cursor-marketplace.cjs), which copy the `skills/` tree for non-Claude
|
|
411
|
+
* consumers. A verbatim copy would ship `model: inherit` and crash those
|
|
412
|
+
* consumers exactly as the Tier-1 file-drop path did before `buildFrontmatter`
|
|
413
|
+
* learned to drop it (Kilo: `Model not found: inherit/.`).
|
|
414
|
+
*
|
|
415
|
+
* Byte-preserving when there is nothing to strip: if `content` has no
|
|
416
|
+
* frontmatter, or the frontmatter carries no `model:` line, the ORIGINAL
|
|
417
|
+
* string is returned unchanged. Callers can therefore compare `stripped ===
|
|
418
|
+
* original` and fall back to a byte-exact `copyFileSync` for untouched files.
|
|
419
|
+
*
|
|
420
|
+
* The `model:` field is always a single-line scalar (`model: inherit`) in the
|
|
421
|
+
* GDD source set, so a line filter is sufficient — no block-scalar handling.
|
|
422
|
+
*
|
|
423
|
+
* @param {string} content Full SKILL.md content (frontmatter + body).
|
|
424
|
+
* @returns {string}
|
|
425
|
+
*/
|
|
426
|
+
function stripModelFromFrontmatter(content) {
|
|
427
|
+
if (typeof content !== 'string' || content === '') return content;
|
|
428
|
+
const { frontmatter, body } = extractFrontmatterAndBody(content);
|
|
429
|
+
if (frontmatter == null) return content; // no frontmatter → nothing to strip
|
|
430
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
431
|
+
const kept = lines.filter((line) => !MODEL_FIELD_RE.test(line));
|
|
432
|
+
if (kept.length === lines.length) return content; // no model line → unchanged
|
|
433
|
+
return '---\n' + kept.join('\n') + '\n---\n' + body;
|
|
434
|
+
}
|
|
435
|
+
|
|
366
436
|
// ---------------------------------------------------------------------------
|
|
367
437
|
// Exports
|
|
368
438
|
// ---------------------------------------------------------------------------
|
|
@@ -373,5 +443,7 @@ module.exports = {
|
|
|
373
443
|
rewriteCodeFenceTools,
|
|
374
444
|
ensureAdapterHeader,
|
|
375
445
|
buildFrontmatter,
|
|
446
|
+
stripModelFromFrontmatter,
|
|
447
|
+
MODEL_FIELD_RE,
|
|
376
448
|
CODEX_TOOL_MAP,
|
|
377
449
|
};
|
|
@@ -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
|
-
|
|
35
|
-
|
|
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
|
|
43
|
-
_cache.
|
|
44
|
-
return data;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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: /(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
435
|
+
let fd;
|
|
432
436
|
try {
|
|
433
|
-
|
|
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
|
|
443
|
-
_cache.
|
|
444
|
-
return data;
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
455
|
+
return fallback;
|
|
456
|
+
}
|
|
457
|
+
} finally {
|
|
458
|
+
fs.closeSync(fd);
|
|
449
459
|
}
|
|
450
460
|
}
|
|
451
461
|
module2.exports = { load: load2, reset, MANIFEST_DIR };
|