@opengsd/gsd-core 1.6.0-rc.1 → 1.6.0-rc.2

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.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-roadmapper.md +6 -0
  3. package/bin/install.js +92 -332
  4. package/commands/gsd/capture.md +5 -1
  5. package/gemini-extension.json +1 -1
  6. package/gsd-core/bin/gsd-tools.cjs +18 -3
  7. package/gsd-core/bin/lib/adr-parser.cjs +21 -6
  8. package/gsd-core/bin/lib/capability-registry.cjs +48 -48
  9. package/gsd-core/bin/lib/commands.cjs +247 -0
  10. package/gsd-core/bin/lib/config-loader.cjs +6 -0
  11. package/gsd-core/bin/lib/config.cjs +6 -0
  12. package/gsd-core/bin/lib/frontmatter.cjs +7 -3
  13. package/gsd-core/bin/lib/phase-id.cjs +25 -11
  14. package/gsd-core/bin/lib/phase.cjs +4 -4
  15. package/gsd-core/bin/lib/probe-core.cjs +7 -0
  16. package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
  17. package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
  18. package/gsd-core/bin/lib/roadmap-parser.cjs +29 -8
  19. package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
  20. package/gsd-core/bin/lib/roadmap.cjs +5 -2
  21. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +423 -3
  22. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
  23. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -28
  24. package/gsd-core/bin/lib/runtime-name-policy.cjs +44 -0
  25. package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
  26. package/gsd-core/bin/lib/surface.cjs +12 -19
  27. package/gsd-core/bin/lib/validate.cjs +5 -2
  28. package/gsd-core/bin/lib/verify.cjs +11 -2
  29. package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
  30. package/gsd-core/bin/shared/config-defaults.manifest.json +2 -1
  31. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  32. package/gsd-core/references/context-budget.md +8 -8
  33. package/gsd-core/references/execute-phase-context-guard.md +16 -0
  34. package/gsd-core/references/planning-config.md +1 -0
  35. package/gsd-core/references/prohibition-probe.md +15 -9
  36. package/gsd-core/workflows/autonomous.md +33 -33
  37. package/gsd-core/workflows/diagnose-issues.md +6 -1
  38. package/gsd-core/workflows/execute-phase.md +8 -6
  39. package/gsd-core/workflows/help/modes/full.md +10 -0
  40. package/gsd-core/workflows/list-seeds.md +63 -0
  41. package/gsd-core/workflows/manager.md +37 -37
  42. package/gsd-core/workflows/pr-branch.md +156 -0
  43. package/gsd-core/workflows/quick.md +6 -1
  44. package/gsd-core/workflows/review.md +10 -2
  45. package/gsd-core/workflows/spec-phase.md +8 -3
  46. package/gsd-core/workflows/verify-phase.md +2 -2
  47. package/package.json +4 -1
  48. package/scripts/prompt-injection-scan.sh +1 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gsd-core",
3
3
  "displayName": "GSD Core",
4
- "version": "1.6.0-rc.1",
4
+ "version": "1.6.0-rc.2",
5
5
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
6
6
  "author": {
7
7
  "name": "open-gsd",
@@ -226,6 +226,10 @@ current milestone number and a two-digit phase index within that milestone
226
226
  active milestone context (default: `1` for new projects). This ensures downstream tools that
227
227
  parse `### Phase N-NN:` headers for milestone-scoped workflows receive correctly prefixed IDs.
228
228
 
229
+ `project_code` is only a phase-directory prefix. Never include `project_code` in ROADMAP phase
230
+ checklist entries or detail headers. For example, even when `project_code: "PROJ"` is configured,
231
+ write `Phase 7` for `sequential` and `Phase 1-07` for `milestone-prefixed`, not `Phase PROJ-7`.
232
+
229
233
  ## Granularity Calibration
230
234
 
231
235
  Read granularity from config.json. Granularity controls compression tolerance.
@@ -328,6 +332,7 @@ After roadmap creation, REQUIREMENTS.md gets updated with phase mappings:
328
332
  ### 1. Summary Checklist (under `## Phases`)
329
333
 
330
334
  Use the form matching `phase_id_convention` from config.
335
+ Do not include `project_code` in checklist phase IDs.
331
336
 
332
337
  **Sequential (default — when absent or `"sequential"`):**
333
338
 
@@ -348,6 +353,7 @@ Use the form matching `phase_id_convention` from config.
348
353
  ### 2. Detail Sections (under `## Phase Details`)
349
354
 
350
355
  Use the header form matching `phase_id_convention` from config.
356
+ Do not include `project_code` in detail header phase IDs.
351
357
 
352
358
  **Sequential (default):**
353
359
 
package/bin/install.js CHANGED
@@ -33,6 +33,11 @@ const {
33
33
  getGlobalConfigDir,
34
34
  getGlobalSkillsBase,
35
35
  } = require('../gsd-core/bin/lib/runtime-homes.cjs');
36
+ // getDirName (runtime -> local config dir name) is relocated out of this
37
+ // installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
38
+ // conversion module's rewrite engine can consume it without importing
39
+ // bin/install.js. Re-exported below for back-compat consumers/tests.
40
+ const { getDirName } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
36
41
  const {
37
42
  applyWorktreeBaseRef,
38
43
  readBaseRefFromSettings,
@@ -358,6 +363,10 @@ const {
358
363
  const {
359
364
  resolveRuntimeArtifactLayout,
360
365
  } = require(path.join(_gsdLibDir, 'runtime-artifact-layout.cjs'));
366
+ const {
367
+ createRuntimeArtifactInstallPlan,
368
+ createRuntimeArtifactUninstallPlan,
369
+ } = require(path.join(_gsdLibDir, 'runtime-artifact-install-plan.cjs'));
361
370
  const {
362
371
  planLegacyCleanup,
363
372
  applyLegacyCleanup,
@@ -461,25 +470,8 @@ Then re-run: npx ${pkg.name}@latest
461
470
  }
462
471
  }
463
472
 
464
- // Helper to get directory name for a runtime (used for local/project installs)
465
- function getDirName(runtime) {
466
- if (runtime === 'copilot') return '.github';
467
- if (runtime === 'opencode') return '.opencode';
468
- if (runtime === 'gemini') return '.gemini';
469
- if (runtime === 'kilo') return '.kilo';
470
- if (runtime === 'codex') return '.codex';
471
- if (runtime === 'antigravity') return '.agents';
472
- if (runtime === 'cursor') return '.cursor';
473
- if (runtime === 'windsurf') return '.devin';
474
- if (runtime === 'augment') return '.augment';
475
- if (runtime === 'trae') return '.trae';
476
- if (runtime === 'qwen') return '.qwen';
477
- if (runtime === 'hermes') return '.hermes';
478
- if (runtime === 'kimi') return '.kimi-code';
479
- if (runtime === 'codebuddy') return '.codebuddy';
480
- if (runtime === 'cline') return '.cline';
481
- return '.claude';
482
- }
473
+ // getDirName (runtime -> local config dir name) now lives in
474
+ // runtime-name-policy.cjs (ADR-1508 / #1510 Phase 1); imported + re-exported.
483
475
 
484
476
  /**
485
477
  * Get the config directory path relative to home directory for a runtime
@@ -605,30 +597,12 @@ if (hasHelp) {
605
597
  process.exit(0);
606
598
  }
607
599
 
608
- /**
609
- * Compute the path prefix used for `@file` references in installed command/skill
610
- * markdown. For global installs into a runtime config dir under $HOME, we
611
- * normally substitute the home prefix with `$HOME` so paths expand correctly
612
- * inside double-quoted shell commands. OpenCode is exempt on every platform:
613
- * its `@file` include syntax does NOT shell-expand `$HOME`, so a literal
614
- * `@$HOME/...` is treated as a path relative to the config command/ dir, which
615
- * resolves to `command/$HOME/...` (file not found). For OpenCode we always emit
616
- * the absolute resolved path. (#2376 Windows, #2831 macOS/Linux.)
617
- *
618
- * @param {object} args
619
- * @param {boolean} args.isGlobal - Global runtime install vs local project
620
- * @param {boolean} args.isOpencode - Whether the runtime is OpenCode
621
- * @param {boolean} args.isWindowsHost - process.platform === 'win32'
622
- * @param {string} args.resolvedTarget - Absolute target dir, forward-slashed
623
- * @param {string} args.homeDir - User home dir, forward-slashed
624
- * @returns {string} pathPrefix ending with '/'
625
- */
626
- function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget, homeDir }) {
627
- if (isGlobal && resolvedTarget.startsWith(homeDir) && !isOpencode) {
628
- return '$HOME' + resolvedTarget.slice(homeDir.length) + '/';
629
- }
630
- return `${resolvedTarget}/`;
631
- }
600
+ // computePathPrefix: implementation moved to runtimeArtifactConversion._computePathPrefix
601
+ // (ADR-1508 / #1511 Phase 2 — single owner). The const binding above (~line 638)
602
+ // re-exports it here for call sites and module.exports.
603
+ // Original doc: Compute the path prefix used for `@file` references in installed
604
+ // command/skill markdown. For global installs under $HOME uses $HOME/... form;
605
+ // OpenCode always uses the absolute path (#2376 Windows, #2831 macOS/Linux).
632
606
 
633
607
  // normalizeNodePath, resolveNodeRunner, resolveBashRunner, referencesHook are
634
608
  // now owned by the runtime-hooks-surface module. Import them here so
@@ -643,6 +617,19 @@ const referencesHook = hooksSurface.referencesHook;
643
617
  // applySettingsJsonHooks: mutates settings.hooks.* in place with all GSD-managed
644
618
  // hook registrations for settings.json-surface runtimes (ADR-857 phase 5f-1b).
645
619
  const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks;
620
+ // processAttribution: pure Co-Authored-By content transform, relocated to the
621
+ // conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js
622
+ // callers continue to work and there is a single implementation. (All call
623
+ // sites are below this line, so the const binding has no TDZ hazard.)
624
+ const processAttribution = runtimeArtifactConversion.processAttribution;
625
+ // computePathPrefix / applyRuntimeContentRewritesInPlace / applyRuntimeContentRewritesForCommandsInPlace:
626
+ // Single implementations now live in runtimeArtifactConversion (ADR-1508 / #1511 Phase 2).
627
+ // Re-bound here so install.js call sites and exports continue to work unchanged.
628
+ // Local bodies replaced by breadcrumb comments at their original locations.
629
+ // All call sites are below this line → no TDZ hazard.
630
+ const computePathPrefix = runtimeArtifactConversion._computePathPrefix;
631
+ const applyRuntimeContentRewritesInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesInPlace;
632
+ const applyRuntimeContentRewritesForCommandsInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace;
646
633
 
647
634
  function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
648
635
  return hooksSurface.rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts);
@@ -1417,24 +1404,9 @@ function getCommitAttribution(runtime) {
1417
1404
  return result;
1418
1405
  }
1419
1406
 
1420
- /**
1421
- * Process Co-Authored-By lines based on attribution setting
1422
- * @param {string} content - File content to process
1423
- * @param {null|undefined|string} attribution - null=remove, undefined=keep, string=replace
1424
- * @returns {string} Processed content
1425
- */
1426
- function processAttribution(content, attribution) {
1427
- if (attribution === null) {
1428
- // Remove Co-Authored-By lines and the preceding blank line
1429
- return content.replace(/(\r?\n){2}Co-Authored-By:.*$/gim, '');
1430
- }
1431
- if (attribution === undefined) {
1432
- return content;
1433
- }
1434
- // Replace with custom attribution (escape $ to prevent backreference injection)
1435
- const safeAttribution = attribution.replace(/\$/g, '$$$$');
1436
- return content.replace(/Co-Authored-By:.*$/gim, `Co-Authored-By: ${safeAttribution}`);
1437
- }
1407
+ // processAttribution (pure Co-Authored-By content transform) relocated to
1408
+ // runtime-artifact-conversion.cjs (ADR-1508 / #1510 Phase 1); bound above.
1409
+ // getCommitAttribution stays here — it is impure install-time config I/O.
1438
1410
 
1439
1411
  /**
1440
1412
  * Convert Claude Code frontmatter to opencode format
@@ -6689,24 +6661,10 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
6689
6661
  * @param {string} pathPrefix e.g. "~/.codex/" — trailing-slash string
6690
6662
  * @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
6691
6663
  */
6692
- function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGlobal = false) {
6693
- if (!fs.existsSync(stagedDir)) return;
6694
-
6695
- // Walk all SKILL.md files under stagedDir
6696
- const walkAndRewrite = (dir) => {
6697
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
6698
- const fullPath = path.join(dir, entry.name);
6699
- if (entry.isDirectory()) {
6700
- walkAndRewrite(fullPath);
6701
- } else if (entry.name.endsWith('.md')) {
6702
- let content = fs.readFileSync(fullPath, 'utf8');
6703
- content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
6704
- fs.writeFileSync(fullPath, content);
6705
- }
6706
- }
6707
- };
6708
- walkAndRewrite(stagedDir);
6709
- }
6664
+ // applyRuntimeContentRewritesInPlace: walk loop is now owned by
6665
+ // runtimeArtifactConversion.applyRuntimeContentRewritesInPlace (ADR-1508 / #1511 Phase 2).
6666
+ // The const binding above (~line 629) delegates here. Call sites in installRuntimeArtifacts
6667
+ // pass attribution as the 5th arg (getCommitAttribution(runtime)) per the new contract.
6710
6668
 
6711
6669
  /**
6712
6670
  * Apply per-runtime content rewrites to flat .md files in a staged commands dir.
@@ -6724,30 +6682,10 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGl
6724
6682
  * @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
6725
6683
  * @returns {string} path to a temp dir with rewritten files (caller is responsible for cleanup)
6726
6684
  */
6727
- function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix, isGlobal = false) {
6728
- if (!fs.existsSync(stagedDir)) return stagedDir;
6729
- // Always copy to a temp dir — stageSkillsForProfile() returns the original source
6730
- // dir on full/default profile (skills === '*'), so writing in-place would corrupt the
6731
- // package source. A temp copy is unconditional to keep the code simple and safe.
6732
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cmd-rewrites-'));
6733
- try {
6734
- for (const entry of fs.readdirSync(stagedDir, { withFileTypes: true })) {
6735
- if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
6736
- let content = fs.readFileSync(path.join(stagedDir, entry.name), 'utf8');
6737
- content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
6738
- // For augment commands, apply the markdown conversion so tool references
6739
- // and skill paths use Augment equivalents.
6740
- if (runtime === 'augment') {
6741
- content = convertClaudeToAugmentMarkdown(content);
6742
- }
6743
- fs.writeFileSync(path.join(tempDir, entry.name), content);
6744
- }
6745
- } catch (err) {
6746
- try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { /* best-effort */ }
6747
- throw err;
6748
- }
6749
- return tempDir;
6750
- }
6685
+ // applyRuntimeContentRewritesForCommandsInPlace: copy+rewrite loop is now owned by
6686
+ // runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace (ADR-1508 / #1511 Phase 2).
6687
+ // The const binding above (~line 630) delegates here. Call sites in installRuntimeArtifacts
6688
+ // pass attribution as the 5th arg (getCommitAttribution(runtime)) per the new contract.
6751
6689
 
6752
6690
  /**
6753
6691
  * Apply the per-runtime rewrite table to a single content string.
@@ -6759,196 +6697,12 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
6759
6697
  * @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
6760
6698
  * @returns {string}
6761
6699
  */
6762
- function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6763
- const dirName = getDirName(runtime);
6764
- const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
6765
-
6766
- switch (runtime) {
6767
- case 'codex':
6768
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6769
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6770
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6771
- content = content.replace(/~\/\.codex\//g, pathPrefix);
6772
- content = processAttribution(content, getCommitAttribution(runtime));
6773
- break;
6774
-
6775
- case 'cline':
6776
- // Slash forms: both the original ~/.claude/ (safety net) and the stage-time
6777
- // converted ~/.cline/ (from convertClaudeToCliineMarkdown) → pathPrefix
6778
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6779
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6780
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6781
- content = content.replace(/~\/\.cline\//g, pathPrefix);
6782
- content = content.replace(/\$HOME\/\.cline\//g, pathPrefix);
6783
- // Bare forms (no trailing slash)
6784
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
6785
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
6786
- content = content.replace(/~\/\.cline\b/g, normalizedPathPrefix);
6787
- content = content.replace(/\$HOME\/\.cline\b/g, normalizedPathPrefix);
6788
- content = processAttribution(content, getCommitAttribution(runtime));
6789
- break;
6790
-
6791
- case 'cursor':
6792
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6793
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6794
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6795
- // Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
6796
- // .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
6797
- // fires between 'e' and '-', which rewrites .claude-plugin → .cursor-plugin).
6798
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
6799
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
6800
- content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
6801
- content = content.replace(/~\/\.cursor\//g, pathPrefix);
6802
- content = processAttribution(content, getCommitAttribution(runtime));
6803
- break;
6804
-
6805
- case 'windsurf': {
6806
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6807
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6808
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6809
- // Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
6810
- // .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
6811
- // fires between 'e' and '-', which rewrites .claude-plugin → .devin-plugin).
6812
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
6813
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
6814
- content = content.replace(/~\/\.codeium\/windsurf\//g, pathPrefix);
6815
- // Stage-1 converter rewrites .claude/skills/ → .devin/skills/ (workspace-relative
6816
- // form). For global installs the real path is pathPrefix + skills/, so fix that up
6817
- // here using the real isGlobal flag (threaded from installRuntimeArtifacts scope,
6818
- // not derived from pathPrefix substring which misclassifies custom config dirs).
6819
- // For local installs, the relative .devin/ form is correct — leave it. (#1085)
6820
- if (isGlobal) {
6821
- content = content.replace(/\.devin\/skills\//g, `${pathPrefix}skills/`);
6822
- content = content.replace(/\.\/\.devin\//g, pathPrefix);
6823
- content = content.replace(/~\/\.devin(?![\w-])/g, normalizedPathPrefix);
6824
- content = content.replace(/\$HOME\/\.devin(?![\w-])/g, normalizedPathPrefix);
6825
- }
6826
- content = processAttribution(content, getCommitAttribution(runtime));
6827
- break;
6828
- }
6829
-
6830
- case 'augment':
6831
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6832
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6833
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6834
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
6835
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
6836
- content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
6837
- content = content.replace(/~\/\.augment\//g, pathPrefix);
6838
- content = content.replace(/\$HOME\/\.augment\//g, pathPrefix);
6839
- content = content.replace(/~\/\.augment(?![\w-])/g, normalizedPathPrefix);
6840
- content = content.replace(/\$HOME\/\.augment(?![\w-])/g, normalizedPathPrefix);
6841
- content = processAttribution(content, getCommitAttribution(runtime));
6842
- break;
6843
-
6844
- case 'trae':
6845
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6846
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6847
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6848
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
6849
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
6850
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
6851
- content = content.replace(/~\/\.trae\//g, pathPrefix);
6852
- content = processAttribution(content, getCommitAttribution(runtime));
6853
- break;
6854
-
6855
- case 'codebuddy':
6856
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6857
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6858
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6859
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
6860
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
6861
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
6862
- // The codebuddy converter rewrites `.claude/` → `.codebuddy/` at stage
6863
- // time, so `$HOME/.claude/...` arrives here as `$HOME/.codebuddy/...`.
6864
- // Normalize BOTH the `~/` and `$HOME/` forms (slash + bare) to the install
6865
- // target so `--config-dir`/local installs don't leak the default home.
6866
- content = content.replace(/~\/\.codebuddy\//g, pathPrefix);
6867
- content = content.replace(/\$HOME\/\.codebuddy\//g, pathPrefix);
6868
- content = content.replace(/~\/\.codebuddy\b/g, normalizedPathPrefix);
6869
- content = content.replace(/\$HOME\/\.codebuddy\b/g, normalizedPathPrefix);
6870
- content = processAttribution(content, getCommitAttribution(runtime));
6871
- break;
6872
-
6873
- case 'copilot':
6874
- // Copilot converter handles path rewrites; only attribution here
6875
- content = processAttribution(content, getCommitAttribution('copilot'));
6876
- break;
6877
-
6878
- case 'antigravity':
6879
- // Antigravity converter handles path rewrites; only attribution here
6880
- content = processAttribution(content, getCommitAttribution('antigravity'));
6881
- break;
6882
-
6883
- case 'claude':
6884
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6885
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6886
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6887
- content = processAttribution(content, getCommitAttribution(runtime));
6888
- break;
6889
-
6890
- case 'qwen':
6891
- // Branding rewrites run before path rewrites to avoid consuming
6892
- // patterns that the path step would also match.
6893
- content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
6894
- content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
6895
- // Base path rewrites (use ~/ and $HOME/ slash forms first — most specific)
6896
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6897
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6898
- content = content.replace(/~\/\.qwen\//g, pathPrefix);
6899
- content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
6900
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
6901
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
6902
- content = content.replace(/~\/\.qwen(?![\w-])/g, normalizedPathPrefix);
6903
- content = content.replace(/\$HOME\/\.qwen(?![\w-])/g, normalizedPathPrefix);
6904
- // Bare relative .claude/ → .qwen/ (residual refs not matched above)
6905
- content = content.replace(/\.claude\//g, '.qwen/');
6906
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6907
- content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
6908
- content = processAttribution(content, getCommitAttribution(runtime));
6909
- break;
6910
-
6911
- case 'hermes':
6912
- // Branding rewrites run before path rewrites (same rationale as qwen)
6913
- content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
6914
- content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
6915
- // Base path rewrites
6916
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6917
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6918
- content = content.replace(/~\/\.hermes\//g, pathPrefix);
6919
- content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
6920
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
6921
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
6922
- content = content.replace(/~\/\.hermes(?![\w-])/g, normalizedPathPrefix);
6923
- content = content.replace(/\$HOME\/\.hermes(?![\w-])/g, normalizedPathPrefix);
6924
- // Bare relative .claude/ → .hermes/ (residual refs)
6925
- content = content.replace(/\.claude\//g, '.hermes/');
6926
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6927
- content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
6928
- content = processAttribution(content, getCommitAttribution(runtime));
6929
- break;
6930
-
6931
- case 'kimi':
6932
- content = content.replace(/~\/\.claude\//g, pathPrefix);
6933
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6934
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6935
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
6936
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
6937
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
6938
- content = processAttribution(content, getCommitAttribution(runtime));
6939
- break;
6940
-
6941
- default:
6942
- // Unknown runtime — no rewrites.
6943
- // OpenCode/Kilo are intentionally absent: their skills are written by
6944
- // installOpencodeFamilySkills, which applies pathPrefix BEFORE the
6945
- // command→skill conversion (mirroring copyFlattenedCommands) rather than
6946
- // rewriting already-converted SKILL.md bodies. See #784.
6947
- break;
6948
- }
6949
-
6950
- return content;
6951
- }
6700
+ // _applyRuntimeRewrites: single implementation lives in runtimeArtifactConversion
6701
+ // (ADR-1508 / #1511 Phase 2). Bound here so install.js call sites and exports are
6702
+ // reference-identical to the conversion module (consistent with the walkers above).
6703
+ // All call sites are below this line → no TDZ hazard.
6704
+ const _applyRuntimeRewrites = runtimeArtifactConversion._applyRuntimeRewrites;
6705
+ const _stampNonClaudeRuntimeDefaults = runtimeArtifactConversion._stampNonClaudeRuntimeDefaults;
6952
6706
 
6953
6707
  /**
6954
6708
  * Copy a staged directory's contents into destDir.
@@ -7258,36 +7012,25 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
7258
7012
  _runLegacyInstallMigrations(runtime, configDir, scope);
7259
7013
 
7260
7014
  const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
7261
-
7262
- // Compute pathPrefix once for the rewrite step (same derivation as the
7263
- // top-level install() function).
7264
- const _resolvedTarget = path.resolve(configDir).replace(/\\/g, '/');
7265
- const _homeDir = os.homedir().replace(/\\/g, '/');
7266
- const pathPrefix = computePathPrefix({
7267
- isGlobal: scope === 'global',
7268
- isOpencode: runtime === 'opencode',
7269
- isWindowsHost: process.platform === 'win32',
7270
- resolvedTarget: _resolvedTarget,
7271
- homeDir: _homeDir,
7015
+ const planResult = createRuntimeArtifactInstallPlan({
7016
+ layout,
7017
+ resolvedProfile,
7018
+ homedir: () => os.homedir(),
7019
+ platform: process.platform,
7020
+ resolveAttribution: getCommitAttribution,
7272
7021
  });
7273
7022
 
7274
- for (const kind of layout.kinds) {
7275
- const staged = kind.stage(resolvedProfile);
7276
- // stagedForCopy: the directory to copy from (may differ from staged if rewrites
7277
- // produce a temp copy — see applyRuntimeContentRewritesForCommandsInPlace).
7278
- let stagedForCopy = staged;
7279
- const isGlobal = scope === 'global';
7280
- if (kind.kind === 'skills' || kind.kind === 'kimi-agents') {
7281
- applyRuntimeContentRewritesInPlace(staged, runtime, pathPrefix, isGlobal);
7282
- } else if (kind.kind === 'commands') {
7283
- // Returns a temp dir with rewritten content so source files are never mutated.
7284
- stagedForCopy = applyRuntimeContentRewritesForCommandsInPlace(staged, runtime, pathPrefix, isGlobal);
7285
- }
7286
- // applyRuntimeContentRewritesForCommandsInPlace() returns a fresh mkdtemp dir under
7287
- // os.tmpdir() (gsd-cmd-rewrites-*); remove it once copied so it does not accumulate (#856).
7288
- const tempToClean = stagedForCopy !== staged ? stagedForCopy : null;
7289
- try {
7290
- const dest = path.join(layout.configDir, kind.destSubpath);
7023
+ const cleanupDirs = planResult.ok ? planResult.plan.cleanupDirs : planResult.cleanupDirs;
7024
+ try {
7025
+ if (!planResult.ok) {
7026
+ throw new Error(planResult.message);
7027
+ }
7028
+
7029
+ const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind]));
7030
+ for (const item of planResult.plan.items) {
7031
+ const kind = kindsByName.get(item.kind);
7032
+ if (!kind) throw new Error(`Install plan returned unknown artifact kind: ${item.kind}`);
7033
+ const dest = item.destDir;
7291
7034
  fs.mkdirSync(dest, { recursive: true });
7292
7035
  if (kind.kind === 'skills' && fs.existsSync(dest)) {
7293
7036
  // Pre-prune: snapshot user-owned content before _removeGsdEntries wipes it,
@@ -7314,7 +7057,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
7314
7057
  }
7315
7058
 
7316
7059
  _removeGsdEntries(dest, kind);
7317
- _copyStaged(stagedForCopy, dest, kind);
7060
+ _copyStaged(item.sourceDir, dest, kind);
7318
7061
 
7319
7062
  // Restore user-owned dirs after the prune+copy
7320
7063
  for (const [dirName, snap] of toPreserve) {
@@ -7324,13 +7067,13 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
7324
7067
  // For non-skills kinds (commands, agents): no user content to preserve;
7325
7068
  // just prune stale gsd-* entries and copy new ones.
7326
7069
  _removeGsdEntries(dest, kind);
7327
- _copyStaged(stagedForCopy, dest, kind);
7328
- }
7329
- } finally {
7330
- if (tempToClean) {
7331
- try { fs.rmSync(tempToClean, { recursive: true, force: true }); } catch { /* best-effort */ }
7070
+ _copyStaged(item.sourceDir, dest, kind);
7332
7071
  }
7333
7072
  }
7073
+ } finally {
7074
+ for (const dir of cleanupDirs) {
7075
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
7076
+ }
7334
7077
  }
7335
7078
 
7336
7079
  // Hermes: after the install loop has written all gsd-<stem>/ dirs to
@@ -7447,9 +7190,14 @@ function uninstallRuntimeArtifacts(runtime, configDir, scope) {
7447
7190
  const savedLegacyArtifacts = _runLegacyUninstallCleanup(runtime, configDir, scope);
7448
7191
 
7449
7192
  const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
7450
- for (const kind of layout.kinds) {
7451
- const dest = path.join(layout.configDir, kind.destSubpath);
7452
- _removeGsdEntries(dest, kind);
7193
+ const plan = createRuntimeArtifactUninstallPlan(layout);
7194
+ const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind]));
7195
+ for (const item of plan.items) {
7196
+ const kind = kindsByName.get(item.kind);
7197
+ if (!kind) {
7198
+ throw new Error(`Runtime artifact uninstall plan referenced unknown kind: ${item.kind}`);
7199
+ }
7200
+ _removeGsdEntries(item.destDir, kind);
7453
7201
  }
7454
7202
 
7455
7203
  // Hermes: after removing gsd-* skill dirs from skills/gsd/, also remove
@@ -7540,6 +7288,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
7540
7288
  }
7541
7289
  content = processAttribution(content, getCommitAttribution(runtime));
7542
7290
 
7291
+ // #1521: stamp the workflow runtime-resolution block so every non-Claude
7292
+ // install resolves its own runtime identity and defaults use_worktrees=false.
7293
+ // copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
7294
+ // _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix
7295
+ // live in real installs (it is a no-op for files without those lines).
7296
+ if (runtime !== 'claude') {
7297
+ content = _stampNonClaudeRuntimeDefaults(content, runtime);
7298
+ }
7299
+
7543
7300
  // #3683 — normalize /gsd:<cmd> → /gsd-<cmd> in any body passing through
7544
7301
  // copyWithPathReplacement for runtimes that register commands under the
7545
7302
  // hyphen form; normalizeAgentBodyForRuntime self-gates on
@@ -10044,7 +9801,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10044
9801
  if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
10045
9802
  const stem = entry.name.slice(0, -3);
10046
9803
  let content = fs.readFileSync(path.join(gsdSrc, entry.name), 'utf8');
10047
- content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
9804
+ content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, getCommitAttribution(runtime));
10048
9805
  content = normalizeAgentBodyForRuntime(content, runtime, cmdNames);
10049
9806
  fs.writeFileSync(path.join(commandsDir, `gsd-${stem}.md`), content);
10050
9807
  cmdCount++;
@@ -12291,7 +12048,10 @@ module.exports = {
12291
12048
  // #1191 — exported so tests exercise the REAL readSettings, not a replica
12292
12049
  readSettings,
12293
12050
  stripJsonComments,
12294
- ...runtimeArtifactConversion,
12051
+ // Compatibility relays retained after auditing the former broad
12052
+ // runtimeArtifactConversion spread (#1559).
12053
+ processAttribution,
12054
+ applyRuntimeContentRewritesForCommandsInPlace,
12295
12055
  };
12296
12056
 
12297
12057
  // Main logic — only run when not loaded as a module for testing
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd:capture
3
3
  description: Capture ideas, tasks, notes, and seeds to their destination
4
- argument-hint: "[--note | --backlog | --seed | --list] [text]"
4
+ argument-hint: "[--note | --backlog | --seed | --list | --list-seeds] [text]"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
@@ -21,6 +21,7 @@ Mode routing:
21
21
  - **--backlog**: Add an idea to the backlog parking lot (999.x numbering) → add-backlog workflow
22
22
  - **--seed**: Capture a forward-looking idea with trigger conditions → plant-seed workflow
23
23
  - **--list**: List pending todos and select one to work on → check-todos workflow
24
+ - **--list-seeds**: List/audit captured seeds (optional status filter) → list-seeds workflow
24
25
  </objective>
25
26
 
26
27
  <routing>
@@ -32,6 +33,7 @@ Mode routing:
32
33
  | --backlog | ROADMAP.md backlog section (999.x) | add-backlog |
33
34
  | --seed | .planning/seeds/SEED-NNN-slug.md | plant-seed |
34
35
  | --list | Interactive todo browser + action router | check-todos |
36
+ | --list-seeds | Read-only seed list/audit (optional status filter) | list-seeds |
35
37
 
36
38
  </routing>
37
39
 
@@ -41,6 +43,7 @@ Mode routing:
41
43
  @~/.claude/gsd-core/workflows/add-backlog.md
42
44
  @~/.claude/gsd-core/workflows/plant-seed.md
43
45
  @~/.claude/gsd-core/workflows/check-todos.md
46
+ @~/.claude/gsd-core/workflows/list-seeds.md
44
47
  @~/.claude/gsd-core/references/ui-brand.md
45
48
  </execution_context>
46
49
 
@@ -51,6 +54,7 @@ Parse the first token of $ARGUMENTS:
51
54
  - If it is `--note`: strip the flag, pass remainder to note workflow
52
55
  - If it is `--backlog`: strip the flag, pass remainder to add-backlog workflow
53
56
  - If it is `--seed`: strip the flag, pass remainder to plant-seed workflow
57
+ - If it is `--list-seeds`: strip the flag, pass remainder (optional status filter) to list-seeds workflow
54
58
  - If it is `--list`: pass remainder (optional area filter) to check-todos workflow
55
59
  - Otherwise: pass all of $ARGUMENTS to add-todo workflow
56
60
  </context>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.6.0-rc.1",
3
+ "version": "1.6.0-rc.2",
4
4
  "description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }