@opengsd/gsd-core 1.7.0-rc.5 → 1.7.0

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 (112) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-executor.md +2 -1
  4. package/agents/gsd-security-auditor.md +13 -15
  5. package/agents/gsd-ui-checker.md +2 -0
  6. package/agents/gsd-ui-researcher.md +1 -0
  7. package/bin/install.js +975 -196
  8. package/commands/gsd/mempalace-capture.md +27 -1
  9. package/commands/gsd/surface.md +6 -6
  10. package/gsd-core/bin/gsd-tools.cjs +63 -2
  11. package/gsd-core/bin/lib/api-coverage.cjs +3 -4
  12. package/gsd-core/bin/lib/audit.cjs +7 -6
  13. package/gsd-core/bin/lib/capability-registry.cjs +503 -87
  14. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  15. package/gsd-core/bin/lib/check-command-router.cjs +1 -1
  16. package/gsd-core/bin/lib/clock.cjs +19 -0
  17. package/gsd-core/bin/lib/commands.cjs +48 -9
  18. package/gsd-core/bin/lib/config-loader.cjs +6 -2
  19. package/gsd-core/bin/lib/config.cjs +12 -0
  20. package/gsd-core/bin/lib/core-utils.cjs +8 -2
  21. package/gsd-core/bin/lib/drift.cjs +4 -4
  22. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  23. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  24. package/gsd-core/bin/lib/host-integration.cjs +33 -8
  25. package/gsd-core/bin/lib/init.cjs +60 -53
  26. package/gsd-core/bin/lib/install-engine.cjs +93 -22
  27. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  28. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  29. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  30. package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
  31. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  32. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  33. package/gsd-core/bin/lib/milestone.cjs +217 -31
  34. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  35. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  36. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  37. package/gsd-core/bin/lib/phase.cjs +436 -61
  38. package/gsd-core/bin/lib/plan-scan.cjs +3 -0
  39. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  40. package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
  41. package/gsd-core/bin/lib/roadmap.cjs +100 -49
  42. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
  43. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  44. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
  45. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  46. package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
  47. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
  48. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  49. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  50. package/gsd-core/bin/lib/security.cjs +7 -37
  51. package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
  52. package/gsd-core/bin/lib/smart-entry.cjs +4 -3
  53. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  54. package/gsd-core/bin/lib/state-transition.cjs +100 -45
  55. package/gsd-core/bin/lib/state.cjs +391 -126
  56. package/gsd-core/bin/lib/surface.cjs +12 -8
  57. package/gsd-core/bin/lib/template.cjs +2 -1
  58. package/gsd-core/bin/lib/uat.cjs +54 -8
  59. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  60. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  61. package/gsd-core/bin/lib/verify.cjs +4 -3
  62. package/gsd-core/bin/lib/workstream.cjs +3 -2
  63. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  64. package/gsd-core/bin/lib/write-set.cjs +38 -0
  65. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  66. package/gsd-core/bin/shared/model-catalog.json +8 -3
  67. package/gsd-core/references/checkpoints.md +12 -0
  68. package/gsd-core/references/ui-consideration-probe.md +73 -0
  69. package/gsd-core/templates/UI-SPEC.md +25 -0
  70. package/gsd-core/templates/VALIDATION.md +2 -0
  71. package/gsd-core/workflows/add-tests.md +1 -1
  72. package/gsd-core/workflows/audit-milestone.md +7 -4
  73. package/gsd-core/workflows/debug.md +2 -0
  74. package/gsd-core/workflows/execute-phase.md +5 -3
  75. package/gsd-core/workflows/fast.md +8 -22
  76. package/gsd-core/workflows/plan-phase.md +6 -0
  77. package/gsd-core/workflows/progress.md +2 -2
  78. package/gsd-core/workflows/quick.md +2 -0
  79. package/gsd-core/workflows/review.md +42 -3
  80. package/gsd-core/workflows/secure-phase.md +1 -1
  81. package/gsd-core/workflows/settings-advanced.md +7 -4
  82. package/gsd-core/workflows/ship.md +8 -2
  83. package/gsd-core/workflows/spec-phase.md +1 -1
  84. package/gsd-core/workflows/transition.md +1 -1
  85. package/gsd-core/workflows/ui-phase.md +146 -1
  86. package/gsd-core/workflows/validate-phase.md +2 -2
  87. package/hooks/dist/gsd-statusline.js +164 -14
  88. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  89. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  90. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  91. package/hooks/gsd-statusline.js +164 -14
  92. package/hooks/gsd-windsurf-pre-command.js +275 -0
  93. package/hooks/gsd-windsurf-pre-write.js +132 -0
  94. package/hooks/managed-hooks-registry.cjs +2 -0
  95. package/package.json +10 -4
  96. package/pi/gsd.cjs +354 -0
  97. package/scripts/build-hooks.js +3 -0
  98. package/scripts/ci-test-scope.cjs +39 -1
  99. package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
  100. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  101. package/scripts/gen-registry.cjs +128 -0
  102. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  103. package/scripts/lint-table-schema-drift.cjs +157 -0
  104. package/scripts/lint-test-file-count.allowlist.json +2 -1
  105. package/scripts/registry-schema.cjs +565 -0
  106. package/scripts/validate-registry.cjs +117 -0
  107. package/skills/gsd-mempalace-capture/SKILL.md +27 -1
  108. package/skills/gsd-surface/SKILL.md +6 -6
  109. package/vscode/browser.js +197 -0
  110. package/vscode/extension.js +383 -0
  111. package/vscode/host-binding.js +113 -0
  112. package/vscode/package.json +96 -0
@@ -13,6 +13,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const node_os_1 = __importDefault(require("node:os"));
15
15
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
16
+ const clock_cjs_1 = require("./clock.cjs");
16
17
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
17
18
  const io = require("./io.cjs");
18
19
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
@@ -63,7 +64,7 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
63
64
  const { findPhaseInternal } = phaseLocator;
64
65
  const { getRoadmapPhaseInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
65
66
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
66
- const { escapeRegex, normalizePhaseName, phaseTokenMatches, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE } = phaseId;
67
+ const { escapeRegex, normalizePhaseName, phaseTokenMatches, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery } = phaseId;
67
68
  const { pruneOrphanedWorktrees } = worktreeSafety;
68
69
  const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, getActiveWorkstream, findContextMdIn, } = planningWorkspace;
69
70
  const { determinePhaseStatus } = commandsMod;
@@ -74,25 +75,10 @@ const { evaluateUatPassed } = uatPredicateMod;
74
75
  void stripShippedMilestones;
75
76
  // Accept all bold/colon variants of the Requirements header (#2769)
76
77
  const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
77
- // #2056: normalizePhaseName() strips ANY [A-Z][A-Z0-9_]*- prefix as a project
78
- // code (e.g. 'MEM-01' → '01'), so a foreign-prefixed workstream/task id would
79
- // collapse to its numeric suffix and resolve to an unrelated numeric phase via
80
- // the dir/roadmap fallback. init plan-phase must require EXACT prefixed
81
- // evidence — a phase dir whose token literally IS the prefixed query, or a
82
- // roadmap entry literally headed with it — before accepting such a match. The
83
- // configured project_code's own prefix (e.g. LKML-01 under project_code: LKML)
84
- // is never foreign and always passes through.
85
- function parsePhasePrefix(phase) {
86
- const match = String(phase).match(/^([A-Z][A-Z0-9_]*)-(?=\d)/i);
87
- return match ? match[1] : null;
88
- }
89
- function isForeignPrefixedPhaseQuery(phase, projectCode) {
90
- const prefix = parsePhasePrefix(phase);
91
- if (!prefix)
92
- return false;
93
- const configured = typeof projectCode === 'string' ? projectCode.trim() : '';
94
- return !configured || prefix.toUpperCase() !== configured.toUpperCase();
95
- }
78
+ // #2056/#2104: isForeignPrefixedPhaseQuery is imported from phase-id.cts
79
+ // (the canonical predicate). parsePhasePrefix is no longer needed locally.
80
+ // phaseInfoMatchesExactPrefix and roadmapPhaseMatchesExactPrefix are local
81
+ // helpers that post-filter the lookup results for foreign-prefix queries.
96
82
  function phaseInfoMatchesExactPrefix(phaseInfo, phase) {
97
83
  const num = phaseInfo?.['phase_number'];
98
84
  const numStr = typeof num === 'string' ? num : (typeof num === 'number' ? String(num) : '');
@@ -103,6 +89,23 @@ function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
103
89
  const section = typeof sectionRaw === 'string' ? sectionRaw : '';
104
90
  return new RegExp(`^#{2,4}\\s*Phase\\s+${escapeRegex(phase)}(?:\\b|\\s|:)`, 'i').test(section);
105
91
  }
92
+ // #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
93
+ // with the #2056 foreign-prefix guard, so every init command gets the same
94
+ // protection without duplicating the guard logic at each call site.
95
+ function guardedFindPhase(cwd, phase, projectCode) {
96
+ let phaseInfo = findPhaseInternal(cwd, phase);
97
+ if (isForeignPrefixedPhaseQuery(phase, projectCode) && !phaseInfoMatchesExactPrefix(phaseInfo, phase)) {
98
+ phaseInfo = null;
99
+ }
100
+ return phaseInfo;
101
+ }
102
+ function guardedGetRoadmapPhase(cwd, phase, projectCode) {
103
+ let roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
104
+ if (isForeignPrefixedPhaseQuery(phase, projectCode) && !roadmapPhaseMatchesExactPrefix(roadmapPhase, phase)) {
105
+ roadmapPhase = null;
106
+ }
107
+ return roadmapPhase;
108
+ }
106
109
  function listPhaseSummaryFiles(phaseDir) {
107
110
  return scanPhasePlans(phaseDir)['summaryFiles'];
108
111
  }
@@ -201,7 +204,7 @@ function getInitGitState(cwd) {
201
204
  }
202
205
  resolved = node_path_1.default.resolve(resolved);
203
206
  if (process.platform === 'win32') {
204
- return resolved.replace(/\//g, '\\').toLowerCase();
207
+ return (0, shell_command_projection_cjs_1.toNativePath)(resolved).toLowerCase();
205
208
  }
206
209
  return resolved;
207
210
  };
@@ -211,7 +214,7 @@ function getInitGitState(cwd) {
211
214
  try {
212
215
  const prefixResult = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--show-prefix'], { cwd, timeout: 5000 });
213
216
  if (prefixResult['exitCode'] === 0) {
214
- const prefix = (typeof prefixResult['stdout'] === 'string' ? prefixResult['stdout'] : '').trim().replace(/\\/g, '/');
217
+ const prefix = (0, shell_command_projection_cjs_1.posixNormalize)((typeof prefixResult['stdout'] === 'string' ? prefixResult['stdout'] : '').trim());
215
218
  inNestedSubdir = prefix.length > 0 && prefix !== '.' && prefix !== './';
216
219
  resolvedByGitPrefix = true;
217
220
  }
@@ -228,7 +231,7 @@ function getInitGitState(cwd) {
228
231
  }
229
232
  else {
230
233
  const rel = node_path_1.default.relative(rootNorm, cwdNorm);
231
- const relNorm = process.platform === 'win32' ? rel.replace(/\//g, '\\') : rel;
234
+ const relNorm = (0, shell_command_projection_cjs_1.toNativePath)(rel);
232
235
  inNestedSubdir =
233
236
  relNorm !== '' &&
234
237
  relNorm !== '.' &&
@@ -242,7 +245,7 @@ function getInitGitState(cwd) {
242
245
  }
243
246
  }
244
247
  if (inNestedSubdir && typeof worktreeRoot === 'string') {
245
- const toComparableRaw = (p) => p.replace(/\\/g, '/').replace(/\/+$/g, '').toLowerCase();
248
+ const toComparableRaw = (p) => (0, shell_command_projection_cjs_1.posixNormalize)(p).replace(/\/+$/g, '').toLowerCase();
246
249
  if (toComparableRaw(worktreeRoot) === toComparableRaw(String(cwd))) {
247
250
  inNestedSubdir = false;
248
251
  }
@@ -258,9 +261,9 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
258
261
  error('phase required for init execute-phase');
259
262
  }
260
263
  const config = loadConfig(cwd);
261
- let phaseInfo = findPhaseInternal(cwd, phase);
264
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
262
265
  const milestone = getMilestoneInfo(cwd);
263
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
266
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
264
267
  if (phaseInfo?.['archived'] && roadmapPhase?.['found']) {
265
268
  phaseInfo = null;
266
269
  }
@@ -361,18 +364,9 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
361
364
  error('phase required for init plan-phase');
362
365
  }
363
366
  const config = loadConfig(cwd);
364
- const foreignPrefixedPhase = isForeignPrefixedPhaseQuery(phase, config.project_code);
365
- let phaseInfo = findPhaseInternal(cwd, phase);
366
- // #2056: a foreign-prefixed query must only match a phase whose token literally
367
- // IS the prefixed query (e.g. a real 'MEM-01-*' dir); otherwise the numeric
368
- // fallback ('MEM-01' → '01') would resolve to the unrelated numeric phase.
369
- if (foreignPrefixedPhase && !phaseInfoMatchesExactPrefix(phaseInfo, phase)) {
370
- phaseInfo = null;
371
- }
372
- let roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
373
- if (foreignPrefixedPhase && !roadmapPhaseMatchesExactPrefix(roadmapPhase, phase)) {
374
- roadmapPhase = null;
375
- }
367
+ // #2056/#2104: foreign-prefixed queries must not collapse to numeric phases.
368
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
369
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
376
370
  if (phaseInfo?.['archived'] && roadmapPhase?.['found']) {
377
371
  phaseInfo = null;
378
372
  }
@@ -620,8 +614,8 @@ function cmdInitQuick(cwd, description, raw) {
620
614
  quick_id: quickId,
621
615
  slug: slug,
622
616
  description: description || null,
623
- date: now.toISOString().split('T')[0],
624
- timestamp: now.toISOString(),
617
+ date: clock_cjs_1.realClock.localToday(),
618
+ timestamp: clock_cjs_1.realClock.nowIso(),
625
619
  quick_dir: '.planning/quick',
626
620
  task_dir: slug ? `.planning/quick/${quickId}-${slug}` : null,
627
621
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
@@ -679,15 +673,15 @@ function cmdInitVerifyWork(cwd, phase, raw) {
679
673
  }
680
674
  const config = loadConfig(cwd);
681
675
  const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
682
- let phaseInfo = findPhaseInternal(cwd, phase);
676
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
683
677
  if (phaseInfo?.['archived']) {
684
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
678
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
685
679
  if (roadmapPhase?.['found']) {
686
680
  phaseInfo = null;
687
681
  }
688
682
  }
689
683
  if (!phaseInfo) {
690
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
684
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
691
685
  if (roadmapPhase?.['found']) {
692
686
  const phaseName = roadmapPhase['phase_name'];
693
687
  phaseInfo = {
@@ -736,9 +730,24 @@ function cmdInitVerifyWork(cwd, phase, raw) {
736
730
  }
737
731
  function cmdInitPhaseOp(cwd, phase, raw) {
738
732
  const config = loadConfig(cwd);
739
- let phaseInfo = findPhaseInternal(cwd, phase);
733
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
734
+ // #2237: surface ambiguous phase-directory collisions instead of silently
735
+ // taking the first match when unrelated projects share a .planning/phases/ tree.
736
+ if (phaseInfo?.['ambiguous_matches']) {
737
+ const matches = phaseInfo['ambiguous_matches'];
738
+ const result = {
739
+ phase_found: false,
740
+ phase_dir: null,
741
+ phase_number: null,
742
+ phase_name: null,
743
+ ambiguous_matches: matches,
744
+ warning: `Phase ${phase} is ambiguous: ${matches.length} directories match (${matches.map((m) => `"${m}"`).join(', ')}). Set a distinct project_code in .planning/config.json to scope resolution.`,
745
+ };
746
+ output(withProjectRoot(cwd, result), raw);
747
+ return;
748
+ }
740
749
  if (phaseInfo?.['archived']) {
741
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
750
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
742
751
  if (roadmapPhase?.['found']) {
743
752
  const phaseName = roadmapPhase['phase_name'];
744
753
  phaseInfo = {
@@ -759,7 +768,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
759
768
  }
760
769
  }
761
770
  if (!phaseInfo) {
762
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
771
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
763
772
  if (roadmapPhase?.['found']) {
764
773
  const phaseName = roadmapPhase['phase_name'];
765
774
  phaseInfo = {
@@ -856,7 +865,6 @@ function cmdInitPhaseOp(cwd, phase, raw) {
856
865
  }
857
866
  function cmdInitTodos(cwd, area, raw) {
858
867
  const config = loadConfig(cwd);
859
- const now = new Date();
860
868
  const pendingDir = node_path_1.default.join(planningDir(cwd), 'todos', 'pending');
861
869
  let count = 0;
862
870
  const todos = [];
@@ -892,8 +900,8 @@ function cmdInitTodos(cwd, area, raw) {
892
900
  }
893
901
  const result = {
894
902
  commit_docs: config.commit_docs,
895
- date: now.toISOString().split('T')[0],
896
- timestamp: now.toISOString(),
903
+ date: clock_cjs_1.realClock.localToday(),
904
+ timestamp: clock_cjs_1.realClock.nowIso(),
897
905
  todo_count: count,
898
906
  todos,
899
907
  area_filter: area || null,
@@ -1014,7 +1022,6 @@ function cmdInitMilestoneOp(cwd, raw) {
1014
1022
  }
1015
1023
  function cmdInitMapCodebase(cwd, raw) {
1016
1024
  const config = loadConfig(cwd);
1017
- const now = new Date();
1018
1025
  const codebaseDir = node_path_1.default.join(planningRoot(cwd), 'codebase');
1019
1026
  let existingMaps = [];
1020
1027
  try {
@@ -1029,8 +1036,8 @@ function cmdInitMapCodebase(cwd, raw) {
1029
1036
  search_gitignored: config.search_gitignored,
1030
1037
  parallelization: config.parallelization,
1031
1038
  subagent_timeout: config.subagent_timeout,
1032
- date: now.toISOString().split('T')[0],
1033
- timestamp: now.toISOString(),
1039
+ date: clock_cjs_1.realClock.localToday(),
1040
+ timestamp: clock_cjs_1.realClock.nowIso(),
1034
1041
  codebase_dir: '.planning/codebase',
1035
1042
  existing_maps: existingMaps,
1036
1043
  has_maps: existingMaps.length > 0,
@@ -1770,7 +1777,7 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
1770
1777
  if (entry.kind === 'directive') {
1771
1778
  return `- Load the \`${entry.name}\` skill via the Skill tool before proceeding (plugin-provided).`;
1772
1779
  }
1773
- return `- @${String(entry.ref).replace(/\\/g, '/')}`;
1780
+ return `- @${(0, shell_command_projection_cjs_1.posixNormalize)(String(entry.ref))}`;
1774
1781
  }).join('\n');
1775
1782
  return `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`;
1776
1783
  }
@@ -27,6 +27,7 @@ const runtimeArtifactLayout = require("./runtime-artifact-layout.cjs");
27
27
  const runtimeArtifactInstallPlan = require("./runtime-artifact-install-plan.cjs");
28
28
  const runtimeNamePolicy = require("./runtime-name-policy.cjs");
29
29
  const installProfiles = require("./install-profiles.cjs");
30
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
30
31
  const { processAttribution } = runtimeArtifactConversion;
31
32
  // resolveRuntimeArtifactLayout: accessed via module ref (not destructured) so
32
33
  // test stubs that monkeypatch the module's exports are seen at call time.
@@ -102,6 +103,17 @@ function convertClaudeCommandToOpencodeSkill(content, skillName) {
102
103
  function convertClaudeCommandToKiloSkill(content, skillName) {
103
104
  return runtimeArtifactConversion.convertClaudeCommandToKiloSkill(content, skillName);
104
105
  }
106
+ /**
107
+ * Converter-name registry for the OpenCode-family combined skills installer
108
+ * (ADR-1239 / #2093). Maps the `converter` string declared on each runtime's
109
+ * artifactLayout skills-kind descriptor (capabilities/<runtime>/capability.json)
110
+ * to the actual conversion function, so `installOpencodeFamilySkills` dispatches
111
+ * off the descriptor instead of a `frontmatterDialect === 'kilo'` runtime check.
112
+ */
113
+ const SKILLS_CONVERTER_REGISTRY = {
114
+ convertClaudeCommandToOpencodeSkill,
115
+ convertClaudeCommandToKiloSkill,
116
+ };
105
117
  // ---------------------------------------------------------------------------
106
118
  // User-artifact preservation helpers
107
119
  // ---------------------------------------------------------------------------
@@ -302,9 +314,13 @@ function _copyStaged(stagedDir, destDir, kind, configDir, runtime) {
302
314
  let destName;
303
315
  if (kind.kind === 'agents') {
304
316
  // Agent files already carry the gsd- prefix in the source dir.
305
- // #1575: copilot agents get .agent.md suffix (mirrors inline loop line ~9118).
306
- destName = runtime === 'copilot'
307
- ? entry.name.replace(/\.md$/, '.agent.md')
317
+ // #2099: descriptor-driven via hostBehaviors.agentFileExtension (was
318
+ // hardcoded `runtime === 'copilot'`). copilot declares '.agent.md';
319
+ // every other runtime's descriptor leaves this unset, so destName falls
320
+ // back to entry.name unchanged (byte-parity, #1575 origin comment).
321
+ const _agentExt = runtime ? _hostBehaviors(runtime).agentFileExtension : undefined;
322
+ destName = _agentExt
323
+ ? entry.name.replace(/\.md$/, _agentExt)
308
324
  : entry.name;
309
325
  }
310
326
  else if (namespacedByDir) {
@@ -440,7 +456,7 @@ function _runLegacyInstallMigrations(runtime, configDir, scope = 'global') {
440
456
  // that for Hermes the flat skills/gsd-*/ removal (below) does not delete the freshly
441
457
  // created skills/gsd-dev-preferences/ skill dir.
442
458
  let savedLegacyArtifacts = null;
443
- if (runtime === 'claude' || runtime === 'qwen' || runtime === 'hermes') {
459
+ if (_hostBehaviors(runtime).legacyCommandsGsdInstallMigration) {
444
460
  if (node_fs_1.default.existsSync(legacyCommandsGsd)) {
445
461
  savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsGsd, ['dev-preferences.md']);
446
462
  node_fs_1.default.rmSync(legacyCommandsGsd, { recursive: true });
@@ -497,7 +513,8 @@ function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
497
513
  // commands/gsd/ for claude local, preserving dev-preferences.md by restoring it
498
514
  // to the same location (#1423). Using migrateLegacyDevPreferencesToSkill here
499
515
  // (which would redirect to skills/) conflicts with the test contract for local installs.
500
- const isLegacyCommandsGsd = runtime === 'qwen' || runtime === 'hermes' || (runtime === 'claude' && scope === 'global');
516
+ const _lu = _hostBehaviors(runtime).legacyCommandsGsdUninstall;
517
+ const isLegacyCommandsGsd = _lu === true || (_lu === 'global' && scope === 'global');
501
518
  if (isLegacyCommandsGsd) {
502
519
  const legacyCommandsGsd = node_path_1.default.join(configDir, 'commands', 'gsd');
503
520
  if (node_fs_1.default.existsSync(legacyCommandsGsd)) {
@@ -651,6 +668,18 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
651
668
  const nestedGsdDirForCleanup = node_path_1.default.join(configDir, 'skills', 'gsd');
652
669
  _removeHermesBareStemDirs(nestedGsdDirForCleanup);
653
670
  }
671
+ // Generic-branch nativePlugin staging (ADR-1239 / #2102 Stage 1): runtimes
672
+ // outside the OpenCode/Kilo combined-family install (e.g. pi, whose
673
+ // artifactLayout is empty and which never sets combinedFamilyInstall) still
674
+ // need their declared hostBehaviors.nativePlugin file copied into configDir.
675
+ // findInstallSourceRoot resolves the repo/package root independent of
676
+ // configDir contents (marker check, then a walk-up from __dirname), so this
677
+ // is safe even when configDir has no .gsd-source marker (artifactLayout: []).
678
+ if (behaviors.nativePlugin) {
679
+ const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
680
+ const src = node_path_1.default.dirname(node_path_1.default.dirname(commandsGsdDir));
681
+ _installNativePluginIfDeclared(runtime, configDir, behaviors, src);
682
+ }
654
683
  }
655
684
  // ---------------------------------------------------------------------------
656
685
  // installOpencodeFamilySkills
@@ -679,9 +708,16 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
679
708
  const rawDir = rawCommandsDir;
680
709
  if (!rawDir || !node_fs_1.default.existsSync(rawDir))
681
710
  return 0;
682
- const converter = _hostBehaviors(runtime).frontmatterDialect === 'kilo'
683
- ? convertClaudeCommandToKiloSkill
684
- : convertClaudeCommandToOpencodeSkill;
711
+ // #2093: descriptor-driven — dispatch off the skills-kind entry's `converter`
712
+ // string (capabilities/<runtime>/capability.json artifactLayout) via the
713
+ // SKILLS_CONVERTER_REGISTRY, instead of a `frontmatterDialect === 'kilo'`
714
+ // runtime check. Fail loud if the descriptor names an unregistered converter
715
+ // (mirrors the converter=null throw in runtime-artifact-layout.cts).
716
+ const converterName = skillsKindEntry.converter;
717
+ const converter = converterName ? SKILLS_CONVERTER_REGISTRY[converterName] : undefined;
718
+ if (!converter) {
719
+ throw new TypeError(`installOpencodeFamilySkills: unknown skills converter '${String(converterName)}' for runtime '${runtime}'`);
720
+ }
685
721
  const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(targetDir, skillsKindEntry.destSubpath);
686
722
  // Symlink-escape guard: reject if any path component between targetDir and
687
723
  // dest is a symlink that would redirect writes outside the config root.
@@ -769,6 +805,16 @@ function installOpencodeFamilyCommands(runtime, destDir, srcDir, pathPrefix, res
769
805
  let content = node_fs_1.default.readFileSync(srcPath, 'utf8');
770
806
  content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
771
807
  content = processAttribution(content, resolveAttribution(runtime));
808
+ // #2093: this commands-kind entry's descriptor `converter` field is
809
+ // intentionally `null` (see capabilities/{kilo,opencode}/capability.json —
810
+ // the flattened-command writer above applies its own path/attribution
811
+ // rewrites and has no per-file converter slot to key on), so there is no
812
+ // descriptor string to dispatch through here. `frontmatterDialect` is the
813
+ // documented, intentional dispatch key for frontmatter-shape selection —
814
+ // it is itself descriptor-driven (not a `runtime === 'kilo'` check), so it
815
+ // already satisfies the fold-to-descriptor requirement. Only the SKILLS
816
+ // converter site above (installOpencodeFamilySkills) has a real
817
+ // `converter` string to key on via SKILLS_CONVERTER_REGISTRY.
772
818
  content = _hostBehaviors(runtime).frontmatterDialect === 'kilo'
773
819
  ? runtimeArtifactConversion.convertClaudeToKiloFrontmatter(content)
774
820
  : runtimeArtifactConversion.convertClaudeToOpencodeFrontmatter(content);
@@ -777,12 +823,44 @@ function installOpencodeFamilyCommands(runtime, destDir, srcDir, pathPrefix, res
777
823
  }
778
824
  }
779
825
  // ---------------------------------------------------------------------------
826
+ // _installNativePluginIfDeclared
827
+ // ---------------------------------------------------------------------------
828
+ /**
829
+ * Copy a runtime's declared native-extension/plugin file (hostBehaviors.nativePlugin)
830
+ * into its resolved config dir, when the runtime descriptor declares one.
831
+ *
832
+ * Extracted (ADR-1239 / #2102 Stage 1) from the body previously inlined in
833
+ * installOpencodeFamilyArtifacts so a runtime that is NOT part of the
834
+ * OpenCode/Kilo combined-family install (e.g. pi, whose artifactLayout is
835
+ * empty and which never sets combinedFamilyInstall) can still get its
836
+ * nativePlugin file staged via the generic installRuntimeArtifacts branch.
837
+ * Behavior for opencode/kilo is unchanged — same source resolution, same
838
+ * mkdir + copyFileSync call, same silent no-op when the source is missing.
839
+ *
840
+ * @param runtime - canonical runtime id (only used for the assertDestWithinConfigHome guard)
841
+ * @param configDir - resolved runtime config directory
842
+ * @param behaviors - the runtime's hostBehaviors descriptor
843
+ * @param src - repo/package root (two levels up from the commands/gsd source dir)
844
+ */
845
+ function _installNativePluginIfDeclared(runtime, configDir, behaviors, src) {
846
+ const np = behaviors.nativePlugin;
847
+ if (np && np.source) {
848
+ const pluginSrc = node_path_1.default.join(src, np.source);
849
+ if (node_fs_1.default.existsSync(pluginSrc)) {
850
+ const destDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, np.dir);
851
+ node_fs_1.default.mkdirSync(destDir, { recursive: true });
852
+ node_fs_1.default.copyFileSync(pluginSrc, node_path_1.default.join(destDir, np.file));
853
+ }
854
+ }
855
+ }
856
+ // ---------------------------------------------------------------------------
780
857
  // installOpencodeFamilyArtifacts
781
858
  // ---------------------------------------------------------------------------
782
859
  /**
783
- * Combined-family install orchestrator for OpenCode/Kilo (ADR-1239 / #2087).
784
- * Stages the flattened commands surface + skills surface + (OpenCode only)
785
- * native plugin adapter, mirroring the bespoke `else if (isOpencode ||
860
+ * Combined-family install orchestrator for OpenCode/Kilo (ADR-1239 / #2087,
861
+ * #2093). Stages the flattened commands surface + skills surface + (any
862
+ * runtime whose hostBehaviors declares `nativePlugin` — OpenCode and, since
863
+ * #2093, Kilo) native plugin adapter, mirroring the bespoke `else if (isOpencode ||
786
864
  * isKilo)` block previously inlined in bin/install.js.
787
865
  *
788
866
  * @param runtime - 'opencode' or 'kilo'
@@ -806,21 +884,13 @@ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfi
806
884
  isGlobal,
807
885
  isOpencode: behaviors.skipHomePrefixSubstitution === true,
808
886
  isWindowsHost: process.platform === 'win32',
809
- resolvedTarget: node_path_1.default.resolve(configDir).replace(/\\/g, '/'),
810
- homeDir: node_os_1.default.homedir().replace(/\\/g, '/'),
887
+ resolvedTarget: (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(configDir)),
888
+ homeDir: (0, shell_command_projection_cjs_1.posixNormalize)(node_os_1.default.homedir()),
811
889
  });
812
890
  const commandDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, 'command');
813
891
  installOpencodeFamilyCommands(runtime, commandDir, rawCommandsDir, pathPrefix, resolveAttribution);
814
892
  installOpencodeFamilySkills(runtime, configDir, rawCommandsDir, pathPrefix, resolveAttribution);
815
- const np = behaviors.nativePlugin;
816
- if (np && np.source) {
817
- const pluginSrc = node_path_1.default.join(src, np.source);
818
- if (node_fs_1.default.existsSync(pluginSrc)) {
819
- const destDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, np.dir);
820
- node_fs_1.default.mkdirSync(destDir, { recursive: true });
821
- node_fs_1.default.copyFileSync(pluginSrc, node_path_1.default.join(destDir, np.file));
822
- }
823
- }
893
+ _installNativePluginIfDeclared(runtime, configDir, behaviors, src);
824
894
  }
825
895
  // ---------------------------------------------------------------------------
826
896
  // uninstallRuntimeArtifacts
@@ -879,6 +949,7 @@ module.exports = {
879
949
  installOpencodeFamilySkills,
880
950
  installOpencodeFamilyCommands,
881
951
  installOpencodeFamilyArtifacts,
952
+ _installNativePluginIfDeclared,
882
953
  _hostBehaviors,
883
954
  _copyStaged,
884
955
  hasExistingSymlinkBetween,
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.validateInstallerMigrationRecord = validateInstallerMigrationRecord;
15
15
  exports.validateInstallerMigrationActions = validateInstallerMigrationActions;
16
16
  const node_path_1 = __importDefault(require("node:path"));
17
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
17
18
  function getStr(record, field) {
18
19
  const v = record[field];
19
20
  return typeof v === 'string' ? v : '';
@@ -56,7 +57,7 @@ function requireActionEvidence(action, field, migration) {
56
57
  }
57
58
  function validateSafeRelPath(relPath, migration, actionType) {
58
59
  const source = actionSource(migration, { relPath });
59
- const normalized = relPath.replace(/\\/g, '/');
60
+ const normalized = (0, shell_command_projection_cjs_1.posixNormalize)(relPath);
60
61
  if (node_path_1.default.isAbsolute(normalized) || node_path_1.default.win32.isAbsolute(normalized)) {
61
62
  throw new Error(`migration action ${actionType} relPath must stay inside configDir: ${source}`);
62
63
  }
@@ -42,6 +42,9 @@ exports.BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
42
42
  'hooks/gsd-cursor-stop.js',
43
43
  'hooks/gsd-cursor-subagent-start.js',
44
44
  'hooks/gsd-cursor-subagent-stop.js',
45
+ // Windsurf/Cascade blocking hooks — registered by writeWindsurfHooksJson (#2100).
46
+ 'hooks/gsd-windsurf-pre-write.js',
47
+ 'hooks/gsd-windsurf-pre-command.js',
45
48
  'hooks/gsd-ensure-canonical-path.js',
46
49
  'hooks/gsd-graphify-update.sh',
47
50
  'hooks/gsd-phase-boundary.sh',
@@ -118,7 +118,7 @@ function normalizeRelPath(relPath) {
118
118
  if (typeof relPath !== 'string' || relPath.trim() === '') {
119
119
  throw new Error('migration action relPath must be a non-empty string');
120
120
  }
121
- const normalized = relPath.replace(/\\/g, '/');
121
+ const normalized = (0, shell_command_projection_cjs_1.posixNormalize)(relPath);
122
122
  if (node_path_1.default.isAbsolute(normalized) || node_path_1.default.win32.isAbsolute(normalized)) {
123
123
  throw new Error(`migration action relPath must stay inside configDir: ${relPath}`);
124
124
  }