@hegemonart/get-design-done 1.60.1 → 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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +38 -0
- package/hooks/context-exhaustion.ts +10 -5
- package/hooks/gdd-mcp-circuit-breaker.js +17 -2
- package/package.json +2 -1
- package/scripts/cli/gdd-events.mjs +25 -7
- package/scripts/generate-skill-frontmatter.cjs +9 -1
- package/scripts/injection-patterns.cjs +47 -0
- package/scripts/lib/ds-arms/design-arms-store.cjs +8 -1
- package/scripts/lib/install/converters/shared.cjs +22 -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.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.
|
|
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.
|
|
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,44 @@ 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
|
+
|
|
23
|
+
## [1.60.2] - 2026-06-13
|
|
24
|
+
|
|
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`).
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **CodeQL / SAST workflow** (`.github/workflows/codeql.yml`) - `javascript-typescript` with the `security-extended` query suite, on push / PR / weekly schedule. Analysis-only (alerts surface in the Security tab; the job is non-blocking on pre-existing findings). A regression guard for every future PR, established now on a clean tree.
|
|
30
|
+
- **`npm audit` production gate in CI** - `npm audit --omit=dev --audit-level=high` in the security job. Scoped to production dependencies (what ships to consumers); dev-only advisories don't gate the build. Currently green.
|
|
31
|
+
- **Dangerous-link scheme detection in the injection scanner** - `scripts/injection-patterns.cjs` now flags `javascript:` URIs, `data:text/html` / `data:` script payloads, userinfo-credential URLs (`scheme://user:pass@host`), and secret-bearing query params (cross-referenced to the redaction token shapes). These flow from untrusted markdown read by the Read hook and the design-authority watcher's feed ingest, which previously had zero coverage for them. Pattern count 21 to 27; both the runtime hook and the CI scanner auto-consume the new patterns. Tightly anchored with negative fixtures so legitimate `https://`, `mailto:`, `data:image`, and ordinary query strings are not flagged.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **`hono` override `>=4.12.23`** added to `package.json` (transitive via `@modelcontextprotocol/sdk`). Precautionary / future-proofing - our `npm audit` does not currently flag the resolved `hono@4.12.21`; this pins the dependency forward regardless. Resolves to `4.12.25`.
|
|
36
|
+
|
|
37
|
+
### Breaking changes
|
|
38
|
+
|
|
39
|
+
None.
|
|
40
|
+
|
|
41
|
+
5,143/5,143 tests pass.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
7
45
|
## [1.60.1] - 2026-06-10
|
|
8
46
|
|
|
9
47
|
**Security hardening** - two HIGH-severity vulnerabilities closed before the upcoming rebrand copies the foundation layer across every runtime. Both were reachable by a prompt-injected agent, undercutting the trust boundary the plugin's own scanners exist to defend. Each fix ships with failing-then-passing regression tests; an independent adversarial audit confirmed both vectors are dead with no surviving bypass.
|
|
@@ -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.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",
|
|
@@ -156,6 +156,7 @@
|
|
|
156
156
|
},
|
|
157
157
|
"overrides": {
|
|
158
158
|
"fast-json-patch": "^3.1.1",
|
|
159
|
+
"hono": ">=4.12.23",
|
|
159
160
|
"qs": ">=6.15.2"
|
|
160
161
|
}
|
|
161
162
|
}
|
|
@@ -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 = [];
|
|
@@ -41,6 +41,53 @@ const INJECTION_PATTERNS = [
|
|
|
41
41
|
{ name: 'tar-home-netcat', re: /\btar\s+c[fzvj]+\s+-\s+~[^\n]*\|\s*(nc|ssh|curl)/ },
|
|
42
42
|
{ name: 'env-dot-leak', re: /process\.env\.[A-Z][A-Z0-9_]*_(KEY|TOKEN|SECRET)\s*[^;,\n]*(fetch|axios|XMLHttpRequest|http\.request)/ },
|
|
43
43
|
{ name: 'ssh-key-cat', re: /\bcat\s+~?\/?\.ssh\/id_(rsa|ed25519|ecdsa|dsa)\b/ },
|
|
44
|
+
|
|
45
|
+
// ── dangerous URL schemes + credential links (60.2 / SEC-CI-03) ──────
|
|
46
|
+
// These flow from untrusted markdown (Read hook) and RSS/article ingest.
|
|
47
|
+
// Each regex is anchored tightly to avoid false-positives on the repo's
|
|
48
|
+
// own shipped docs (the CI scan:injection gate scans them) and is
|
|
49
|
+
// linear-time (bounded quantifiers, no nested/overlapping repetition).
|
|
50
|
+
|
|
51
|
+
// `javascript:` used as a link/href target. The colon must be directly
|
|
52
|
+
// followed by a non-whitespace payload char — so prose like "JavaScript:"
|
|
53
|
+
// (a sentence colon, followed by a space) and the bare word "JavaScript"
|
|
54
|
+
// do NOT match. Preceded by start-of-string or a non-word char so it
|
|
55
|
+
// anchors on `](javascript:` / `href="javascript:` / `=javascript:`.
|
|
56
|
+
{ name: 'javascript: uri', re: /(?:^|[^\w])javascript:(?=\S)/i },
|
|
57
|
+
|
|
58
|
+
// `data:text/html` URIs (optional ;base64). Will NOT match `data:image/…`,
|
|
59
|
+
// nor `data: <word>` prose (colon-space): the literal `text/html` is required.
|
|
60
|
+
{ name: 'data:text/html uri', re: /\bdata:text\/html\b/i },
|
|
61
|
+
|
|
62
|
+
// `data:` URI carrying a script payload (covers data: media types beyond
|
|
63
|
+
// text/html). `[^\s<]{0,200}` is a bounded run (no ReDoS) that also
|
|
64
|
+
// EXCLUDES `<`, so it cannot reach across a `<script` that appears BEFORE
|
|
65
|
+
// `data:` on the line (e.g. export-formats.md:27 "…<script> … data: URIs"):
|
|
66
|
+
// there the char after `data:` is a backtick+space, the run stops at the
|
|
67
|
+
// space, and no script marker follows. Requires `data:` to be immediately
|
|
68
|
+
// followed (no space) by payload chars that lead into `<script`/`%3Cscript`.
|
|
69
|
+
{ name: 'data: script payload', re: /data:[^\s<]{0,200}(?:<script|%3Cscript)/i },
|
|
70
|
+
|
|
71
|
+
// userinfo-credential URL: `scheme://user:pass@host`. The `:` must appear
|
|
72
|
+
// in the userinfo segment BEFORE the `@`, and both must precede the first
|
|
73
|
+
// `/` of the path (i.e. inside the authority). Mutually-exclusive char
|
|
74
|
+
// classes on the boundary chars keep it linear. Does NOT match
|
|
75
|
+
// `mailto:user@host` (no `://`), bare `user@host` (no `://`), a plain
|
|
76
|
+
// `https://host/path` (no `@`), nor an `@` that appears only in the path.
|
|
77
|
+
{ name: 'userinfo credentials url', re: /:\/\/[^/\s:@]+:[^/\s@]*@/ },
|
|
78
|
+
|
|
79
|
+
// Secret-bearing query param. Two linear alternatives:
|
|
80
|
+
// (a) a query KEY named like a credential (token/api_key/secret/…)
|
|
81
|
+
// followed by `=` and a non-trivial value; OR
|
|
82
|
+
// (b) any query value matching a redact.cjs secret SHAPE
|
|
83
|
+
// (sk-ant-/sk-/jwt/AIza/ghp_/gh[sour]_/AKIA/xox…).
|
|
84
|
+
// `[^&\s#]+` and the shape bodies are bounded by their delimiters / fixed
|
|
85
|
+
// lengths — no catastrophic backtracking. Does NOT match benign params
|
|
86
|
+
// like `?q=`, `?lang=en`, `?sort=desc`, `?page=2`.
|
|
87
|
+
{
|
|
88
|
+
name: 'secret-bearing query param',
|
|
89
|
+
re: /[?&](?:access_token|client_secret|api[_-]?key|apikey|token|secret|password|auth)=[^&\s#]+|[?&][\w-]{1,40}=(?:sk-ant-[\w-]{20,}|sk-[\w-]{20,}|eyJ[\w-]{10,}\.[\w-]{10,}\.[\w-]{10,}|AIza[\w-]{35}|ghp_[A-Za-z0-9]{36,}|gh[sour]_[A-Za-z0-9]{36,}|AKIA[0-9A-Z]{16}|xox[baprs]-[\w-]{10,})/i,
|
|
90
|
+
},
|
|
44
91
|
];
|
|
45
92
|
|
|
46
93
|
/**
|
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 };
|