@opengsd/gsd-core 1.7.0-rc.6 → 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 (76) 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/bin/install.js +5 -4
  6. package/commands/gsd/mempalace-capture.md +27 -1
  7. package/commands/gsd/surface.md +6 -6
  8. package/gsd-core/bin/gsd-tools.cjs +63 -2
  9. package/gsd-core/bin/lib/api-coverage.cjs +3 -4
  10. package/gsd-core/bin/lib/audit.cjs +7 -6
  11. package/gsd-core/bin/lib/capability-registry.cjs +57 -57
  12. package/gsd-core/bin/lib/check-command-router.cjs +1 -1
  13. package/gsd-core/bin/lib/clock.cjs +19 -0
  14. package/gsd-core/bin/lib/commands.cjs +48 -9
  15. package/gsd-core/bin/lib/config-loader.cjs +6 -2
  16. package/gsd-core/bin/lib/config.cjs +12 -0
  17. package/gsd-core/bin/lib/core-utils.cjs +8 -2
  18. package/gsd-core/bin/lib/drift.cjs +4 -4
  19. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  20. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  21. package/gsd-core/bin/lib/init.cjs +27 -13
  22. package/gsd-core/bin/lib/install-engine.cjs +3 -2
  23. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  24. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  25. package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
  26. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  27. package/gsd-core/bin/lib/milestone.cjs +217 -31
  28. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  29. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  30. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  31. package/gsd-core/bin/lib/phase.cjs +436 -61
  32. package/gsd-core/bin/lib/plan-scan.cjs +3 -0
  33. package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
  34. package/gsd-core/bin/lib/roadmap.cjs +100 -49
  35. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -6
  36. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  37. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +2 -1
  38. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +25 -17
  39. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  40. package/gsd-core/bin/lib/security.cjs +1 -1
  41. package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
  42. package/gsd-core/bin/lib/smart-entry.cjs +4 -3
  43. package/gsd-core/bin/lib/state-transition.cjs +100 -45
  44. package/gsd-core/bin/lib/state.cjs +391 -126
  45. package/gsd-core/bin/lib/surface.cjs +2 -2
  46. package/gsd-core/bin/lib/template.cjs +2 -1
  47. package/gsd-core/bin/lib/uat.cjs +54 -8
  48. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  49. package/gsd-core/bin/lib/verify.cjs +4 -3
  50. package/gsd-core/bin/lib/workstream.cjs +3 -2
  51. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  52. package/gsd-core/bin/lib/write-set.cjs +38 -0
  53. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  54. package/gsd-core/references/checkpoints.md +12 -0
  55. package/gsd-core/workflows/add-tests.md +1 -1
  56. package/gsd-core/workflows/debug.md +2 -0
  57. package/gsd-core/workflows/execute-phase.md +5 -3
  58. package/gsd-core/workflows/fast.md +8 -22
  59. package/gsd-core/workflows/progress.md +2 -2
  60. package/gsd-core/workflows/quick.md +2 -0
  61. package/gsd-core/workflows/review.md +42 -3
  62. package/gsd-core/workflows/secure-phase.md +1 -1
  63. package/gsd-core/workflows/ship.md +8 -2
  64. package/gsd-core/workflows/spec-phase.md +1 -1
  65. package/gsd-core/workflows/transition.md +1 -1
  66. package/hooks/dist/gsd-statusline.js +164 -14
  67. package/hooks/gsd-statusline.js +164 -14
  68. package/package.json +4 -2
  69. package/scripts/ci-test-scope.cjs +39 -1
  70. package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
  71. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  72. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  73. package/scripts/lint-table-schema-drift.cjs +157 -0
  74. package/skills/gsd-mempalace-capture/SKILL.md +27 -1
  75. package/skills/gsd-surface/SKILL.md +6 -6
  76. package/vscode/package.json +1 -1
@@ -45,6 +45,7 @@ const { extractFrontmatter } = frontmatter;
45
45
  const modelProfiles = require("./model-profiles.cjs");
46
46
  const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
47
47
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
48
+ const clock_cjs_1 = require("./clock.cjs");
48
49
  // ─── Phase Status ─────────────────────────────────────────────────────────────
49
50
  /**
50
51
  * Determine phase status by checking plan/summary counts AND verification state.
@@ -592,6 +593,7 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
592
593
  // Stage files
593
594
  const explicitFiles = files && files.length > 0;
594
595
  const filesToStage = explicitFiles ? files : ['.planning/'];
596
+ const stagedPaths = [];
595
597
  for (const file of filesToStage) {
596
598
  const fullPath = node_path_1.default.join(cwd, file);
597
599
  if (!node_fs_1.default.existsSync(fullPath)) {
@@ -607,12 +609,32 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
607
609
  }
608
610
  else {
609
611
  (0, shell_command_projection_cjs_1.execGit)(['add', file], { cwd });
612
+ stagedPaths.push(file);
610
613
  }
611
614
  }
612
- // Commit (--no-verify skips pre-commit hooks, used by parallel executor agents)
613
- const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', sanitizedMessage];
615
+ // Commit — when the caller declared a scope (--files), append a pathspec so
616
+ // only the declared files land in the commit, not the entire index (#2112).
617
+ // The pathspec uses stagedPaths (not filesToStage) so skipped missing files
618
+ // are excluded — otherwise git would record them as deletions (#2014).
619
+ // During a merge, git refuses partial commits — fall back to a bare commit.
620
+ // --amend is left without a pathspec: amending with -- <paths> is a different
621
+ // operation that rewrites the tip with only those paths.
622
+ if (explicitFiles && stagedPaths.length === 0 && !amend) {
623
+ const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
624
+ output(result, raw, 'nothing');
625
+ return;
626
+ }
627
+ const isMergeInProgress = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd }).exitCode === 0;
628
+ const canScope = explicitFiles && stagedPaths.length > 0 && !amend
629
+ && !isMergeInProgress;
630
+ const commitArgs = amend
631
+ ? ['commit', '--amend', '--no-edit']
632
+ : ['commit', '-m', sanitizedMessage];
614
633
  if (noVerify)
615
634
  commitArgs.push('--no-verify');
635
+ if (canScope) {
636
+ commitArgs.push('--', ...stagedPaths);
637
+ }
616
638
  const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd });
617
639
  if (commitResult.exitCode !== 0) {
618
640
  if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
@@ -710,12 +732,21 @@ function cmdCommitToSubrepo(cwd, message, files, raw) {
710
732
  for (const [repo, repoFiles] of Object.entries(grouped)) {
711
733
  const repoCwd = node_path_1.default.join(cwd, repo);
712
734
  // Stage files (strip sub-repo prefix for paths relative to that repo)
735
+ const stagedRelPaths = [];
713
736
  for (const file of repoFiles) {
714
737
  const relativePath = file.slice(repo.length + 1);
715
- (0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
738
+ const addResult = (0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
739
+ if (addResult.exitCode === 0) {
740
+ stagedRelPaths.push(relativePath);
741
+ }
716
742
  }
717
- // Commit
718
- const commitResult = (0, shell_command_projection_cjs_1.execGit)(['commit', '-m', message], { cwd: repoCwd });
743
+ // Commit — pathspec limits the commit to the staged files only (#2112)
744
+ const isMergeInProgressSub = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd: repoCwd }).exitCode === 0;
745
+ const canScopeSub = stagedRelPaths.length > 0 && !isMergeInProgressSub;
746
+ const commitArgs = canScopeSub
747
+ ? ['commit', '-m', message, '--', ...stagedRelPaths]
748
+ : ['commit', '-m', message];
749
+ const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
719
750
  if (commitResult.exitCode !== 0) {
720
751
  if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
721
752
  repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
@@ -837,8 +868,16 @@ function cmdPrSubrepo(cwd, repo, branch, commitMessage, raw) {
837
868
  error(`Failed to stage ${file} in ${repo}: ${addResult.stderr}`);
838
869
  }
839
870
  }
840
- // 5. Commit
841
- const commitResult = (0, shell_command_projection_cjs_1.execGit)(['commit', '-m', commitMessage], { cwd: repoCwd });
871
+ // 5. Commit — pathspec limits the commit to the staged files only (#2112).
872
+ // changedFiles includes both old and new paths for renames so the full
873
+ // rename is captured atomically (pathspec on newPath alone would leave the
874
+ // deletion of oldPath stranded in the index).
875
+ const isMergeInProgressPr = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd: repoCwd }).exitCode === 0;
876
+ const canScopePr = changedFiles.length > 0 && !isMergeInProgressPr;
877
+ const commitArgs = canScopePr
878
+ ? ['commit', '-m', commitMessage, '--', ...changedFiles]
879
+ : ['commit', '-m', commitMessage];
880
+ const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
842
881
  if (commitResult.exitCode !== 0) {
843
882
  rollback();
844
883
  error(`Failed to commit in ${repo}: ${commitResult.stderr}`);
@@ -1223,7 +1262,7 @@ function cmdTodoComplete(cwd, filename, raw) {
1223
1262
  (0, shell_command_projection_cjs_1.platformEnsureDir)(completedDir);
1224
1263
  // Read, add completion timestamp, move
1225
1264
  let content = node_fs_1.default.readFileSync(sourcePath, 'utf-8');
1226
- const today = new Date().toISOString().split('T')[0];
1265
+ const today = clock_cjs_1.realClock.localToday();
1227
1266
  content = `completed: ${today}\n` + content;
1228
1267
  (0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(completedDir, filename), content);
1229
1268
  node_fs_1.default.unlinkSync(sourcePath);
@@ -1232,7 +1271,7 @@ function cmdTodoComplete(cwd, filename, raw) {
1232
1271
  function cmdScaffold(cwd, type, options, raw) {
1233
1272
  const { phase, name } = options;
1234
1273
  const padded = phase ? normalizePhaseName(phase) : '00';
1235
- const today = new Date().toISOString().split('T')[0];
1274
+ const today = clock_cjs_1.realClock.localToday();
1236
1275
  // Find phase directory
1237
1276
  const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
1238
1277
  const phaseDir = phaseInfo ? node_path_1.default.join(cwd, phaseInfo['directory']) : null;
@@ -158,11 +158,15 @@ const _warnedUnknownConfigKeys = new Set();
158
158
  // ─── Git utilities ────────────────────────────────────────────────────────────
159
159
  const _gitIgnoredCache = new Map();
160
160
  function isGitIgnored(cwd, targetPath) {
161
- const key = cwd + '::' + targetPath;
161
+ // #2206: strip trailing slashes — `git check-ignore` has a quirk where a
162
+ // CRLF .gitignore with blank lines falsely reports a trailing-slash path
163
+ // (e.g. `.planning/`) as ignored. Normalizing here protects every call site.
164
+ const normalized = targetPath.replace(/\/+$/, '');
165
+ const key = cwd + '::' + normalized;
162
166
  if (_gitIgnoredCache.has(key))
163
167
  return _gitIgnoredCache.get(key);
164
168
  // --no-index checks .gitignore rules regardless of whether the file is tracked.
165
- const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--', targetPath], { cwd });
169
+ const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--', normalized], { cwd });
166
170
  const ignored = result.exitCode === 0;
167
171
  _gitIgnoredCache.set(key, ignored);
168
172
  return ignored;
@@ -692,6 +692,18 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
692
692
  const VALID_CONTEXT_POSITIONS = ['front', 'end'];
693
693
  if (kp === 'statusline.context_position')
694
694
  assertEnumValue(parsedValue, val, VALID_CONTEXT_POSITIONS, 'statusline.context_position');
695
+ // statusline.show_context_tokens — boolean only
696
+ if (kp === 'statusline.show_context_tokens') {
697
+ if (typeof parsedValue !== 'boolean') {
698
+ error(`Invalid statusline.show_context_tokens '${val}'. Must be a boolean (true or false).`);
699
+ }
700
+ }
701
+ // statusline.show_git — boolean only
702
+ if (kp === 'statusline.show_git') {
703
+ if (typeof parsedValue !== 'boolean') {
704
+ error(`Invalid statusline.show_git '${val}'. Must be a boolean (true or false).`);
705
+ }
706
+ }
695
707
  // Fallow scope + profile enum validation (#3424)
696
708
  const VALID_FALLOW_SCOPES = ['phase', 'repo'];
697
709
  if (kp === 'code_quality.fallow.scope')
@@ -27,10 +27,16 @@ const { comparePhaseNum } = phaseIdModule;
27
27
  // eslint-disable-next-line @typescript-eslint/no-require-imports
28
28
  const planningWorkspace = require("./planning-workspace.cjs");
29
29
  const { findContextMdIn } = planningWorkspace;
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
31
+ const shellCommandProjection = require("./shell-command-projection.cjs");
30
32
  // ─── Path helpers ────────────────────────────────────────────────────────────
31
- /** Normalize a relative path to always use forward slashes (cross-platform). */
33
+ /**
34
+ * Normalize a relative path to always use forward slashes (cross-platform).
35
+ * Delegates to the single separator seam in shell-command-projection so there is
36
+ * exactly one implementation of native→POSIX conversion across the codebase.
37
+ */
32
38
  function toPosixPath(p) {
33
- return p.split(node_path_1.default.sep).join('/');
39
+ return shellCommandProjection.toPosixPath(p);
34
40
  }
35
41
  /**
36
42
  * Scan immediate child directories for separate git repos.
@@ -69,7 +69,7 @@ const SAFE_PATH_RE = /^(?!.*\.\.)(?:[A-Za-z0-9_.][A-Za-z0-9_.\-]*)(?:\/[A-Za-z0-
69
69
  function classifyFile(file) {
70
70
  if (typeof file !== 'string' || !file)
71
71
  return null;
72
- const norm = file.replace(/\\/g, '/');
72
+ const norm = (0, shell_command_projection_cjs_1.posixNormalize)(file);
73
73
  if (MIGRATION_RES.some((r) => r.test(norm)))
74
74
  return 'migration';
75
75
  if (ROUTE_RES.some((r) => r.test(norm)))
@@ -87,7 +87,7 @@ function classifyFile(file) {
87
87
  * check `structureMd.includes('src/lib')` holds.
88
88
  */
89
89
  function isPathMapped(file, structureMd) {
90
- const norm = file.replace(/\\/g, '/');
90
+ const norm = (0, shell_command_projection_cjs_1.posixNormalize)(file);
91
91
  const parts = norm.split('/');
92
92
  // Check prefixes from longest to shortest; any hit means "mapped".
93
93
  for (let i = parts.length - 1; i >= 1; i--) {
@@ -130,7 +130,7 @@ function detectDrift(input) {
130
130
  const elements = [];
131
131
  const seen = new Map();
132
132
  for (const rawFile of added) {
133
- const file = rawFile.replace(/\\/g, '/');
133
+ const file = (0, shell_command_projection_cjs_1.posixNormalize)(rawFile);
134
134
  const specific = classifyFile(file);
135
135
  let category = specific;
136
136
  if (!category) {
@@ -251,7 +251,7 @@ function chooseAffectedPaths(paths) {
251
251
  for (const raw of paths || []) {
252
252
  if (typeof raw !== 'string' || !raw)
253
253
  continue;
254
- const file = raw.replace(/\\/g, '/');
254
+ const file = (0, shell_command_projection_cjs_1.posixNormalize)(raw);
255
255
  const parts = file.split('/');
256
256
  if (parts.length === 0)
257
257
  continue;
@@ -522,6 +522,27 @@ const FRONTMATTER_SCHEMAS = {
522
522
  summary: { required: ['phase', 'plan', 'subsystem', 'tags', 'duration', 'completed'] },
523
523
  verification: { required: ['phase', 'verified', 'status', 'score'] },
524
524
  };
525
+ /**
526
+ * Strip ALL frontmatter blocks from the start of `content`.
527
+ *
528
+ * Handles CRLF line endings and multiple stacked blocks (corruption
529
+ * recovery): greedily strips consecutive `---...---` blocks separated by
530
+ * optional whitespace, so a doubled/tripled frontmatter header (e.g. from a
531
+ * botched merge) is fully removed, not just the first block.
532
+ *
533
+ * Canonical home for this primitive (#2143 audit dedup): previously
534
+ * duplicated byte-identically in both `state.cts` and `state-transition.cts`.
535
+ */
536
+ function stripFrontmatter(content) {
537
+ let result = content;
538
+ while (true) {
539
+ const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
540
+ if (stripped === result)
541
+ break;
542
+ result = stripped;
543
+ }
544
+ return result;
545
+ }
525
546
  function cmdFrontmatterGet(cwd, filePath, field, raw) {
526
547
  if (!filePath) {
527
548
  error('file path required');
@@ -654,6 +675,7 @@ module.exports = {
654
675
  parseFrontmatter: extractFrontmatter,
655
676
  reconstructFrontmatter,
656
677
  spliceFrontmatter,
678
+ stripFrontmatter,
657
679
  noOpObjectListSetError,
658
680
  parseMustHavesBlock,
659
681
  FRONTMATTER_SCHEMAS,
@@ -26,6 +26,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
26
26
  const node_path_1 = __importDefault(require("node:path"));
27
27
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
28
28
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
29
+ const clock_cjs_1 = require("./clock.cjs");
29
30
  // eslint-disable-next-line @typescript-eslint/no-require-imports
30
31
  const ioMod = require("./io.cjs");
31
32
  const { output } = ioMod;
@@ -276,7 +277,7 @@ function buildStateMd(phaseMap) {
276
277
  const status = currentEntry ? 'Ready to plan' : 'All phases complete';
277
278
  const filled = Math.round(pct / 10);
278
279
  const bar = `[${'█'.repeat(filled)}${'░'.repeat(10 - filled)}]`;
279
- const today = new Date().toISOString().split('T')[0];
280
+ const today = clock_cjs_1.realClock.localToday();
280
281
  return [
281
282
  '# Project State',
282
283
  '',
@@ -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
@@ -203,7 +204,7 @@ function getInitGitState(cwd) {
203
204
  }
204
205
  resolved = node_path_1.default.resolve(resolved);
205
206
  if (process.platform === 'win32') {
206
- return resolved.replace(/\//g, '\\').toLowerCase();
207
+ return (0, shell_command_projection_cjs_1.toNativePath)(resolved).toLowerCase();
207
208
  }
208
209
  return resolved;
209
210
  };
@@ -213,7 +214,7 @@ function getInitGitState(cwd) {
213
214
  try {
214
215
  const prefixResult = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--show-prefix'], { cwd, timeout: 5000 });
215
216
  if (prefixResult['exitCode'] === 0) {
216
- 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());
217
218
  inNestedSubdir = prefix.length > 0 && prefix !== '.' && prefix !== './';
218
219
  resolvedByGitPrefix = true;
219
220
  }
@@ -230,7 +231,7 @@ function getInitGitState(cwd) {
230
231
  }
231
232
  else {
232
233
  const rel = node_path_1.default.relative(rootNorm, cwdNorm);
233
- const relNorm = process.platform === 'win32' ? rel.replace(/\//g, '\\') : rel;
234
+ const relNorm = (0, shell_command_projection_cjs_1.toNativePath)(rel);
234
235
  inNestedSubdir =
235
236
  relNorm !== '' &&
236
237
  relNorm !== '.' &&
@@ -244,7 +245,7 @@ function getInitGitState(cwd) {
244
245
  }
245
246
  }
246
247
  if (inNestedSubdir && typeof worktreeRoot === 'string') {
247
- const toComparableRaw = (p) => p.replace(/\\/g, '/').replace(/\/+$/g, '').toLowerCase();
248
+ const toComparableRaw = (p) => (0, shell_command_projection_cjs_1.posixNormalize)(p).replace(/\/+$/g, '').toLowerCase();
248
249
  if (toComparableRaw(worktreeRoot) === toComparableRaw(String(cwd))) {
249
250
  inNestedSubdir = false;
250
251
  }
@@ -613,8 +614,8 @@ function cmdInitQuick(cwd, description, raw) {
613
614
  quick_id: quickId,
614
615
  slug: slug,
615
616
  description: description || null,
616
- date: now.toISOString().split('T')[0],
617
- timestamp: now.toISOString(),
617
+ date: clock_cjs_1.realClock.localToday(),
618
+ timestamp: clock_cjs_1.realClock.nowIso(),
618
619
  quick_dir: '.planning/quick',
619
620
  task_dir: slug ? `.planning/quick/${quickId}-${slug}` : null,
620
621
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
@@ -730,6 +731,21 @@ function cmdInitVerifyWork(cwd, phase, raw) {
730
731
  function cmdInitPhaseOp(cwd, phase, raw) {
731
732
  const config = loadConfig(cwd);
732
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
+ }
733
749
  if (phaseInfo?.['archived']) {
734
750
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
735
751
  if (roadmapPhase?.['found']) {
@@ -849,7 +865,6 @@ function cmdInitPhaseOp(cwd, phase, raw) {
849
865
  }
850
866
  function cmdInitTodos(cwd, area, raw) {
851
867
  const config = loadConfig(cwd);
852
- const now = new Date();
853
868
  const pendingDir = node_path_1.default.join(planningDir(cwd), 'todos', 'pending');
854
869
  let count = 0;
855
870
  const todos = [];
@@ -885,8 +900,8 @@ function cmdInitTodos(cwd, area, raw) {
885
900
  }
886
901
  const result = {
887
902
  commit_docs: config.commit_docs,
888
- date: now.toISOString().split('T')[0],
889
- timestamp: now.toISOString(),
903
+ date: clock_cjs_1.realClock.localToday(),
904
+ timestamp: clock_cjs_1.realClock.nowIso(),
890
905
  todo_count: count,
891
906
  todos,
892
907
  area_filter: area || null,
@@ -1007,7 +1022,6 @@ function cmdInitMilestoneOp(cwd, raw) {
1007
1022
  }
1008
1023
  function cmdInitMapCodebase(cwd, raw) {
1009
1024
  const config = loadConfig(cwd);
1010
- const now = new Date();
1011
1025
  const codebaseDir = node_path_1.default.join(planningRoot(cwd), 'codebase');
1012
1026
  let existingMaps = [];
1013
1027
  try {
@@ -1022,8 +1036,8 @@ function cmdInitMapCodebase(cwd, raw) {
1022
1036
  search_gitignored: config.search_gitignored,
1023
1037
  parallelization: config.parallelization,
1024
1038
  subagent_timeout: config.subagent_timeout,
1025
- date: now.toISOString().split('T')[0],
1026
- timestamp: now.toISOString(),
1039
+ date: clock_cjs_1.realClock.localToday(),
1040
+ timestamp: clock_cjs_1.realClock.nowIso(),
1027
1041
  codebase_dir: '.planning/codebase',
1028
1042
  existing_maps: existingMaps,
1029
1043
  has_maps: existingMaps.length > 0,
@@ -1763,7 +1777,7 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
1763
1777
  if (entry.kind === 'directive') {
1764
1778
  return `- Load the \`${entry.name}\` skill via the Skill tool before proceeding (plugin-provided).`;
1765
1779
  }
1766
- return `- @${String(entry.ref).replace(/\\/g, '/')}`;
1780
+ return `- @${(0, shell_command_projection_cjs_1.posixNormalize)(String(entry.ref))}`;
1767
1781
  }).join('\n');
1768
1782
  return `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`;
1769
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.
@@ -883,8 +884,8 @@ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfi
883
884
  isGlobal,
884
885
  isOpencode: behaviors.skipHomePrefixSubstitution === true,
885
886
  isWindowsHost: process.platform === 'win32',
886
- resolvedTarget: node_path_1.default.resolve(configDir).replace(/\\/g, '/'),
887
- 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()),
888
889
  });
889
890
  const commandDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, 'command');
890
891
  installOpencodeFamilyCommands(runtime, commandDir, rawCommandsDir, pathPrefix, resolveAttribution);
@@ -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
  }
@@ -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
  }