@opengsd/gsd-core 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +31 -1
  4. package/agents/gsd-code-fixer.md +107 -34
  5. package/agents/gsd-codebase-mapper.md +1 -1
  6. package/agents/gsd-debug-session-manager.md +36 -0
  7. package/agents/gsd-executor.md +20 -7
  8. package/agents/gsd-intel-updater.md +3 -3
  9. package/agents/gsd-phase-researcher.md +4 -2
  10. package/agents/gsd-plan-checker.md +20 -0
  11. package/agents/gsd-planner.md +15 -23
  12. package/agents/gsd-project-researcher.md +2 -2
  13. package/agents/gsd-ui-auditor.md +0 -40
  14. package/bin/install.js +236 -107
  15. package/commands/gsd/plan-review-convergence.md +5 -1
  16. package/gsd-core/bin/gsd-tools.cjs +882 -4
  17. package/gsd-core/bin/lib/api-coverage.cjs +22 -8
  18. package/gsd-core/bin/lib/audit.cjs +8 -8
  19. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  20. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  21. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  22. package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
  23. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  24. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  25. package/gsd-core/bin/lib/check-command-router.cjs +12 -2
  26. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  27. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
  28. package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
  29. package/gsd-core/bin/lib/commands.cjs +246 -18
  30. package/gsd-core/bin/lib/config-loader.cjs +200 -28
  31. package/gsd-core/bin/lib/config.cjs +90 -5
  32. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  33. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  34. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  35. package/gsd-core/bin/lib/init.cjs +44 -19
  36. package/gsd-core/bin/lib/install-engine.cjs +1 -0
  37. package/gsd-core/bin/lib/milestone.cjs +36 -9
  38. package/gsd-core/bin/lib/model-catalog.cjs +51 -1
  39. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  40. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  41. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  42. package/gsd-core/bin/lib/phase-id.cjs +278 -5
  43. package/gsd-core/bin/lib/phase.cjs +61 -6
  44. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  45. package/gsd-core/bin/lib/plan-scan.cjs +1 -1
  46. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  47. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  48. package/gsd-core/bin/lib/project-root.cjs +48 -0
  49. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  50. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  51. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  52. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  53. package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
  54. package/gsd-core/bin/lib/roadmap.cjs +10 -4
  55. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
  56. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
  57. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
  58. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  59. package/gsd-core/bin/lib/smart-entry.cjs +1 -1
  60. package/gsd-core/bin/lib/state-document.cjs +164 -20
  61. package/gsd-core/bin/lib/state-transition.cjs +28 -10
  62. package/gsd-core/bin/lib/state.cjs +141 -21
  63. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  64. package/gsd-core/bin/lib/uat.cjs +9 -7
  65. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  66. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  67. package/gsd-core/bin/lib/validate.cjs +32 -0
  68. package/gsd-core/bin/lib/verification.cjs +51 -14
  69. package/gsd-core/bin/lib/verify.cjs +146 -22
  70. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  71. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  72. package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
  73. package/gsd-core/bin/shared/model-catalog.json +5 -0
  74. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  75. package/gsd-core/references/context-budget.md +40 -0
  76. package/gsd-core/references/gate-prompts.md +6 -3
  77. package/gsd-core/references/model-profile-resolution.md +64 -13
  78. package/gsd-core/references/offer-next.md +88 -0
  79. package/gsd-core/references/planning-config.md +2 -1
  80. package/gsd-core/references/reviewer-instances.md +28 -21
  81. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  82. package/gsd-core/references/ui-consideration-probe.md +2 -2
  83. package/gsd-core/references/worktree-branch-check.md +4 -4
  84. package/gsd-core/templates/summary-minimal.md +4 -0
  85. package/gsd-core/templates/summary-standard.md +4 -0
  86. package/gsd-core/templates/summary.md +7 -0
  87. package/gsd-core/workflows/ai-integration-phase.md +4 -4
  88. package/gsd-core/workflows/audit-fix.md +4 -0
  89. package/gsd-core/workflows/audit-milestone.md +8 -0
  90. package/gsd-core/workflows/autonomous.md +19 -15
  91. package/gsd-core/workflows/check-todos.md +2 -2
  92. package/gsd-core/workflows/code-review-fix.md +14 -6
  93. package/gsd-core/workflows/code-review.md +93 -21
  94. package/gsd-core/workflows/debug.md +10 -2
  95. package/gsd-core/workflows/diagnose-issues.md +4 -0
  96. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  97. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  98. package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
  99. package/gsd-core/workflows/discuss-phase.md +2 -2
  100. package/gsd-core/workflows/docs-update.md +8 -0
  101. package/gsd-core/workflows/eval-review.md +1 -1
  102. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  103. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  104. package/gsd-core/workflows/execute-phase.md +85 -115
  105. package/gsd-core/workflows/execute-plan.md +5 -4
  106. package/gsd-core/workflows/explore.md +4 -0
  107. package/gsd-core/workflows/extract-learnings.md +21 -0
  108. package/gsd-core/workflows/help/modes/full.md +3 -3
  109. package/gsd-core/workflows/import.md +4 -1
  110. package/gsd-core/workflows/ingest-docs.md +4 -0
  111. package/gsd-core/workflows/map-codebase.md +13 -6
  112. package/gsd-core/workflows/new-milestone.md +10 -2
  113. package/gsd-core/workflows/new-project.md +11 -4
  114. package/gsd-core/workflows/next.md +5 -2
  115. package/gsd-core/workflows/plan-phase.md +42 -46
  116. package/gsd-core/workflows/plan-review-convergence.md +18 -14
  117. package/gsd-core/workflows/progress.md +1 -1
  118. package/gsd-core/workflows/quick.md +14 -3
  119. package/gsd-core/workflows/review.md +146 -575
  120. package/gsd-core/workflows/scan.md +9 -1
  121. package/gsd-core/workflows/secure-phase.md +10 -2
  122. package/gsd-core/workflows/ship.md +41 -11
  123. package/gsd-core/workflows/smart-entry.md +1 -1
  124. package/gsd-core/workflows/ui-phase.md +8 -1
  125. package/gsd-core/workflows/ui-review.md +8 -1
  126. package/gsd-core/workflows/update.md +104 -5
  127. package/gsd-core/workflows/validate-phase.md +10 -2
  128. package/gsd-core/workflows/verify-work.md +8 -1
  129. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  130. package/hooks/dist/gsd-cursor-stop.js +6 -2
  131. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  132. package/hooks/dist/gsd-graphify-update.sh +9 -0
  133. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  134. package/hooks/dist/gsd-prompt-guard.js +101 -2
  135. package/hooks/dist/gsd-read-guard.js +100 -2
  136. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  137. package/hooks/dist/gsd-statusline.js +9 -6
  138. package/hooks/dist/gsd-workflow-guard.js +110 -6
  139. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  140. package/hooks/dist/lib/cursor-workspace.js +74 -0
  141. package/hooks/gsd-cursor-session-start.js +6 -2
  142. package/hooks/gsd-cursor-stop.js +6 -2
  143. package/hooks/gsd-cursor-subagent-start.js +6 -2
  144. package/hooks/gsd-graphify-update.sh +9 -0
  145. package/hooks/gsd-phase-boundary.sh +14 -2
  146. package/hooks/gsd-prompt-guard.js +101 -2
  147. package/hooks/gsd-read-guard.js +100 -2
  148. package/hooks/gsd-read-injection-scanner.js +109 -2
  149. package/hooks/gsd-statusline.js +9 -6
  150. package/hooks/gsd-workflow-guard.js +110 -6
  151. package/hooks/gsd-worktree-path-guard.js +132 -8
  152. package/hooks/lib/cursor-workspace.js +74 -0
  153. package/package.json +7 -7
  154. package/pi/gsd.cjs +26 -1
  155. package/scripts/check-coverage-gate.cjs +51 -0
  156. package/scripts/check-glossary-refs.cjs +24 -0
  157. package/scripts/ci-test-scope.cjs +67 -17
  158. package/scripts/gen-adr-index.cjs +6 -4
  159. package/scripts/gen-capability-matrix.cjs +26 -2
  160. package/scripts/gen-capability-registry.cjs +132 -34
  161. package/scripts/gen-emitted-baseline.cjs +145 -0
  162. package/scripts/gen-registry.cjs +39 -15
  163. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  164. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  165. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  166. package/scripts/lint-resolution-provenance.cjs +9 -0
  167. package/scripts/mutation-matrix.cjs +4 -0
  168. package/scripts/prompt-injection-scan.sh +6 -0
  169. package/scripts/registry-schema.cjs +372 -94
  170. package/scripts/release-notes/conventional-title.cjs +19 -1
  171. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  172. package/scripts/validate-registry.cjs +10 -6
  173. package/scripts/workflow-size.cjs +16 -8
  174. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  175. package/vscode/package.json +1 -1
  176. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  177. package/scripts/update-size-baseline.cjs +0 -68
@@ -19,6 +19,8 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
19
19
  // providing a deterministic failure path when git stalls (locked index, hung
20
20
  // remote, stalled NFS mount, etc.). Callers can override via deps.timeout.
21
21
  const DEFAULT_GIT_TIMEOUT_MS = 10000;
22
+ const WORKTREE_AGENT_BRANCH_RE = /^(worktree-)?agent-[A-Za-z0-9._/-]+$/;
23
+ const WORKTREE_AGENT_BRANCH_PATTERN = WORKTREE_AGENT_BRANCH_RE.source;
22
24
  /**
23
25
  * Execute a git command via the shell-projection seam, with a derived
24
26
  * `timedOut` field. Tests inject mocks via deps.execGit using the new
@@ -302,7 +304,7 @@ function normalizeCleanupManifestEntry(entry) {
302
304
  const expectedBase = typeof e.expected_base === 'string' ? e.expected_base : '';
303
305
  if (!worktreePath || !branch || !expectedBase)
304
306
  return null;
305
- if (!/^worktree-agent-[A-Za-z0-9._/-]+$/.test(branch))
307
+ if (!WORKTREE_AGENT_BRANCH_RE.test(branch))
306
308
  return null;
307
309
  const rawAllowedBases = Array.isArray(e.allowed_bases) ? e.allowed_bases : [];
308
310
  const allowedBases = Array.from(new Set([expectedBase, ...rawAllowedBases.filter((base) => typeof base === 'string' && base.length > 0)]));
@@ -449,20 +451,18 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
449
451
  // the executor's content could be lost. cat-file -e HEAD:<path> returns
450
452
  // exit 0 only when the object exists in the committed HEAD tree.
451
453
  //
452
- // Fail-closed on timeout/fatal git errors: if we cannot determine whether
453
- // the file is committed, do NOT rescue it (rescuing an actually-committed
454
- // file would re-create the untracked collision; the merge will surface the
455
- // issue). The cleanup will be blocked by merge_failed in the worst case,
456
- // which is the observable behaviour before this fix and is recoverable.
454
+ // #2556: rescue whenever the object is NOT confirmed committed (any non-zero
455
+ // exit). `git cat-file -e` returns 128 — NOT 1 — for an absent path (the
456
+ // normal uncommitted-SUMMARY state), so the previous `!== 1` check never
457
+ // rescued and the untracked file was silently discarded by `worktree remove
458
+ // --force`. Data safety wins: an un-rescued untracked SUMMARY is lost, while
459
+ // a spurious rescue is usually a no-op — the destination check below skips the
460
+ // copy when the main tree already holds identical content (which is also what
461
+ // guards the #706 merge collision). A divergent dest is overwritten, but only
462
+ // uncommitted main-tree content could be lost (committed content is git-recoverable).
457
463
  const catFileResult = execGit(['-C', worktreePath, 'cat-file', '-e', `HEAD:${relPath}`], { cwd: repoRoot });
458
- if (catFileResult.exitCode !== 1) {
459
- // Rescue only when cat-file definitively reports the object is absent (exit 1).
460
- // exit 0 → object exists (committed on HEAD) — merge will carry it, skip.
461
- // exit 128 → fatal git error (corrupt store, unborn HEAD, etc.) — uncertain,
462
- // fail-closed: do NOT rescue to avoid recreating the #706 collision.
463
- // timedOut / null / other → unreliable result — same fail-closed policy.
464
- // In all non-1 cases the merge will either succeed naturally (0) or surface
465
- // the problem safely (128/timeout), which is the recoverable pre-fix behaviour.
464
+ if (catFileResult.exitCode === 0) {
465
+ // exit 0 → the SUMMARY is committed on HEAD; the merge will carry it, so skip rescue.
466
466
  continue;
467
467
  }
468
468
  const dest = node_path_1.default.join(repoRoot, relPath);
@@ -751,7 +751,7 @@ function planWorktreeRecordAgent(manifestRaw, fields) {
751
751
  return {
752
752
  ok: false,
753
753
  reason: 'invalid_entry',
754
- hint: `Entry failed cleanup-manifest validation: --path/--branch/--base must be non-empty and --branch must match ^worktree-agent-[A-Za-z0-9._/-]+$ (got branch="${branch}"). Fix the field and re-run.`,
754
+ hint: `Entry failed cleanup-manifest validation: --path/--branch/--base must be non-empty and --branch must match ${WORKTREE_AGENT_BRANCH_PATTERN} (accepts both agent-<id> and worktree-agent-<id> namespaces; got branch="${branch}"). Fix the field and re-run.`,
755
755
  entry: null,
756
756
  manifest: null,
757
757
  };
@@ -893,6 +893,348 @@ function cmdWorktreeRecordAgent(cwd, args = [], deps = {}) {
893
893
  write(`${JSON.stringify({ ok: true, reason: 'ok', entry: plan.entry, manifest_path: resolved }, null, 2)}\n`);
894
894
  return { ok: true, reason: 'ok', entry: plan.entry, manifest_path: resolved };
895
895
  }
896
+ /**
897
+ * Pure planner for `worktree create`. Validates the four required fields
898
+ * (write-strict, same missing-field-hint style as `planWorktreeRecordAgent`),
899
+ * then runs the candidate entry through the SAME `normalizeCleanupManifestEntry`
900
+ * validation the cleanup-wave reader and record-agent use — so a worktree this
901
+ * verb creates is guaranteed manageable by cleanup-wave/reap-orphans, and an
902
+ * entry that would fail the reader's branch-namespace guard is rejected here,
903
+ * fail-closed, before any git command runs.
904
+ */
905
+ function planWorktreeCreate(fields) {
906
+ const agentId = (fields.agentId || '').trim();
907
+ const worktreePath = (fields.worktreePath || '').trim();
908
+ const branch = (fields.branch || '').trim();
909
+ const base = (fields.base || '').trim();
910
+ const missing = [];
911
+ if (!agentId)
912
+ missing.push('--agent-id');
913
+ if (!worktreePath)
914
+ missing.push('--path');
915
+ if (!branch)
916
+ missing.push('--branch');
917
+ if (!base)
918
+ missing.push('--base');
919
+ if (missing.length > 0) {
920
+ return {
921
+ ok: false,
922
+ reason: 'missing_field',
923
+ hint: `worktree create requires ${missing.join(', ')}. Re-run with all of --agent-id, --path, --branch, --base set to non-empty (non-whitespace) values.`,
924
+ entry: null,
925
+ };
926
+ }
927
+ const candidate = {
928
+ agent_id: agentId,
929
+ worktree_path: worktreePath,
930
+ branch,
931
+ expected_base: base,
932
+ };
933
+ const entry = normalizeCleanupManifestEntry(candidate);
934
+ if (!entry) {
935
+ return {
936
+ ok: false,
937
+ reason: 'invalid_entry',
938
+ hint: `Entry failed cleanup-manifest validation: --path/--branch/--base must be non-empty and --branch must match ${WORKTREE_AGENT_BRANCH_PATTERN} (accepts both agent-<id> and worktree-agent-<id> namespaces; got branch="${branch}"). Fix the field and re-run.`,
939
+ entry: null,
940
+ };
941
+ }
942
+ // #2584 FIX 4 — git argument-injection guard: a value starting with '-'
943
+ // could be parsed by git as a FLAG rather than a positional argument (e.g.
944
+ // base="--upload-pack=x", path="-f"). `git worktree add` / `git rev-parse`
945
+ // support for a `--` end-of-options separator is inconsistent across git
946
+ // versions, so rejecting a leading dash outright — not relying on `--` — is
947
+ // the portable fix.
948
+ if (branch.startsWith('-') || base.startsWith('-') || worktreePath.startsWith('-')) {
949
+ return {
950
+ ok: false,
951
+ reason: 'unsafe_leading_dash',
952
+ hint: `--branch/--base/--path must not start with "-" (a leading dash would be parsed by git as a flag, not a value). Got branch="${branch}" base="${base}" path="${worktreePath}".`,
953
+ entry: null,
954
+ };
955
+ }
956
+ // #2584 FIX 4 — path-traversal guard: reject a ".." path segment in --path.
957
+ // Absolute paths ARE allowed (the orchestrator legitimately uses them —
958
+ // Phase-3 root confinement is out of Phase-2 scope); only a literal ".."
959
+ // component is rejected. Split on BOTH separators so the guard is effective
960
+ // on a Windows-style path too.
961
+ if (worktreePath.split(/[/\\]/).includes('..')) {
962
+ return {
963
+ ok: false,
964
+ reason: 'unsafe_path_traversal',
965
+ hint: `--path must not contain a ".." path segment (got: "${worktreePath}").`,
966
+ entry: null,
967
+ };
968
+ }
969
+ return { ok: true, reason: 'ok', entry };
970
+ }
971
+ /**
972
+ * Best-effort bounded rollback of a partial/orphaned worktree (#2584 FIX 3,
973
+ * scope narrowed by FIX 5). Invoked ONLY when a `git worktree add` TIMED OUT
974
+ * mid-operation — a SIGTERM'd `add` can leave a `.git/worktrees/<name>` admin
975
+ * entry / directory on disk that got past validation into the file checkout,
976
+ * so the partial is genuinely THIS call's own creation and is safe to
977
+ * best-effort remove immediately. It is deliberately NOT invoked on a clean
978
+ * non-zero `add` exit (see FIX 5) — the most common such failure is a
979
+ * COLLISION (the path/branch is already a registered worktree), git fails
980
+ * FAST there having created nothing, and the branch namespace this verb
981
+ * writes into (`worktree-agent-*`/`agent-*`) is exactly the concurrent-
982
+ * executor namespace, so a colliding path is very plausibly a LIVE PEER
983
+ * executor whose uncommitted work `--force` would destroy. Also invoked from
984
+ * `cmdWorktreeCreate` when a successful `add` is followed by a manifest-write
985
+ * failure (#2584 FIX 1) — that path proves THIS call created the worktree, so
986
+ * removing it is safe. This is immediate best-effort hygiene, not the only
987
+ * safety net: `reapOrphanWorktrees` scans the `.git/worktrees/` admin
988
+ * directory directly (a genuine directory-scan backstop, not manifest-only),
989
+ * so any partial this call cannot reach is still eventually discovered and
990
+ * reaped there. The result is intentionally ignored and a throw is
991
+ * swallowed: this is best-effort cleanup, never a new source of truth, and
992
+ * must never mask or block the caller's own degraded-but-honest return.
993
+ */
994
+ function rollbackPartialWorktree(execGit, worktreePath, repoRoot) {
995
+ try {
996
+ execGit(['worktree', 'remove', '--force', worktreePath], { cwd: repoRoot });
997
+ }
998
+ catch {
999
+ // best-effort only — a throwing rollback must never mask the original failure.
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Execute a `planWorktreeCreate` plan via bounded git. Fail-closed at every
1004
+ * step — a timeout or a non-zero exit degrades to a structured result rather
1005
+ * than throwing, and the base must resolve BEFORE any worktree is created (no
1006
+ * partial/orphaned worktree on a bad base). Returns `cwd` — the working
1007
+ * directory Phase 3's executor spawn will pass through.
1008
+ */
1009
+ function executeWorktreeCreatePlan(plan, repoRoot, deps = {}) {
1010
+ const execGit = deps.execGit || execGitDefault;
1011
+ if (!plan || !plan.ok || !plan.entry) {
1012
+ return {
1013
+ ok: false,
1014
+ reason: plan ? plan.reason : 'missing_plan',
1015
+ };
1016
+ }
1017
+ const { worktree_path: worktreePath, branch, expected_base: base } = plan.entry;
1018
+ const normalizedPath = (0, shell_command_projection_cjs_1.posixNormalize)(worktreePath);
1019
+ // 1. Verify the base resolves BEFORE creating anything (fail-closed). Nothing
1020
+ // has been created on this path yet, so there is nothing to roll back.
1021
+ const baseCheck = execGit(['rev-parse', '--verify', '--quiet', `${base}^{commit}`], { cwd: repoRoot });
1022
+ if (baseCheck.timedOut) {
1023
+ return { ok: false, reason: 'git_timeout', worktree_path: normalizedPath, branch, base };
1024
+ }
1025
+ if (!gitResultOk(baseCheck)) {
1026
+ return { ok: false, reason: 'base_unresolved', worktree_path: normalizedPath, branch, base, stderr: baseCheck.stderr || '' };
1027
+ }
1028
+ // 2. Create the worktree + branch together.
1029
+ const addResult = execGit(['worktree', 'add', '-b', branch, worktreePath, base], { cwd: repoRoot });
1030
+ if (addResult.timedOut) {
1031
+ // #2584 FIX 3: a SIGTERM'd `add` can leave a partial worktree on disk.
1032
+ rollbackPartialWorktree(execGit, worktreePath, repoRoot);
1033
+ return { ok: false, reason: 'git_timeout', worktree_path: normalizedPath, branch, base };
1034
+ }
1035
+ if (addResult.exitCode !== 0) {
1036
+ // #2584 FIX 5: deliberately NO rollback here. A clean non-zero exit is
1037
+ // most commonly a COLLISION (path/branch already a registered worktree),
1038
+ // and git fails FAST on that — it creates nothing. A colliding path in
1039
+ // this branch namespace is very plausibly a LIVE PEER executor;
1040
+ // `git worktree remove --force` on it would destroy real, uncommitted
1041
+ // work. The safe response to a clean failure is to fail loudly and leave
1042
+ // whatever is already on disk untouched.
1043
+ return { ok: false, reason: 'worktree_add_failed', worktree_path: normalizedPath, branch, base, stderr: addResult.stderr || '' };
1044
+ }
1045
+ return {
1046
+ ok: true,
1047
+ reason: 'created',
1048
+ worktree_path: normalizedPath,
1049
+ branch,
1050
+ base,
1051
+ cwd: normalizedPath,
1052
+ };
1053
+ }
1054
+ /**
1055
+ * CLI command: create a git worktree + branch off `--base`, then append the
1056
+ * validated manifest entry so the worktree is immediately manageable by
1057
+ * `worktree cleanup-wave` / `worktree reap-orphans`.
1058
+ *
1059
+ * Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha>
1060
+ *
1061
+ * #2584 FIX 1 — ORDERING CONTRACT: every manifest read/parse/shape-validate/
1062
+ * plan step runs BEFORE the git side effect (step 5). The ONLY manifest
1063
+ * operation that can run AFTER `git worktree add` has succeeded is the final
1064
+ * guarded write (step 6), and a failure there triggers a best-effort rollback
1065
+ * of the just-created worktree — so a malformed/mis-shaped manifest, or a
1066
+ * `writeFile` IO error, can never leave a REAL worktree on disk with no
1067
+ * manifest entry (cleanup-wave/reap-orphans only discover worktrees via the
1068
+ * manifest, never a directory scan) or an uncaught throw.
1069
+ */
1070
+ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
1071
+ const flag = (name) => {
1072
+ const i = args.indexOf(name);
1073
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : '';
1074
+ };
1075
+ const write = deps.write || ((s) => process.stdout.write(s));
1076
+ const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
1077
+ const manifestPath = flag('--manifest');
1078
+ if (!manifestPath) {
1079
+ writeErr('Usage: worktree create --manifest <path> --agent-id <id> --path <worktree> --branch <branch> --base <sha> [--root <dir>]\n');
1080
+ process.exitCode = 2;
1081
+ return { ok: false, reason: 'usage' };
1082
+ }
1083
+ // 1. Read the manifest (no side effect yet).
1084
+ const resolved = node_path_1.default.resolve(cwd, manifestPath);
1085
+ const readFile = deps.readFile || ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
1086
+ let manifestRaw;
1087
+ try {
1088
+ manifestRaw = readFile(resolved);
1089
+ }
1090
+ catch (err) {
1091
+ const hint = `Manifest not found or unreadable at ${manifestPath}. The orchestrator must initialize it ({"orchestrator_root": "...", "worktrees": []}) before creating agent worktrees.`;
1092
+ writeErr(`[gsd] worktree.create: manifest_read_failed — ${hint}\n`);
1093
+ write(`${JSON.stringify({ ok: false, reason: 'manifest_read_failed', hint, error: err.message }, null, 2)}\n`);
1094
+ process.exitCode = 1;
1095
+ return { ok: false, reason: 'manifest_read_failed', hint };
1096
+ }
1097
+ // 2. Parse + shape-validate the manifest BEFORE any git command runs.
1098
+ // Mirrors planWorktreeRecordAgent's shell-acceptance rules (canonical
1099
+ // {worktrees:[]} object OR a bare top-level array).
1100
+ let parsed;
1101
+ try {
1102
+ parsed = JSON.parse(manifestRaw);
1103
+ }
1104
+ catch {
1105
+ const hint = 'Manifest is not valid JSON. The orchestrator must initialize it as {"orchestrator_root": "...", "worktrees": []} before creating agent worktrees.';
1106
+ writeErr(`[gsd] worktree.create: invalid_manifest_json — ${hint}\n`);
1107
+ write(`${JSON.stringify({ ok: false, reason: 'invalid_manifest_json', hint }, null, 2)}\n`);
1108
+ process.exitCode = 1;
1109
+ return { ok: false, reason: 'invalid_manifest_json', hint };
1110
+ }
1111
+ let worktrees;
1112
+ let writeBack;
1113
+ if (Array.isArray(parsed)) {
1114
+ worktrees = parsed;
1115
+ writeBack = worktrees;
1116
+ }
1117
+ else if (parsed && typeof parsed === 'object') {
1118
+ const container = parsed;
1119
+ if (container.worktrees === undefined)
1120
+ container.worktrees = [];
1121
+ if (!Array.isArray(container.worktrees)) {
1122
+ const hint = 'Manifest "worktrees" must be an array. Re-initialize as {"orchestrator_root": "...", "worktrees": []}.';
1123
+ writeErr(`[gsd] worktree.create: manifest_shape_invalid — ${hint}\n`);
1124
+ write(`${JSON.stringify({ ok: false, reason: 'manifest_shape_invalid', hint }, null, 2)}\n`);
1125
+ process.exitCode = 1;
1126
+ return { ok: false, reason: 'manifest_shape_invalid', hint };
1127
+ }
1128
+ worktrees = container.worktrees;
1129
+ writeBack = container;
1130
+ }
1131
+ else {
1132
+ const hint = 'Manifest must be a JSON object {"worktrees": []} or a top-level array.';
1133
+ writeErr(`[gsd] worktree.create: manifest_shape_invalid — ${hint}\n`);
1134
+ write(`${JSON.stringify({ ok: false, reason: 'manifest_shape_invalid', hint }, null, 2)}\n`);
1135
+ process.exitCode = 1;
1136
+ return { ok: false, reason: 'manifest_shape_invalid', hint };
1137
+ }
1138
+ // 3. Plan (pure, no I/O) — still before any git command.
1139
+ const plan = planWorktreeCreate({
1140
+ agentId: flag('--agent-id'),
1141
+ worktreePath: flag('--path'),
1142
+ branch: flag('--branch'),
1143
+ base: flag('--base'),
1144
+ });
1145
+ if (!plan.ok || !plan.entry) {
1146
+ writeErr(`[gsd] worktree.create: ${plan.reason} — ${plan.hint || ''}\n`);
1147
+ write(`${JSON.stringify({ ok: false, reason: plan.reason, hint: plan.hint }, null, 2)}\n`);
1148
+ process.exitCode = 1;
1149
+ return { ok: false, reason: plan.reason, hint: plan.hint };
1150
+ }
1151
+ // 3b. Optional root confinement (#2627, Phase 3 — the confinement Phase 2
1152
+ // deferred here from planWorktreeCreate's path-traversal guard).
1153
+ // planWorktreeCreate rejects a literal ".." SEGMENT, but a plain absolute
1154
+ // path outside the project contains no ".." and passes. Phase 3 makes the
1155
+ // orchestrator SPAWN executor processes into these paths, so an
1156
+ // unconfined --path is a write primitive aimed anywhere on the filesystem.
1157
+ //
1158
+ // The root is DECLARED by the caller (`--root`) rather than inferred: agent
1159
+ // worktrees legitimately live outside the orchestrator's own root (a lane
1160
+ // orchestrator creates siblings under the repo's .claude/worktrees/), so
1161
+ // there is no layout this module could derive without guessing. Absent
1162
+ // `--root` the behavior is exactly as shipped in Phase 2 — the
1163
+ // orchestrator-worktree scheduler path always passes it.
1164
+ //
1165
+ // Lexical by design: the worktree does not exist yet, so there is nothing
1166
+ // to realpath, and resolving only the root would not close a symlinked-leaf
1167
+ // hole. Pairs with the leading-dash and ".."-segment guards above.
1168
+ const rootFlag = flag('--root');
1169
+ if (rootFlag) {
1170
+ const absRoot = node_path_1.default.resolve(cwd, rootFlag);
1171
+ const absWorktree = node_path_1.default.resolve(cwd, plan.entry.worktree_path);
1172
+ const rel = node_path_1.default.relative(absRoot, absWorktree);
1173
+ // rel === '' → the worktree IS the root (would clobber the checkout)
1174
+ // rel === '..' / '../…' → escapes the root
1175
+ // path.isAbsolute(rel) → a different Windows drive or UNC root
1176
+ if (rel === '' || rel === '..' || rel.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(rel)) {
1177
+ const hint = `--path must resolve INSIDE --root (root="${absRoot}", path="${absWorktree}"). A worktree outside the declared root is unreachable by manifest-scoped cleanup and would let a spawned executor write outside the project.`;
1178
+ writeErr(`[gsd] worktree.create: path_outside_root — ${hint}\n`);
1179
+ write(`${JSON.stringify({ ok: false, reason: 'path_outside_root', hint }, null, 2)}\n`);
1180
+ process.exitCode = 1;
1181
+ return { ok: false, reason: 'path_outside_root', hint };
1182
+ }
1183
+ }
1184
+ // 4. Compute the deduped final manifest STRING in memory now — the ONLY
1185
+ // manifest work left is the guarded write in step 6, after git succeeds.
1186
+ // #2584 FIX 2: the on-disk entry is the SAME minimal 4-field shape
1187
+ // `cmdWorktreeRecordAgent` writes — never the full normalized entry with
1188
+ // the derived `allowed_bases` — so the two verbs never write divergent
1189
+ // shapes into the same manifest (the reader re-derives `allowed_bases`
1190
+ // from `expected_base` on load, exactly as it does for record-agent).
1191
+ const recorded = {
1192
+ agent_id: plan.entry.agent_id,
1193
+ worktree_path: plan.entry.worktree_path,
1194
+ branch: plan.entry.branch,
1195
+ expected_base: plan.entry.expected_base,
1196
+ };
1197
+ const dedupeKey = `${recorded.worktree_path}\0${recorded.branch}`;
1198
+ const alreadyPresent = worktrees.some((existing) => {
1199
+ const normalized = normalizeCleanupManifestEntry(existing);
1200
+ return normalized !== null && `${normalized.worktree_path}\0${normalized.branch}` === dedupeKey;
1201
+ });
1202
+ if (!alreadyPresent) {
1203
+ worktrees.push(recorded);
1204
+ }
1205
+ const manifestToWrite = `${JSON.stringify(writeBack, null, 2)}\n`;
1206
+ // 5. NOW run the git side effect. Every manifest problem above is caught
1207
+ // before this point, so a malformed/mis-shaped manifest can never leave
1208
+ // an unmanifested worktree on disk. `executeWorktreeCreatePlan` itself
1209
+ // best-effort rolls back a partial worktree on its own add-timeout /
1210
+ // add-failed paths (#2584 FIX 3).
1211
+ const result = executeWorktreeCreatePlan(plan, cwd, deps);
1212
+ if (!result.ok) {
1213
+ writeErr(`[gsd] worktree.create: ${result.reason} — ${result.stderr || ''}\n`);
1214
+ write(`${JSON.stringify({ ok: false, reason: result.reason, stderr: result.stderr }, null, 2)}\n`);
1215
+ process.exitCode = 1;
1216
+ return { ok: false, reason: result.reason, stderr: result.stderr };
1217
+ }
1218
+ // 6. Write the pre-computed manifest string, guarded. A write failure here
1219
+ // means a REAL worktree now exists with NO manifest entry — roll it back
1220
+ // (best-effort) rather than leaving an orphan cleanup-wave/reap-orphans
1221
+ // can never reach (#2584 FIX 1). Never throws past this function.
1222
+ const writeFile = deps.writeFile || ((p, content) => node_fs_1.default.writeFileSync(p, content, 'utf8'));
1223
+ try {
1224
+ writeFile(resolved, manifestToWrite);
1225
+ }
1226
+ catch (err) {
1227
+ const execGit = deps.execGit || execGitDefault;
1228
+ rollbackPartialWorktree(execGit, plan.entry.worktree_path, cwd);
1229
+ const hint = `The worktree was created but the manifest write failed (${err.message}); rolled back the worktree via a best-effort 'git worktree remove --force'.`;
1230
+ writeErr(`[gsd] worktree.create: manifest_write_failed — ${hint}\n`);
1231
+ write(`${JSON.stringify({ ok: false, reason: 'manifest_write_failed', hint, error: err.message }, null, 2)}\n`);
1232
+ process.exitCode = 1;
1233
+ return { ok: false, reason: 'manifest_write_failed', hint };
1234
+ }
1235
+ write(`${JSON.stringify({ ok: true, reason: 'created', entry: recorded, cwd: result.cwd, manifest_path: resolved }, null, 2)}\n`);
1236
+ return { ok: true, reason: 'created', entry: recorded, cwd: result.cwd, manifest_path: resolved };
1237
+ }
896
1238
  /**
897
1239
  * Reap orphaned linked worktrees whose lock owner process is dead, whose
898
1240
  * branch tip is fully merged into the default branch, and whose lock file
@@ -1180,6 +1522,9 @@ module.exports = {
1180
1522
  cmdWorktreeCleanupWave,
1181
1523
  planWorktreeRecordAgent,
1182
1524
  cmdWorktreeRecordAgent,
1525
+ planWorktreeCreate,
1526
+ executeWorktreeCreatePlan,
1527
+ cmdWorktreeCreate,
1183
1528
  reapOrphanWorktrees,
1184
1529
  cmdWorktreeReapOrphans,
1185
1530
  resolveWorktreeRoot,
@@ -33,6 +33,7 @@
33
33
  "_auto_chain_active": false,
34
34
  "node_repair": true,
35
35
  "node_repair_budget": 2,
36
+ "smart_zone_tokens": 100000,
36
37
  "ui_phase": true,
37
38
  "ui_safety_gate": true,
38
39
  "text_mode": false,
@@ -19,6 +19,7 @@
19
19
  "workflow.auto_advance",
20
20
  "workflow.node_repair",
21
21
  "workflow.node_repair_budget",
22
+ "workflow.smart_zone_tokens",
22
23
  "workflow.human_verify_mode",
23
24
  "workflow.text_mode",
24
25
  "workflow.research_before_questions",
@@ -48,9 +49,6 @@
48
49
  "planning.commit_docs",
49
50
  "planning.search_gitignored",
50
51
  "planning.sub_repos",
51
- "review.ollama_host",
52
- "review.lm_studio_host",
53
- "review.llama_cpp_host",
54
52
  "review.default_reviewers",
55
53
  "review.max_prompt_tokens",
56
54
  "review.max_prompt_tokens_per_reviewer",
@@ -117,11 +115,6 @@
117
115
  "source": "^agent_skills\\.[a-zA-Z0-9_-]+$",
118
116
  "description": "agent_skills.<agent-type>"
119
117
  },
120
- {
121
- "topLevel": "review",
122
- "source": "^review\\.models\\.[a-zA-Z0-9_-]+$",
123
- "description": "review.models.<cli-name>"
124
- },
125
118
  {
126
119
  "topLevel": "features",
127
120
  "source": "^features\\.[a-zA-Z0-9_]+$",
@@ -177,11 +170,6 @@
177
170
  "source": "^fast_mode\\.agent_overrides\\.[a-zA-Z0-9_-]+$",
178
171
  "description": "fast_mode.agent_overrides.<agent-id>"
179
172
  },
180
- {
181
- "topLevel": "review",
182
- "source": "^review\\.max_prompt_tokens_per_reviewer\\.[a-zA-Z0-9_-]+$",
183
- "description": "review.max_prompt_tokens_per_reviewer.<reviewer-slug>"
184
- },
185
173
  {
186
174
  "topLevel": "review",
187
175
  "source": "^review\\.reviewer_instances\\.[a-zA-Z0-9_-]+\\.(cli|model|agent)$",
@@ -52,6 +52,11 @@
52
52
  "sonnet": null,
53
53
  "haiku": null
54
54
  },
55
+ "kimi-code": {
56
+ "opus": null,
57
+ "sonnet": null,
58
+ "haiku": null
59
+ },
55
60
  "cursor": {
56
61
  "opus": null,
57
62
  "sonnet": null,
@@ -63,6 +63,11 @@
63
63
  "kimi": [
64
64
  "kimi"
65
65
  ],
66
+ "kimi-code": [
67
+ "kimi-code",
68
+ "kimicode",
69
+ "kimi_code"
70
+ ],
66
71
  "codebuddy": [
67
72
  "codebuddy",
68
73
  "codebuddy-cli"
@@ -83,3 +83,43 @@ Either list works — `enabledMcpjsonServers` is an explicit allow-list, `disabl
83
83
  ### Composition with model_profile
84
84
 
85
85
  Trimming MCPs and tuning `model_profile` are independent levers that **compound**. Disabling a 25k-token MCP saves 25k per turn whether you're running `quality` (opus everywhere) or `budget` (sonnet/haiku); the savings are additive, not in lieu of model tuning. Don't pick one — do both, and audit MCPs first because the per-turn savings show up immediately and stack across every subagent the orchestrator spawns.
86
+
87
+ ---
88
+
89
+ # Phase Sizing (gsd-planner)
90
+
91
+ ## Estimate Emission (#2631, ADR-2629)
92
+
93
+ Every plan carries an `estimate` block. It is the quantitative reason a phase must be sliced — tracer-first says *slice thin*, the estimate says *how thin, for this codebase*.
94
+
95
+ **Compute it:**
96
+ 1. Sum `estimateTokens`-scale cost across the plan: implementation + the files each task reads + verification output. Roughly chars/4 over what the executor will actually touch.
97
+ 2. Run `estimate-calibration` and **multiply your raw figure by its `factor`.** It is the measured estimate-vs-actual ratio for THIS project — a factor of 1 means there is not yet enough history to correct.
98
+ 3. `confidence` is **derived, not judged**: it is the `confidence` value from the same calibration query, keyed to the sample count (`low` <3, `med` 3–5, `high` ≥6). **Do not rate your own certainty.** Self-rated confidence was measured in this project and found weak (`references/honest-verifier.md:25-29`); every signal here routes on measured history instead.
99
+
100
+ **Over budget?** The plan-checker flags a plan whose estimate exceeds `workflow.smart_zone_tokens`. This is advisory — it never blocks. When flagged, re-slice: a tracer plus expansion slices, each inside the budget. Prefer more, smaller plans over one that spends the agent's best early-context tokens and finishes degraded.
101
+
102
+ ## Context Budget Rules
103
+
104
+ Plans should complete within ~50% context (not 80%). No context anxiety, quality maintained start to finish, room for unexpected complexity.
105
+
106
+ **Each plan: 2-3 tasks maximum.**
107
+
108
+ | Context Weight | Tasks/Plan | Context/Task | Total |
109
+ |----------------|------------|--------------|-------|
110
+ | Light (CRUD, config) | 3 | ~10-15% | ~30-45% |
111
+ | Medium (auth, payments) | 2 | ~20-30% | ~40-50% |
112
+ | Heavy (migrations, multi-subsystem) | 1-2 | ~30-40% | ~30-50% |
113
+
114
+ ## Split Signals
115
+
116
+ **ALWAYS split if:**
117
+ - More than 3 tasks
118
+ - Multiple subsystems (DB + API + UI = separate plans)
119
+ - Any task with >5 file modifications
120
+ - Checkpoint + implementation in same plan
121
+ - Discovery + implementation in same plan
122
+
123
+ **CONSIDER splitting:** >5 files total, natural semantic boundaries, context cost estimate exceeds 40% for a single plan. See `<planner_authority_limits>` for prohibited split reasons.
124
+
125
+ See @~/.claude/gsd-core/references/planner-guidance.md for Granularity Calibration table (Coarse/Standard/Fine plans-per-phase).
@@ -90,11 +90,14 @@ Up to 4 suggested next actions with selection (status, resume workflows).
90
90
  3-option handler for existing CONTEXT.md in discuss workflow.
91
91
  - question: "Phase {N} already has a CONTEXT.md. How should we handle it?"
92
92
  - header: "Context"
93
- - options: Overwrite | Append | Cancel
93
+ - options: Update it | View it | Skip
94
94
 
95
95
  ## Pattern: gray-area-option
96
96
  Dynamic template for presenting gray area choices in discuss workflow.
97
97
  - question: "{Gray area title}"
98
98
  - header: "Decision"
99
- - options: {Option 1} | {Option 2} | Let Claude decide
100
- - Note: Options generated at runtime. Always include "Let Claude decide" as last option.
99
+ - options: {Option 1} | {Option 2}
100
+ - Note: Options generated at runtime. Present real choices only — do NOT include a
101
+ "skip" or "you decide" option (the user ran discuss-phase to decide). An "Other"
102
+ free-text escape hatch may be offered per modes/default.md, but never a
103
+ "Let Claude decide" cop-out.