@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
@@ -697,26 +697,35 @@ const VALID_CONVERTER_NAMES = new Set([
697
697
  'convertClaudeAgentToCodebuddyAgent',
698
698
  'convertClaudeAgentToClineAgent',
699
699
  'convertClaudeAgentToCodexAgent',
700
+ // ADR-1239 / #2092 Phase B Upgrade 1 — native .qwen/agents/*.md subagent projection.
701
+ 'convertClaudeAgentToQwenAgent',
700
702
  ]);
701
703
 
702
704
  // C3: Validate role:runtime body
703
705
  const VALID_CONFIG_FORMATS = new Set(['settings-json', 'toml', 'markdown', 'markdown-dir', 'none']);
704
- const VALID_CONFIG_HOME_KINDS = new Set(['dot-home', 'dot-home-nested', 'xdg', 'generic-agents-root']);
706
+ // 'none' added #2103 — Marketplace/VSIX-distributed hosts (e.g. VS Code) with
707
+ // no file-projected config directory at all.
708
+ const VALID_CONFIG_HOME_KINDS = new Set(['dot-home', 'dot-home-nested', 'xdg', 'generic-agents-root', 'none']);
705
709
  const VALID_COMMAND_STYLES = new Set(['slash-hyphen', 'shell-var']);
706
- const VALID_HOOKS_SURFACES = new Set(['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'none']);
710
+ const VALID_HOOKS_SURFACES = new Set(['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'kimi-hooks-toml', 'windsurf-hooks-json', 'none']);
707
711
  const VALID_HOOK_EVENTS = new Set(['claude', 'gemini']);
708
712
  // extensionEvents — the plugin/extension-system event dialect (ADR-1239 amendment / #1943).
709
713
  // DISTINCT from hookEvents (managed-hook dialect): extensionEvents describes the
710
714
  // plugin-owned event subset imperative hosts expose (opencode / pi); 'none' = the
711
715
  // host exposes no extension surface (engine owns the bus, e.g. VS Code).
712
- const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'hermes', 'none']);
716
+ const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'hermes', 'kilo', 'none']);
713
717
  const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
714
718
  const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills', 'kimi-agents']);
715
719
  const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']);
716
720
  const FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks', 'activationKey'];
717
- const VALID_INSTALL_SURFACES = new Set(['settings-json', 'codex-toml', 'copilot-instructions', 'cline-rules', 'cursor-hooks-json', 'profile-marker-only']);
718
- const VALID_PERMISSION_WRITERS = new Set(['opencode', 'kilo']);
719
- const VALID_EXTENDED_HOOK_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'BeforeAgent', 'AfterAgent', 'BeforeModel']);
721
+ // 'none' added #2103 — Marketplace/VSIX-distributed hosts (e.g. VS Code) that
722
+ // are never CLI-installed (no allRuntimes membership, no install flag).
723
+ const VALID_INSTALL_SURFACES = new Set(['settings-json', 'codex-toml', 'copilot-instructions', 'cline-rules', 'cursor-hooks-json', 'profile-marker-only', 'none']);
724
+ // 'antigravity' added #2096 Phase B Upgrade 1 — settings.json permissions.allow writer.
725
+ const VALID_PERMISSION_WRITERS = new Set(['opencode', 'kilo', 'antigravity']);
726
+ // SubagentStart added #2092 Phase B Upgrade 2 (qwen-only today — see
727
+ // capabilities/qwen/capability.json's extendedHookEvents).
728
+ const VALID_EXTENDED_HOOK_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'BeforeAgent', 'AfterAgent', 'BeforeModel', 'SubagentStart']);
720
729
 
721
730
  // ADR-1239 Phase A: hostIntegration axes (MUST stay parity-identical to HOST_INTEGRATION_AXES in src/host-integration.cts)
722
731
  const VALID_EMBEDDING_MODES = new Set(['imperative', 'declarative']);
@@ -736,13 +745,18 @@ const INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES = new Map([
736
745
  ['copilot-instructions', new Set(['copilot-inline'])],
737
746
  ['cline-rules', new Set(['cline-rules'])],
738
747
  ['cursor-hooks-json', new Set(['cursor-hooks-json'])],
739
- ['profile-marker-only', new Set(['none'])],
748
+ ['profile-marker-only', new Set(['none', 'kimi-hooks-toml', 'windsurf-hooks-json'])],
749
+ // 'none' added #2103 — VS Code has no CLI install surface at all; its only
750
+ // valid hooksSurface pairing is the other 'none' (engine owns the hook bus).
751
+ ['none', new Set(['none'])],
740
752
  ]);
741
753
 
742
754
  // GATE B: extended hook event families → required hookEvents value
743
755
  // Gemini agent-events require hookEvents='gemini'; Claude-family events require hookEvents='claude'.
744
756
  const GEMINI_AGENT_EVENTS = new Set(['BeforeAgent', 'AfterAgent', 'BeforeModel']);
745
- const CLAUDE_FAMILY_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged']);
757
+ // SubagentStart added #2092 Phase B Upgrade 2 — Claude hook-event dialect
758
+ // counterpart of SubagentStop (qwen-only today).
759
+ const CLAUDE_FAMILY_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'SubagentStart']);
746
760
 
747
761
  /**
748
762
  * Validate a runtime.configHome object per ADR-1016 Decision 1.
@@ -771,9 +785,18 @@ function validateConfigHome(capId, ch) {
771
785
  );
772
786
  }
773
787
 
774
- // name — required string
775
- if (typeof ch.name !== 'string' || ch.name.length === 0) {
776
- errors.push(ctx + '.name must be a non-empty string');
788
+ // name — required string, except when kind === 'none': the runtime has no
789
+ // file-projected config directory at all, so a descriptive name is
790
+ // optional (a carve-out mirroring the dot-home-nested⇒parent conditional
791
+ // below, not a new validation mechanism). If present it must still be a
792
+ // non-empty string (e.g. vscode's configHome.name stays a descriptive
793
+ // "vscode" string even though it is never used to build a path).
794
+ if (ch.kind !== 'none') {
795
+ if (typeof ch.name !== 'string' || ch.name.length === 0) {
796
+ errors.push(ctx + '.name must be a non-empty string');
797
+ }
798
+ } else if (ch.name !== undefined && (typeof ch.name !== 'string' || ch.name.length === 0)) {
799
+ errors.push(ctx + '.name must be a non-empty string if present when kind is "none"');
777
800
  }
778
801
 
779
802
  // parent — required when kind == dot-home-nested
@@ -973,7 +996,7 @@ function validateRuntimeBody(cap) {
973
996
  );
974
997
  }
975
998
 
976
- // hooksSurface — closed 6-enum (ADR-1016 Decision 5); inline literal guard (CodeQL barrier)
999
+ // hooksSurface — closed 7-enum (ADR-1016 Decision 5); inline literal guard (CodeQL barrier)
977
1000
  if (r.hooksSurface === '__proto__' || r.hooksSurface === 'constructor' || r.hooksSurface === 'prototype') {
978
1001
  errors.push('runtime.hooksSurface "' + r.hooksSurface + '" is a reserved name');
979
1002
  } else if (!VALID_HOOKS_SURFACES.has(r.hooksSurface)) {
@@ -1054,15 +1077,27 @@ function validateRuntimeBody(cap) {
1054
1077
  // localConfigDir — REQUIRED non-empty dot-dir string (ADR-1239 Phase B #1679)
1055
1078
  // Must start with '.' (e.g. ".claude", ".cursor"). Validated here so the registry
1056
1079
  // generator catches any descriptor missing the field before regenerating.
1057
- if (typeof r.localConfigDir !== 'string' || r.localConfigDir.length === 0) {
1080
+ //
1081
+ // #2103: conditional on configHome.kind !== 'none' — a Marketplace/VSIX
1082
+ // host with no file-projected config directory (e.g. VS Code) has no
1083
+ // local dir to name; localConfigDir may be null/absent for such runtimes.
1084
+ const configHomeKind = (r.configHome && typeof r.configHome === 'object') ? r.configHome.kind : undefined;
1085
+ if (configHomeKind !== 'none') {
1086
+ if (typeof r.localConfigDir !== 'string' || r.localConfigDir.length === 0) {
1087
+ errors.push(
1088
+ 'runtime.localConfigDir is required and must be a non-empty string (e.g. ".claude"); ' +
1089
+ 'got: ' + JSON.stringify(r.localConfigDir),
1090
+ );
1091
+ } else if (!r.localConfigDir.startsWith('.')) {
1092
+ errors.push(
1093
+ 'runtime.localConfigDir must start with "." (a dot-dir); got: ' + JSON.stringify(r.localConfigDir),
1094
+ );
1095
+ }
1096
+ } else if (r.localConfigDir !== null && r.localConfigDir !== undefined) {
1058
1097
  errors.push(
1059
- 'runtime.localConfigDir is required and must be a non-empty string (e.g. ".claude"); ' +
1098
+ 'runtime.localConfigDir must be null or absent when configHome.kind is "none"; ' +
1060
1099
  'got: ' + JSON.stringify(r.localConfigDir),
1061
1100
  );
1062
- } else if (!r.localConfigDir.startsWith('.')) {
1063
- errors.push(
1064
- 'runtime.localConfigDir must start with "." (a dot-dir); got: ' + JSON.stringify(r.localConfigDir),
1065
- );
1066
1101
  }
1067
1102
 
1068
1103
  // extendedHookEvents — required array; every element must be in closed enum
@@ -2168,6 +2203,9 @@ const INSTALL_SURFACE_TO_CONFIG_FORMAT = new Map([
2168
2203
  ['cline-rules', 'markdown-dir'],
2169
2204
  ['cursor-hooks-json', 'none'],
2170
2205
  ['profile-marker-only', 'none'],
2206
+ // 'none' added #2103 — a runtime with NO CLI install surface at all (e.g.
2207
+ // VS Code) has no config-file format to write either.
2208
+ ['none', 'none'],
2171
2209
  ]);
2172
2210
 
2173
2211
  /**
@@ -919,7 +919,7 @@ function cmdApiCoverageVerifyPre(projectDir, args, raw) {
919
919
  // .planning/phases/ (or a milestone archive). The raw arg is never used as a
920
920
  // path, so `..`, absolute paths, and arbitrary directories cannot reach a
921
921
  // file read. Mirrors cmdVerifySchemaDrift's token-match approach.
922
- let token = phaseArg.replace(/\\/g, '/').split('/').filter(Boolean).pop() || '';
922
+ let token = (0, shell_command_projection_cjs_1.posixNormalize)(phaseArg).split('/').filter(Boolean).pop() || '';
923
923
  // A token like ".." or "." carries no phase identity → unresolvable.
924
924
  if (token === '.' || token === '..')
925
925
  token = '';
@@ -81,6 +81,25 @@ exports.realClock = {
81
81
  today() {
82
82
  return this.nowIso().split('T')[0];
83
83
  },
84
+ /**
85
+ * Return today's date as a YYYY-MM-DD string in the HOST-LOCAL calendar day.
86
+ * Uses this.now() so the subprocess time-pin adapter (GSD_NOW_MS) is honoured
87
+ * exactly as today()/nowIso() are — deterministic when GSD_NOW_MS and TZ are
88
+ * both pinned (#2136).
89
+ *
90
+ * Operator-facing date-only fields (last_activity, "completed <date>", etc.)
91
+ * must use the local calendar day: an operator reads them as "the day I did
92
+ * this", and they must never name a day ahead of `last_updated`'s local date.
93
+ * `today()` (UTC) stays the source for internal/cosmetic stamps.
94
+ *
95
+ * @returns e.g. "2020-06-14" (local), which may differ from today() near UTC midnight
96
+ */
97
+ localToday() {
98
+ const d = new Date(this.now());
99
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
100
+ const dd = String(d.getDate()).padStart(2, '0');
101
+ return d.getFullYear() + '-' + mm + '-' + dd;
102
+ },
84
103
  /**
85
104
  * Synchronous sleep via Atomics.wait.
86
105
  * This is the identical primitive acquireStateLock and withPlanningLock used
@@ -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
  '',
@@ -430,14 +430,25 @@ function hookEventSurfaceFor(hookEvents) {
430
430
  // events (session/tool/file/permission); pi ~30 fine-grained extension events;
431
431
  // 'none' = the host exposes no extension surface and the engine owns the bus
432
432
  // (VS Code). Declarative hosts (no plugin API) do not set `extensionEvents`.
433
+ // OpenCode's plugin event surface (ADR-1239 §research; ~25 documented events,
434
+ // GSD binds this subset). Hoisted to a named const — rather than duplicated
435
+ // object literals — so the `kilo` dialect below (#2093) can reuse the IDENTICAL
436
+ // array instead of a copy-pasted one that could silently drift out of sync.
437
+ const OPENCODE_EXTENSION_EVENTS = Object.freeze([
438
+ 'session.created', 'session.idle', 'experimental.session.compacting',
439
+ 'tool.execute.before', 'tool.execute.after', 'file.edited',
440
+ // #2087 — additional documented plugin events GSD binds (opencode.ai/docs/plugins):
441
+ // permission decisions + session error surface.
442
+ 'permission.asked', 'permission.replied', 'session.error',
443
+ ]);
433
444
  const EXTENSION_EVENT_SURFACES = Object.freeze({
434
- opencode: Object.freeze([
435
- 'session.created', 'session.idle', 'experimental.session.compacting',
436
- 'tool.execute.before', 'tool.execute.after', 'file.edited',
437
- // #2087 — additional documented plugin events GSD binds (opencode.ai/docs/plugins):
438
- // permission decisions + session error surface.
439
- 'permission.asked', 'permission.replied', 'session.error',
440
- ]),
445
+ opencode: OPENCODE_EXTENSION_EVENTS,
446
+ // #2093 — Kilo Code is an OpenCode fork sharing the same plugin/extension
447
+ // event bus (host hook bus, UPGRADE 1): reuses OPENCODE_EXTENSION_EVENTS
448
+ // verbatim (not a re-derivation), so the two dialects stay pinned together
449
+ // by construction. See .kilo/plugins/gsd-core.js (copied verbatim from
450
+ // .opencode/plugins/gsd-core.js).
451
+ kilo: OPENCODE_EXTENSION_EVENTS,
441
452
  // #2091 — Hermes Agent real plugin hook vocabulary (13 events).
442
453
  // Cite: https://github.com/nousresearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
443
454
  // Replaces the borrowed `hookEvents: "claude"` 6-event surface that silently
@@ -451,7 +462,21 @@ const EXTENSION_EVENT_SURFACES = Object.freeze({
451
462
  'pre_gateway_dispatch', 'pre_approval_request',
452
463
  'transform_tool_result',
453
464
  ]),
454
- pi: Object.freeze(['tool_call']),
465
+ // #2102 Stage 2 — pi's real ExtensionAPI event vocabulary (~30 fine-grained
466
+ // extension events; documentation-sourced, ADR-1239 §research). Replaces the
467
+ // placeholder single-event ['tool_call'] surface — the Stage 1 value only
468
+ // covered the one event pi/gsd.cjs happened to bind at the time, not the
469
+ // full declared surface.
470
+ pi: Object.freeze([
471
+ 'session_start', 'project_trust', 'resources_discover', 'input',
472
+ 'before_agent_start', 'agent_start', 'message_start', 'message_update',
473
+ 'message_end', 'turn_start', 'context', 'before_provider_request',
474
+ 'after_provider_response', 'tool_execution_start', 'tool_execution_update',
475
+ 'tool_execution_end', 'tool_call', 'tool_result', 'turn_end', 'agent_end',
476
+ 'session_before_switch', 'session_shutdown', 'session_before_fork',
477
+ 'session_info_changed', 'session_before_compact', 'session_compact',
478
+ 'session_before_tree', 'session_tree', 'thinking_level_select', 'model_select',
479
+ ]),
455
480
  none: Object.freeze([]),
456
481
  });
457
482
  /**