@opengsd/gsd-core 1.9.0 → 1.9.1
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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.md +106 -33
- package/bin/install.js +50 -52
- package/gsd-core/bin/gsd-tools.cjs +33 -2
- package/gsd-core/bin/lib/capability-registry.cjs +63 -63
- package/gsd-core/bin/lib/milestone.cjs +31 -4
- package/gsd-core/bin/lib/phase.cjs +4 -1
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/verify.cjs +19 -3
- package/gsd-core/workflows/code-review.md +17 -2
- package/package.json +1 -1
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/validate-registry.cjs +10 -6
- package/vscode/package.json +1 -1
package/agents/gsd-code-fixer.md
CHANGED
|
@@ -216,9 +216,36 @@ If a finding references multiple files (in Fix section or Issue section):
|
|
|
216
216
|
|
|
217
217
|
This agent runs as a background process that makes commits. Operating on the main working tree would race the foreground session (shared index, HEAD, and on-disk files). Instead, every instance runs in its own isolated worktree.
|
|
218
218
|
|
|
219
|
+
**#2825: honor `workflow.use_worktrees`.** This is the ONLY writer that hand-rolls a git worktree
|
|
220
|
+
inside the agent prompt; every other writer path (`/gsd:execute-phase`, `/gsd:execute-plan`,
|
|
221
|
+
`/gsd:quick`, `/gsd:diagnose-issues`) reads `workflow.use_worktrees` and skips isolation when it is
|
|
222
|
+
`false`. Read the same flag here and, when it is `false`, edit and commit in the main checkout
|
|
223
|
+
directly (set `wt="."`, no `reviewfix_branch`, no recovery sentinel, no `git worktree add`, and skip
|
|
224
|
+
the cleanup tail — there is no worktree to remove). When the flag is not `false`, the transactional
|
|
225
|
+
worktree path below runs unchanged. A user who explicitly opted out of worktrees must never have a
|
|
226
|
+
worktree created; the hand-rolled worktree also cannot run the project's gates safely (no
|
|
227
|
+
`node_modules`), so the opt-out is also the safe path.
|
|
228
|
+
|
|
219
229
|
The cleanup tail (commit fixes -> remove worktree -> drop recovery sentinel) MUST be **transactional**: either all of (worktree, branch advance, sentinel) end in a clean state, or — if the process is interrupted (system restart, OOM kill) between the last commit and `git worktree remove` — a discoverable recovery sentinel is left behind so a future run, `/gsd:resume-work`, or `/gsd:progress` can complete the cleanup. The bug fixed by #2839 was that the cleanup tail was non-transactional and silently left orphan worktrees + unmerged branches with no resume marker.
|
|
220
230
|
|
|
221
231
|
```bash
|
|
232
|
+
# #2825: honor workflow.use_worktrees — the documented opt-out. When false,
|
|
233
|
+
# edit/commit in the main checkout (wt=".", no temp branch, no sentinel, no
|
|
234
|
+
# cleanup tail). Read the flag the same way the four sibling writer workflows
|
|
235
|
+
# do. NOTE: this read parses .planning/config.json directly via `node` rather
|
|
236
|
+
# than the gsd-tools CLI, because setup_worktree runs BEFORE the canonical
|
|
237
|
+
# launcher preamble is sourced — invoking the CLI here would be undefined at
|
|
238
|
+
# runtime and violates the runtime-launcher-parity preamble-ordering rule.
|
|
239
|
+
# Once the preamble is sourced (later steps), the CLI is available.
|
|
240
|
+
USE_WORKTREES=$(node -e '
|
|
241
|
+
try {
|
|
242
|
+
const fs = require("fs");
|
|
243
|
+
const p = (process.env.GSD_PROJECT_DIR || process.cwd()) + "/.planning/config.json";
|
|
244
|
+
const cfg = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
245
|
+
process.stdout.write(String((cfg.workflow && cfg.workflow.use_worktrees) ?? true));
|
|
246
|
+
} catch { process.stdout.write("true"); }
|
|
247
|
+
')
|
|
248
|
+
|
|
222
249
|
# Derive worktree path from padded_phase (parsed from config in next step,
|
|
223
250
|
# but the shell snippet below is illustrative — adapt once config is parsed).
|
|
224
251
|
# In practice: parse padded_phase from config first, then run:
|
|
@@ -264,34 +291,47 @@ if [ -f "$sentinel" ]; then
|
|
|
264
291
|
rm -f "$sentinel"
|
|
265
292
|
fi
|
|
266
293
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
reviewfix_branch="
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
294
|
+
# #2825: when the user opted out of worktrees, edit/commit in the main
|
|
295
|
+
# checkout directly — no temp branch, no sentinel, no cleanup tail. This is
|
|
296
|
+
# the safe path: the hand-rolled worktree has no node_modules, so it cannot
|
|
297
|
+
# run the project's gates, and an improvised teardown can destroy the real
|
|
298
|
+
# node_modules on Windows (a junction followed by rm -rf). wt="." means every
|
|
299
|
+
# downstream read/edit/commit lands in the main working tree, and the cleanup
|
|
300
|
+
# tail below is a no-op (nothing to fast-forward, no worktree to remove).
|
|
301
|
+
if [ "$USE_WORKTREES" = "false" ]; then
|
|
302
|
+
wt="."
|
|
303
|
+
reviewfix_branch="$branch"
|
|
304
|
+
echo "workflow.use_worktrees=false — editing/committing in the main checkout (no worktree)."
|
|
305
|
+
else
|
|
306
|
+
wt=$(mktemp -d "/tmp/sv-${padded_phase}-reviewfix-XXXXXX")
|
|
307
|
+
|
|
308
|
+
# Create a temp branch from the current branch tip so the worktree
|
|
309
|
+
# attaches to that NEW branch rather than the user's currently-checked-out
|
|
310
|
+
# branch (#2990: git refuses to check out the same branch in two
|
|
311
|
+
# worktrees by default; the original `git worktree add "$wt" "$branch"`
|
|
312
|
+
# failed before the agent could do any work). The temp branch shares
|
|
313
|
+
# history with $branch up to the moment of creation, so commits made
|
|
314
|
+
# inside the worktree fast-forward $branch on cleanup.
|
|
315
|
+
reviewfix_branch="gsd-reviewfix/${padded_phase}-$$"
|
|
316
|
+
git worktree add -b "$reviewfix_branch" "$wt" "$branch"
|
|
317
|
+
|
|
318
|
+
# Write the recovery sentinel ONLY AFTER `git worktree add` succeeds.
|
|
319
|
+
# Writing it before would leave a sentinel pointing at a worktree that does
|
|
320
|
+
# not exist if `git worktree add` itself failed.
|
|
321
|
+
node -e '
|
|
322
|
+
const fs = require("fs");
|
|
323
|
+
const [sentinelPath, worktree_path, branch, reviewfix_branch, padded_phase] = process.argv.slice(1);
|
|
324
|
+
fs.writeFileSync(sentinelPath, JSON.stringify({
|
|
325
|
+
worktree_path,
|
|
326
|
+
branch,
|
|
327
|
+
reviewfix_branch,
|
|
328
|
+
padded_phase,
|
|
329
|
+
started_at: new Date().toISOString()
|
|
330
|
+
}, null, 2));
|
|
331
|
+
' "$sentinel" "$wt" "$branch" "$reviewfix_branch" "$padded_phase"
|
|
332
|
+
|
|
333
|
+
cd "$wt"
|
|
334
|
+
fi
|
|
295
335
|
```
|
|
296
336
|
|
|
297
337
|
Concrete steps:
|
|
@@ -305,9 +345,18 @@ Concrete steps:
|
|
|
305
345
|
|
|
306
346
|
**If `git worktree add` fails**, surface the error and exit — do not force-remove the path, as another concurrent run may be holding it. Do not write the sentinel (the worktree does not exist). Do not delete `$reviewfix_branch` either; if `-b` failed, no temp branch was created.
|
|
307
347
|
|
|
308
|
-
**Cleanup tail (transactional, ALWAYS — even on failure):** After writing REVIEW-FIX.md and before returning to the orchestrator, run the cleanup in this exact order
|
|
348
|
+
**Cleanup tail (transactional, ALWAYS — even on failure — when a worktree was created):** After writing REVIEW-FIX.md and before returning to the orchestrator, run the cleanup in this exact order. (When `workflow.use_worktrees` is `false`, no worktree was created — the cleanup is a no-op and the bash below early-exits.)
|
|
309
349
|
|
|
310
350
|
```bash
|
|
351
|
+
# #2825: when worktrees were disabled, there is nothing to clean up — the
|
|
352
|
+
# agent edited/committed on $branch directly in the main checkout (wt=".",
|
|
353
|
+
# reviewfix_branch==$branch, no sentinel, no temp worktree). Skip the whole
|
|
354
|
+
# tail; the four steps below are all no-ops or harmful (e.g. `git worktree
|
|
355
|
+
# remove "."` ) in that mode.
|
|
356
|
+
if [ "$USE_WORKTREES" = "false" ]; then
|
|
357
|
+
exit 0
|
|
358
|
+
fi
|
|
359
|
+
|
|
311
360
|
# Step 1 (#2990): fast-forward $branch to capture the commits the agent
|
|
312
361
|
# made on $reviewfix_branch. Run from the main repo (not $wt) — the user's
|
|
313
362
|
# checkout owns $branch. --ff-only ensures we never silently drop or
|
|
@@ -354,7 +403,7 @@ fi
|
|
|
354
403
|
rm -f "$sentinel"
|
|
355
404
|
```
|
|
356
405
|
|
|
357
|
-
This cleanup is unconditional — register it mentally as a finally-block obligation. If the agent exits early (config error, no findings, etc.), still run the cleanup tail in order (fast-forward → worktree remove → temp branch delete → sentinel rm) before exit. The sentinel must NEVER be removed before `git worktree remove` succeeds. The temp branch must NEVER be deleted while the fast-forward is in a diverged state.
|
|
406
|
+
This cleanup is unconditional when a worktree was created — register it mentally as a finally-block obligation. If the agent exits early (config error, no findings, etc.), still run the cleanup tail in order (fast-forward → worktree remove → temp branch delete → sentinel rm) before exit. (When `workflow.use_worktrees` is `false`, no worktree exists and the bash above early-exits before these steps.) The sentinel must NEVER be removed before `git worktree remove` succeeds. The temp branch must NEVER be deleted while the fast-forward is in a diverged state.
|
|
358
407
|
</step>
|
|
359
408
|
|
|
360
409
|
<step name="load_context">
|
|
@@ -587,9 +636,33 @@ _Iteration: {N}_
|
|
|
587
636
|
|
|
588
637
|
<critical_rules>
|
|
589
638
|
|
|
590
|
-
**ALWAYS run inside the isolated worktree** — set up via `branch=$(git branch --show-current)` + `wt=$(mktemp -d "/tmp/sv-${padded_phase}-reviewfix-XXXXXX")` + `git worktree add -b "$reviewfix_branch" "$wt" "$branch"` at the very start (see `setup_worktree` step). Using `mktemp` ensures concurrent runs do not collide. Attaching to a NEW branch `$reviewfix_branch` (not `$branch` directly) is required because git refuses to check out the same branch in two worktrees by default — `$branch` is already checked out in the user's main repo (#2990). Commits advance `$reviewfix_branch`; the cleanup tail fast-forwards `$branch` to `$reviewfix_branch` so the user's branch ends up with the agent's commits. Every file read, edit, and commit must happen inside `$wt`. Run the four-step cleanup tail
|
|
591
|
-
|
|
592
|
-
|
|
639
|
+
**ALWAYS run inside the isolated worktree** — set up via `branch=$(git branch --show-current)` + `wt=$(mktemp -d "/tmp/sv-${padded_phase}-reviewfix-XXXXXX")` + `git worktree add -b "$reviewfix_branch" "$wt" "$branch"` at the very start (see `setup_worktree` step). Using `mktemp` ensures concurrent runs do not collide. Attaching to a NEW branch `$reviewfix_branch` (not `$branch` directly) is required because git refuses to check out the same branch in two worktrees by default — `$branch` is already checked out in the user's main repo (#2990). Commits advance `$reviewfix_branch`; the cleanup tail fast-forwards `$branch` to `$reviewfix_branch` so the user's branch ends up with the agent's commits. Every file read, edit, and commit must happen inside `$wt`. Run the four-step cleanup tail when done (treat it as a finally block) — but only when a worktree was actually created; when `workflow.use_worktrees` is `false` the cleanup early-exits (no worktree to remove). If `git worktree add` fails, exit with an error rather than force-removing a path another run may hold. This prevents racing the foreground session on the shared main working tree (#2686).
|
|
640
|
+
|
|
641
|
+
**#2825 — honor `workflow.use_worktrees`.** Before creating a worktree, read the
|
|
642
|
+
`workflow.use_worktrees` config flag (the documented opt-out — same key the four sibling writer
|
|
643
|
+
workflows honor). `setup_worktree` reads it via `node` directly from `.planning/config.json`
|
|
644
|
+
(because that step runs BEFORE the canonical gsd_run launcher preamble is sourced; later steps may
|
|
645
|
+
use `gsd_run query config-get workflow.use_worktrees`). When it is `false`, do NOT create a worktree
|
|
646
|
+
— edit and commit in the main checkout directly (`wt="."`, no temp branch, no sentinel, no cleanup
|
|
647
|
+
tail). A user who opted out of worktrees must
|
|
648
|
+
never have one created. See the `setup_worktree` step for the gated bash.
|
|
649
|
+
|
|
650
|
+
**NEVER `rm -rf` a possible reparse point** (#2825). On Windows, `node_modules` inside the worktree
|
|
651
|
+
may be a junction/reparse point whose target is the REAL `node_modules` in the main checkout — and
|
|
652
|
+
`rm -rf` follows the link and deletes the target's contents (silent, misdiagnosable data loss). Do
|
|
653
|
+
NOT improvise a `node_modules` teardown. The worktree has no `node_modules` by design; if you need
|
|
654
|
+
the project's gates, run them in the main checkout after the fast-forward, OR leave the worktree's
|
|
655
|
+
dependency handling to `git worktree remove` (which does not recurse into a separately-managed
|
|
656
|
+
link). Never use `rm -rf` (or `2>/dev/null || rm -rf || true`) as a fallback for removing a path
|
|
657
|
+
that might be a reparse point — on failure, STOP and surface the error rather than falling through
|
|
658
|
+
to a destructive remove.
|
|
659
|
+
|
|
660
|
+
**Record where verification ran** (#2825). The REVIEW-FIX.md verification section must state whether
|
|
661
|
+
the gates ran in the main checkout or the isolated worktree, so a reader can tell whether the numbers
|
|
662
|
+
are reproducible from the tree they are looking at (a worktree-env run is not reproducible from the
|
|
663
|
+
main checkout after teardown).
|
|
664
|
+
|
|
665
|
+
**ALWAYS run the transactional cleanup tail in order when a worktree was created** (#2839, #2990; skipped — bash early-exits — when `workflow.use_worktrees` is `false`): the cleanup is four steps with strict ordering. (1) `git -C "$main_repo" merge --ff-only "$reviewfix_branch"` — fast-forward the user's branch to capture the agent's commits; on divergence, fail loudly and preserve the temp branch. (2) `git worktree remove "$wt" --force`. (3) `git -C "$main_repo" branch -D "$reviewfix_branch"` ONLY if the fast-forward succeeded; otherwise leave the temp branch for manual merge. (4) `rm -f "$sentinel"` (the recovery sentinel at `${phase_dir}/.review-fix-recovery-pending.json`). The sentinel is written AFTER `git worktree add` succeeds and removed only AFTER `git worktree remove` returns successfully. The temp branch is deleted only when the fast-forward succeeded. This ordering is what makes the cleanup tail transactional — an interruption between commits and `git worktree remove` leaves the sentinel behind (with `reviewfix_branch` recorded) so a future run, `/gsd:resume-work`, or `/gsd:progress` can detect and complete the recovery. Reversing the order recreates the orphan-worktree bug.
|
|
593
666
|
|
|
594
667
|
**ALWAYS use the Write tool to create files** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation.
|
|
595
668
|
|
package/bin/install.js
CHANGED
|
@@ -6983,6 +6983,47 @@ function writeCopilotHookConfig(targetDir) {
|
|
|
6983
6983
|
* Generate config.toml and per-agent .toml files for Codex.
|
|
6984
6984
|
* Reads agent .md files from source, extracts metadata, writes .toml configs.
|
|
6985
6985
|
*/
|
|
6986
|
+
|
|
6987
|
+
/**
|
|
6988
|
+
* #2834: Write ~/.gsd/defaults.json for non-Claude runtimes — sets
|
|
6989
|
+
* resolve_model_ids="omit" (so resolveModelInternal() returns '' instead of
|
|
6990
|
+
* Claude aliases the runtime can't resolve) and runtime=<runtime> (so
|
|
6991
|
+
* resolveRuntime() resolves correctly out of the box). MUST be called BEFORE
|
|
6992
|
+
* installCodexConfig (or any other step that reads defaults.json at generation
|
|
6993
|
+
* time), so a clean first install produces correctly-model-routed agent TOMLs.
|
|
6994
|
+
* No-op for Claude runtimes (Claude is the resolveRuntime fallback + has native
|
|
6995
|
+
* model aliases). Preserves an explicit `true` opt-in and existing values.
|
|
6996
|
+
*/
|
|
6997
|
+
function writeNonClaudeDefaults(runtime) {
|
|
6998
|
+
if (_hostBehaviors(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
|
|
6999
|
+
const gsdDir = path.join(os.homedir(), '.gsd');
|
|
7000
|
+
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
7001
|
+
try {
|
|
7002
|
+
fs.mkdirSync(gsdDir, { recursive: true });
|
|
7003
|
+
let defaults = {};
|
|
7004
|
+
try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* new file */ }
|
|
7005
|
+
if (defaults === null || typeof defaults !== 'object' || Array.isArray(defaults)) {
|
|
7006
|
+
defaults = {};
|
|
7007
|
+
}
|
|
7008
|
+
// Three-valued domain: false/absent → aliases; true → full IDs; "omit" → ''.
|
|
7009
|
+
const existing = defaults.resolve_model_ids;
|
|
7010
|
+
const shouldDefaultToOmit = existing !== true && existing !== 'omit';
|
|
7011
|
+
if (shouldDefaultToOmit) {
|
|
7012
|
+
defaults.resolve_model_ids = 'omit';
|
|
7013
|
+
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
7014
|
+
console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
|
7015
|
+
}
|
|
7016
|
+
// #2395: persist runtime for non-Claude runtimes.
|
|
7017
|
+
if (defaults.runtime === undefined || defaults.runtime === null || defaults.runtime === '') {
|
|
7018
|
+
defaults.runtime = runtime;
|
|
7019
|
+
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
7020
|
+
console.log(` ${green}✓${reset} Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
|
|
7021
|
+
}
|
|
7022
|
+
} catch (e) {
|
|
7023
|
+
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
|
|
6986
7027
|
function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-sandbox') {
|
|
6987
7028
|
// ADR-1239 Phase B write-confinement: every Codex config write stays under targetDir.
|
|
6988
7029
|
const configPath = assertDestWithinConfigHome(targetDir, 'config.toml');
|
|
@@ -11586,6 +11627,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
11586
11627
|
|
|
11587
11628
|
let agentCount = 0;
|
|
11588
11629
|
if (!isMinimalMode(_effectiveInstallMode)) {
|
|
11630
|
+
// #2834: write ~/.gsd/defaults.json (resolve_model_ids + runtime) BEFORE generating
|
|
11631
|
+
// agent TOMLs — installCodexConfig reads defaults.json at generation time, so on a
|
|
11632
|
+
// clean first install the runtime-aware model resolver must already know the runtime.
|
|
11633
|
+
writeNonClaudeDefaults(runtime);
|
|
11589
11634
|
try {
|
|
11590
11635
|
// Generate Codex config.toml and per-agent .toml files.
|
|
11591
11636
|
agentCount = installCodexConfig(targetDir, agentsSrc, plan.sandboxTier);
|
|
@@ -12361,58 +12406,11 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
12361
12406
|
configureAntigravityMcpConfig(isGlobal, configDir);
|
|
12362
12407
|
}
|
|
12363
12408
|
|
|
12364
|
-
//
|
|
12365
|
-
//
|
|
12366
|
-
//
|
|
12367
|
-
//
|
|
12368
|
-
|
|
12369
|
-
// chat model instead of pinning the resolved model. See #1156 (default-to-omit
|
|
12370
|
-
// intent) and #1569 (preserve explicit true). Guard matches the #130-class pattern
|
|
12371
|
-
// on configureOpencodePermissions above.
|
|
12372
|
-
if (!_hostBehaviors(runtime).nativeModelAliases && !process.env.GSD_TEST_MODE) {
|
|
12373
|
-
const gsdDir = path.join(os.homedir(), '.gsd');
|
|
12374
|
-
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
12375
|
-
try {
|
|
12376
|
-
fs.mkdirSync(gsdDir, { recursive: true });
|
|
12377
|
-
let defaults = {};
|
|
12378
|
-
try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* new file */ }
|
|
12379
|
-
// Recover a malformed (valid-JSON-but-non-object) defaults.json to a fresh object so
|
|
12380
|
-
// the write below succeeds and the file is no longer broken. Without this, `null` /
|
|
12381
|
-
// `[]` / a number / a string bypass the parse catch and either throw a TypeError on
|
|
12382
|
-
// property access (swallowed by the outer try/catch, leaving the file broken) or get
|
|
12383
|
-
// a property set that won't round-trip through JSON.stringify. (#1657)
|
|
12384
|
-
if (defaults === null || typeof defaults !== 'object' || Array.isArray(defaults)) {
|
|
12385
|
-
defaults = {};
|
|
12386
|
-
}
|
|
12387
|
-
// Three-valued domain: false/absent → aliases; true → full IDs; "omit" → ''.
|
|
12388
|
-
// Honor ONLY an explicit canonical `true` opt-in (full model IDs) and an existing
|
|
12389
|
-
// "omit"; default everything else — absent, falsy, OR any non-canonical value — to
|
|
12390
|
-
// "omit", the safe non-Claude default. Allowlist-based so malformed values
|
|
12391
|
-
// (0, "", "yes", {}, …) don't leak Claude aliases the runtime can't resolve (#1569).
|
|
12392
|
-
const existing = defaults.resolve_model_ids;
|
|
12393
|
-
const shouldDefaultToOmit = existing !== true && existing !== 'omit';
|
|
12394
|
-
if (shouldDefaultToOmit) {
|
|
12395
|
-
defaults.resolve_model_ids = 'omit';
|
|
12396
|
-
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
12397
|
-
console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
|
12398
|
-
}
|
|
12399
|
-
|
|
12400
|
-
// #2395: also persist `runtime: <runtime>` for non-Claude runtimes, so
|
|
12401
|
-
// resolveRuntime() (precedence: GSD_RUNTIME env > config.runtime > 'claude')
|
|
12402
|
-
// resolves to the install's actual runtime identity out of the box — without
|
|
12403
|
-
// this, agent_runtime and every runtime-branded slash hint falls through to
|
|
12404
|
-
// the hard-coded 'claude' default. Mirrors the resolve_model_ids write above:
|
|
12405
|
-
// honor an explicit pre-existing value (any string), only default-populating
|
|
12406
|
-
// when absent. Claude is the resolveRuntime() fallback, so it needs no write.
|
|
12407
|
-
if (defaults.runtime === undefined || defaults.runtime === null || defaults.runtime === '') {
|
|
12408
|
-
defaults.runtime = runtime;
|
|
12409
|
-
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
|
12410
|
-
console.log(` ${green}✓${reset} Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
|
|
12411
|
-
}
|
|
12412
|
-
} catch (e) {
|
|
12413
|
-
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
|
12414
|
-
}
|
|
12415
|
-
}
|
|
12409
|
+
// #2834: defaults.json (resolve_model_ids + runtime) is now written BEFORE
|
|
12410
|
+
// installCodexConfig via writeNonClaudeDefaults(runtime) — extracted into a
|
|
12411
|
+
// function so it can run at the right point in the flow (before agent TOML
|
|
12412
|
+
// generation reads it). This call is idempotent (preserves existing values).
|
|
12413
|
+
writeNonClaudeDefaults(runtime);
|
|
12416
12414
|
|
|
12417
12415
|
// program + command are now single-source lookups (ADR-1239 Phase B / #1679):
|
|
12418
12416
|
// program is the runtime display label; command is the per-host /gsd-new-project
|
|
@@ -3033,6 +3033,29 @@ function runWithTimeout(argv) {
|
|
|
3033
3033
|
const detached = !isWin && secs > 0;
|
|
3034
3034
|
const spawnFailureCode = (err) =>
|
|
3035
3035
|
(err && err.code === 'ENOENT' ? 127 : err && err.code === 'EACCES' ? 126 : 125);
|
|
3036
|
+
// #2667: on Windows, a `.cmd`/`.bat`/`.exe` command cannot be spawned directly
|
|
3037
|
+
// — Node's CVE-2024-27980 hardening (April 2024, all active lines incl. 22.x)
|
|
3038
|
+
// throws EINVAL when child_process.spawn is given a `.cmd`/`.bat` without a
|
|
3039
|
+
// shell, so e.g. `run-with-timeout 120 -- node_modules/.bin/fallow.cmd` silently
|
|
3040
|
+
// produced empty stdout + exit 125 and the fallow pre-pass no-op'd.
|
|
3041
|
+
//
|
|
3042
|
+
// We do NOT use `shell: true` for this: with `shell:true`, Node space-joins the
|
|
3043
|
+
// unescaped cmdArgs into a cmd.exe command string (DEP0190) — that would re-open
|
|
3044
|
+
// a shell-injection surface and violate the recorded no-shell-for-argv-array
|
|
3045
|
+
// contract (DEFECT.UNBOUNDED-SUBPROCESS, CONTEXT.md:772). Instead we spawn
|
|
3046
|
+
// `cmd.exe /c <cmd> <args>` with an explicit argv ARRAY, which is what Node's
|
|
3047
|
+
// own exec does internally and keeps every arg a discrete, un-interpolated
|
|
3048
|
+
// token. The gate is NARROW: it fires ONLY for the Windows shim extensions,
|
|
3049
|
+
// never for the `bash -c` callers (command is `bash`, no such suffix), so the 7
|
|
3050
|
+
// bash callers keep their array-only argv on every platform. POSIX untouched.
|
|
3051
|
+
// NOTE: .exe is INTENTIONALLY excluded — real PE executables (node.exe, etc.)
|
|
3052
|
+
// spawn fine directly and mediating them through cmd.exe /c breaks the timeout
|
|
3053
|
+
// cap's process-group kill (the wrapped child escapes reap → exit 124 never
|
|
3054
|
+
// fires) and risks cmd.exe mis-parsing an arg like `-e "setTimeout(()=>{})"`.
|
|
3055
|
+
// Only .cmd/.bat are the CVE-2024-27980 EINVAL cases that require mediation.
|
|
3056
|
+
const winShim = isWin && /\.(cmd|bat)$/i.test(path.basename(cmd));
|
|
3057
|
+
const spawnCmd = winShim ? (process.env.ComSpec || 'cmd.exe') : cmd;
|
|
3058
|
+
const spawnArgs = winShim ? ['/d', '/s', '/c', cmd, ...cmdArgs] : cmdArgs;
|
|
3036
3059
|
// Node's setTimeout delay is a 32-bit signed ms int; a larger value silently
|
|
3037
3060
|
// clamps to 1ms → a spurious immediate timeout. Cap the budget (~24.8 days).
|
|
3038
3061
|
const timerMs = Math.min(Math.round(secs * 1000), 2 ** 31 - 1);
|
|
@@ -3043,7 +3066,11 @@ function runWithTimeout(argv) {
|
|
|
3043
3066
|
return new Promise((resolve) => {
|
|
3044
3067
|
let child;
|
|
3045
3068
|
try {
|
|
3046
|
-
|
|
3069
|
+
// #2667: on win32 `.cmd`/`.bat`/`.exe`, spawn cmd.exe with an explicit argv
|
|
3070
|
+
// array (spawnCmd/spawnArgs) rather than the shim directly — preserves the
|
|
3071
|
+
// array-only, no-shell-string argv contract. `detached` is always false on
|
|
3072
|
+
// win32, so it never co-occurs with the cmd.exe mediation.
|
|
3073
|
+
child = spawn(spawnCmd, spawnArgs, { stdio: 'inherit', detached });
|
|
3047
3074
|
} catch (err) {
|
|
3048
3075
|
process.stderr.write(`run-with-timeout: ${cmd}: ${err && err.message ? err.message : 'failed to start'}\n`);
|
|
3049
3076
|
resolve(spawnFailureCode(err));
|
|
@@ -3309,7 +3336,11 @@ async function main() {
|
|
|
3309
3336
|
// move the other at the same time (keep them consistent).
|
|
3310
3337
|
const SKIP_ROOT_RESOLUTION = new Set([
|
|
3311
3338
|
'generate-slug', 'current-timestamp', 'verify-path-exists',
|
|
3312
|
-
|
|
3339
|
+
// #2844: verify-summary was previously skipped, leaving relative file-claim
|
|
3340
|
+
// paths resolved against the raw process.cwd() — invoking from a subdirectory
|
|
3341
|
+
// manufactured "missing files" on an otherwise-correct SUMMARY. It now goes
|
|
3342
|
+
// through findProjectRoot so claims resolve against the project root.
|
|
3343
|
+
'template', 'frontmatter', 'detect-custom-files',
|
|
3313
3344
|
// #1854: restore-custom-files operates on a runtime config dir passed
|
|
3314
3345
|
// explicitly via --config-dir; it never reads .planning/.
|
|
3315
3346
|
'restore-custom-files',
|