@opengsd/gsd-pi 1.2.0-dev.d6c5343c → 1.2.0-dev.e8563f58

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 (132) hide show
  1. package/dist/mcp-server.js +2 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/orchestrator.js +28 -10
  4. package/dist/resources/extensions/gsd/auto-model-selection.js +11 -7
  5. package/dist/resources/extensions/gsd/auto.js +7 -0
  6. package/dist/resources/extensions/gsd/blocked-models.js +28 -0
  7. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +26 -6
  8. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  9. package/dist/resources/extensions/gsd/closeout-wizard.js +92 -0
  10. package/dist/resources/extensions/gsd/commands-handlers.js +46 -3
  11. package/dist/resources/extensions/gsd/consent-question.js +16 -0
  12. package/dist/resources/extensions/gsd/doctor-git-checks.js +2 -18
  13. package/dist/resources/extensions/gsd/gsd-command-home.js +22 -12
  14. package/dist/resources/extensions/gsd/gsd-db.js +2 -1
  15. package/dist/resources/extensions/gsd/milestone-closeout.js +73 -2
  16. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +67 -2
  17. package/dist/resources/extensions/shared/gsd-browser-cli.js +21 -2
  18. package/dist/resources/shared/gsd-browser-path-sync.js +214 -0
  19. package/dist/resources/shared/package-manager-detection.js +1 -1
  20. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  21. package/dist/update-check.d.ts +2 -0
  22. package/dist/update-check.js +24 -1
  23. package/dist/update-cmd.js +20 -3
  24. package/dist/web/standalone/.next/BUILD_ID +1 -1
  25. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  26. package/dist/web/standalone/.next/build-manifest.json +2 -2
  27. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  28. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.html +1 -1
  45. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  52. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  53. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  55. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  56. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  57. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  58. package/package.json +1 -1
  59. package/packages/cloud-mcp-gateway/package.json +2 -2
  60. package/packages/contracts/package.json +1 -1
  61. package/packages/daemon/package.json +4 -4
  62. package/packages/gsd-agent-core/package.json +5 -5
  63. package/packages/gsd-agent-modes/package.json +7 -7
  64. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts +29 -0
  65. package/packages/mcp-server/dist/moonshot-tool-schema.d.ts.map +1 -0
  66. package/packages/mcp-server/dist/moonshot-tool-schema.js +50 -0
  67. package/packages/mcp-server/dist/moonshot-tool-schema.js.map +1 -0
  68. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  69. package/packages/mcp-server/dist/server.js +4 -0
  70. package/packages/mcp-server/dist/server.js.map +1 -1
  71. package/packages/mcp-server/dist/workflow-tools.d.ts +18 -18
  72. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  73. package/packages/mcp-server/dist/workflow-tools.js +99 -38
  74. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  75. package/packages/mcp-server/package.json +5 -4
  76. package/packages/native/package.json +1 -1
  77. package/packages/pi-agent-core/package.json +1 -1
  78. package/packages/pi-ai/dist/index.d.ts +2 -0
  79. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  80. package/packages/pi-ai/dist/index.js +2 -0
  81. package/packages/pi-ai/dist/index.js.map +1 -1
  82. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  83. package/packages/pi-ai/dist/providers/anthropic.js +12 -7
  84. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  85. package/packages/pi-ai/dist/providers/google-shared.d.ts +5 -0
  86. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  87. package/packages/pi-ai/dist/providers/google-shared.js +12 -3
  88. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  89. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  90. package/packages/pi-ai/dist/providers/openai-completions.js +7 -3
  91. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  92. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts +9 -0
  93. package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts.map +1 -0
  94. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js +34 -0
  95. package/packages/pi-ai/dist/utils/moonshot-tool-schema.js.map +1 -0
  96. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  97. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +6 -2
  98. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  99. package/packages/pi-ai/package.json +1 -1
  100. package/packages/pi-coding-agent/package.json +7 -7
  101. package/packages/pi-tui/package.json +2 -2
  102. package/packages/rpc-client/package.json +2 -2
  103. package/pkg/package.json +1 -1
  104. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +11 -0
  105. package/src/resources/extensions/gsd/auto/orchestrator.ts +28 -10
  106. package/src/resources/extensions/gsd/auto-model-selection.ts +16 -7
  107. package/src/resources/extensions/gsd/auto.ts +8 -0
  108. package/src/resources/extensions/gsd/blocked-models.ts +49 -0
  109. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +34 -5
  110. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  111. package/src/resources/extensions/gsd/closeout-wizard.ts +102 -0
  112. package/src/resources/extensions/gsd/commands-handlers.ts +46 -3
  113. package/src/resources/extensions/gsd/consent-question.ts +15 -0
  114. package/src/resources/extensions/gsd/doctor-git-checks.ts +2 -19
  115. package/src/resources/extensions/gsd/gsd-command-home.ts +13 -3
  116. package/src/resources/extensions/gsd/gsd-db.ts +4 -3
  117. package/src/resources/extensions/gsd/milestone-closeout.ts +97 -2
  118. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +69 -0
  119. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +97 -0
  120. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +19 -0
  121. package/src/resources/extensions/gsd/tests/consent-question.test.ts +15 -0
  122. package/src/resources/extensions/gsd/tests/doctor-git-checks-terminal.test.ts +73 -0
  123. package/src/resources/extensions/gsd/tests/gsd-command-home.test.ts +120 -0
  124. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +95 -4
  125. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
  126. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +273 -38
  127. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +81 -2
  128. package/src/resources/extensions/shared/gsd-browser-cli.ts +23 -2
  129. package/src/resources/shared/gsd-browser-path-sync.ts +273 -0
  130. package/src/resources/shared/package-manager-detection.ts +1 -1
  131. /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → LDHRKiRBIVZmiuMjrL1Vy}/_buildManifest.js +0 -0
  132. /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → LDHRKiRBIVZmiuMjrL1Vy}/_ssgManifest.js +0 -0
@@ -28,13 +28,17 @@ export function buildGsdHomeModel(state, closeout) {
28
28
  const workLabel = activeWorkLabel(state);
29
29
  const strandedQuick = closeout?.strandedQuick ?? null;
30
30
  const unmergedMilestone = closeout?.unmergedMilestones?.[0];
31
+ const idleResidueHint = closeout?.idleResidueHint ?? null;
32
+ const hasIdleResidue = Boolean(idleResidueHint);
31
33
  const nextReason = complete
32
34
  ? "all milestones are complete"
33
35
  : blocked
34
36
  ? "the active milestone is blocked"
35
- : !hasActiveWork
36
- ? "there is no active milestone"
37
- : "";
37
+ : hasIdleResidue
38
+ ? "milestone git residue needs recovery"
39
+ : !hasActiveWork
40
+ ? "there is no active milestone"
41
+ : "";
38
42
  const canAdvance = hasActiveWork && !blocked && !complete;
39
43
  const unmappedActive = complete ? countUnmappedActiveRequirements() : 0;
40
44
  const recommended = strandedQuick
@@ -43,11 +47,13 @@ export function buildGsdHomeModel(state, closeout) {
43
47
  ? "finish_milestone"
44
48
  : blocked
45
49
  ? "fix_recover"
46
- : canAdvance
47
- ? "continue_step"
48
- : complete && unmappedActive > 0
49
- ? "review_requirements_backlog"
50
- : "start_configure";
50
+ : hasIdleResidue
51
+ ? "fix_recover"
52
+ : canAdvance
53
+ ? "continue_step"
54
+ : complete && unmappedActive > 0
55
+ ? "review_requirements_backlog"
56
+ : "start_configure";
51
57
  const completionSummary = complete
52
58
  ? appendRequirementsBacklogToSummary(state, [
53
59
  `All milestones complete${state.lastCompletedMilestone ? ` after ${state.lastCompletedMilestone.id}: ${state.lastCompletedMilestone.title}` : ""}.`,
@@ -57,7 +63,9 @@ export function buildGsdHomeModel(state, closeout) {
57
63
  ? [`Quick task Q${strandedQuick.taskNum} finished on ${strandedQuick.quickBranch} but is not merged to ${strandedQuick.originalBranch}.`]
58
64
  : unmergedMilestone
59
65
  ? [`${unmergedMilestone.milestoneId} is complete but not merged into ${unmergedMilestone.integrationBranch}.`]
60
- : completionSummary;
66
+ : idleResidueHint
67
+ ? [idleResidueHint.message]
68
+ : completionSummary;
61
69
  return {
62
70
  title: "GSD — What now?",
63
71
  summary: [
@@ -130,10 +138,12 @@ export function buildGsdHomeModel(state, closeout) {
130
138
  label: "Fix or recover",
131
139
  description: blocked
132
140
  ? "Review the blocker and recovery commands for the active milestone."
133
- : disabled("This becomes active when closeout, validation, or state recovery is needed.", "no blocker is active"),
134
- enabled: blocked,
141
+ : hasIdleResidue
142
+ ? "Review stranded milestone worktrees/branches and run the suggested recovery command."
143
+ : disabled("This becomes active when closeout, validation, or state recovery is needed.", "no blocker is active"),
144
+ enabled: blocked || hasIdleResidue,
135
145
  recommended: recommended === "fix_recover",
136
- disabledReason: blocked ? undefined : "no blocker is active",
146
+ disabledReason: blocked || hasIdleResidue ? undefined : "no blocker is active",
137
147
  },
138
148
  {
139
149
  id: "start_configure",
@@ -169,7 +169,7 @@ export function insertArtifact(a) {
169
169
  export function insertMilestone(m) {
170
170
  if (!getDbOrNull())
171
171
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
172
- getDbOrNull().prepare(`INSERT OR IGNORE INTO milestones (
172
+ const result = getDbOrNull().prepare(`INSERT OR IGNORE INTO milestones (
173
173
  id, title, status, depends_on, created_at,
174
174
  vision, success_criteria, key_risks, proof_strategy,
175
175
  verification_contract, verification_integration, verification_operational, verification_uat,
@@ -199,6 +199,7 @@ export function insertMilestone(m) {
199
199
  ":requirement_coverage": m.planning?.requirementCoverage ?? "",
200
200
  ":boundary_map_markdown": m.planning?.boundaryMapMarkdown ?? "",
201
201
  });
202
+ return (result.changes ?? 0) > 0;
202
203
  }
203
204
  export function upsertMilestonePlanning(milestoneId, planning) {
204
205
  if (!getDbOrNull())
@@ -4,10 +4,13 @@
4
4
  // - preflight: dispatch git clean before complete-milestone agent (auto-dispatch)
5
5
  // - postUnit: git commit, artifact verify, DB settle, then GitHub finalize
6
6
  // - recovery: DB repair from artifacts, then GitHub finalize
7
+ import { existsSync } from "node:fs";
7
8
  import { loadFile } from "./files.js";
8
9
  import { resolveMilestoneFile } from "./paths.js";
9
- import { getMilestone, getClosedSliceIds, isDbAvailable } from "./gsd-db.js";
10
+ import { getMilestone, getClosedSliceIds, getLatestAssessmentByScope, getMilestoneSlices, isDbAvailable, } from "./gsd-db.js";
10
11
  import { isClosedStatus } from "./status-guards.js";
12
+ import { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
13
+ import { handleCompleteMilestone } from "./tools/complete-milestone.js";
11
14
  import { runSafely } from "./auto-utils.js";
12
15
  import { extractVerdict, isAcceptableUatVerdict } from "./verdict-parser.js";
13
16
  import { uatSignoffBlockerGuidance } from "./guidance.js";
@@ -19,6 +22,60 @@ import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
19
22
  import { commitPendingMilestoneCloseoutChanges, findMissingSummaries, isVerificationNotApplicable, readUatGateVerdict, } from "./auto-dispatch.js";
20
23
  const COMPLETE_MILESTONE_DB_SETTLE_MS = 1500;
21
24
  const COMPLETE_MILESTONE_DB_SETTLE_POLL_MS = 100;
25
+ /**
26
+ * True when a milestone is terminal for git cleanup (orphaned worktrees, stale branches).
27
+ * DB-authoritative (ADR-017): closed status, or validation-pass with all slices closed.
28
+ * When the DB is unavailable we cannot make this decision and conservatively
29
+ * return false so callers leave the worktree/branch alone instead of cleaning
30
+ * up based on parsed projections.
31
+ */
32
+ export async function isCompletedMilestoneTerminal(_basePath, milestoneId) {
33
+ if (!isDbAvailable())
34
+ return false;
35
+ const milestone = getMilestone(milestoneId);
36
+ if (!milestone)
37
+ return false;
38
+ if (isClosedStatus(milestone.status)) {
39
+ return true;
40
+ }
41
+ const validation = getLatestAssessmentByScope(milestoneId, "milestone-validation");
42
+ if (validation?.status !== "pass") {
43
+ return false;
44
+ }
45
+ const slices = getMilestoneSlices(milestoneId);
46
+ if (slices.length === 0)
47
+ return false;
48
+ return slices.every((slice) => isClosedStatus(slice.status));
49
+ }
50
+ /** Write a missing milestone SUMMARY projection when canonical DB closeout already settled. */
51
+ export async function repairMissingMilestoneSummaryProjection(basePath, milestoneId) {
52
+ const milestone = getMilestone(milestoneId);
53
+ if (!milestone) {
54
+ return { ok: false, error: `milestone not found: ${milestoneId}` };
55
+ }
56
+ const artifactBasePath = resolveCanonicalMilestoneRoot(basePath, milestoneId);
57
+ const summaryPath = resolveExpectedArtifactPath("complete-milestone", milestoneId, artifactBasePath);
58
+ if (summaryPath && existsSync(summaryPath)) {
59
+ return { ok: true };
60
+ }
61
+ const result = await handleCompleteMilestone({
62
+ milestoneId,
63
+ title: milestone.title,
64
+ oneLiner: "Canonical closeout completed; summary projection repaired automatically.",
65
+ narrative: "The workflow database recorded this milestone as complete, but the milestone SUMMARY artifact was missing on disk. " +
66
+ "Dispatch policy repaired the projection so closeout proof and cleanup can proceed.",
67
+ verificationPassed: true,
68
+ triggerReason: "closeout-projection-repair",
69
+ }, basePath);
70
+ if ("error" in result) {
71
+ return { ok: false, error: result.error };
72
+ }
73
+ const writtenSummaryPath = result.summaryPath;
74
+ if (result.stale || !writtenSummaryPath || !existsSync(writtenSummaryPath)) {
75
+ return { ok: false, error: "milestone SUMMARY projection write failed" };
76
+ }
77
+ return { ok: true };
78
+ }
22
79
  /**
23
80
  * True when the milestone is closed in the DB and the completion summary artifact exists.
24
81
  * Polls briefly so post-unit verification can observe the tool's DB write.
@@ -65,7 +122,21 @@ export async function evaluateCompleteMilestoneDispatch(ctx) {
65
122
  if (isDbAvailable()) {
66
123
  const milestone = getMilestone(mid);
67
124
  if (milestone && isClosedStatus(milestone.status)) {
68
- return { action: "skip" };
125
+ const artifactBasePath = resolveCanonicalMilestoneRoot(basePath, mid);
126
+ const summaryPath = resolveExpectedArtifactPath("complete-milestone", mid, artifactBasePath);
127
+ const summaryMissing = !summaryPath || !existsSync(summaryPath);
128
+ if (summaryMissing) {
129
+ const repair = await repairMissingMilestoneSummaryProjection(basePath, mid);
130
+ if (!repair.ok) {
131
+ logWarning("dispatch", `Milestone ${mid} is closed in DB but SUMMARY repair failed: ${repair.error}. Dispatching complete-milestone to retry.`);
132
+ }
133
+ else {
134
+ return { action: "skip" };
135
+ }
136
+ }
137
+ else {
138
+ return { action: "skip" };
139
+ }
69
140
  }
70
141
  }
71
142
  const closeoutGitStop = commitPendingMilestoneCloseoutChanges(basePath, mid);
@@ -6,9 +6,10 @@ import { loadWriteGateSnapshot, shouldBlockContextArtifactSaveInSnapshot, should
6
6
  import { getActiveRequirements, getAllMilestones, getMilestone, getSliceStatusSummary, getSliceTaskCounts, insertMilestone, insertAssessment, insertGateRun, readTransaction, saveGateResult, upsertQualityGate, } from "../gsd-db.js";
7
7
  import { GATE_REGISTRY } from "../gate-registry.js";
8
8
  import { generateRequirementsMd, saveArtifactToDb } from "../db-writer.js";
9
- import { clearPathCache, relSliceFile, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
9
+ import { clearPathCache, normalizeRealPath, relSliceFile, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
10
10
  import { saveFile, clearParseCache } from "../files.js";
11
11
  import { unlinkSync } from "node:fs";
12
+ import { hostname } from "node:os";
12
13
  import { join } from "node:path";
13
14
  import { handleCompleteMilestone } from "./complete-milestone.js";
14
15
  import { handleCompleteTask } from "./complete-task.js";
@@ -25,9 +26,11 @@ import { logError, logWarning } from "../workflow-logger.js";
25
26
  import { invalidateStateCache } from "../state.js";
26
27
  import { loadEffectiveGSDPreferences } from "../preferences.js";
27
28
  import { parseProject } from "../schemas/parsers.js";
28
- import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
29
+ import { autoSession, getAutoRuntimeSnapshot, isAutoActive } from "../auto-runtime-state.js";
29
30
  import { renderPlanFromDb } from "../markdown-renderer.js";
30
31
  import { prepareUatRun, saveUatAttemptArtifact, } from "../uat-run.js";
32
+ import { registerAutoWorker, markWorkerStopping, getAutoWorker } from "../db/auto-workers.js";
33
+ import { claimMilestoneLease, releaseMilestoneLease, getMilestoneLease, refreshMilestoneLease, milestoneLeaseTtlSeconds, } from "../db/milestone-leases.js";
31
34
  export const SUPPORTED_SUMMARY_ARTIFACT_TYPES = [
32
35
  "SUMMARY",
33
36
  "RESEARCH",
@@ -61,6 +64,19 @@ function blockIfWrongAutoUnit(requiredUnitType, operation) {
61
64
  isError: true,
62
65
  };
63
66
  }
67
+ function milestoneLeaseConflictResult(milestoneId, byWorker, expiresAt) {
68
+ return {
69
+ content: [{ type: "text", text: `Milestone ${milestoneId} is currently leased by ${byWorker}. Retry after ${expiresAt}.` }],
70
+ details: {
71
+ operation: "plan_milestone",
72
+ error: "milestone_lease_conflict",
73
+ milestoneId,
74
+ byWorker,
75
+ expiresAt,
76
+ },
77
+ isError: true,
78
+ };
79
+ }
64
80
  function registerProjectMilestoneSequence(content) {
65
81
  const parsed = parseProject(content);
66
82
  const registered = [];
@@ -1066,7 +1082,45 @@ export async function executePlanMilestone(params, basePath = process.cwd()) {
1066
1082
  isError: true,
1067
1083
  };
1068
1084
  }
1085
+ let workerId = null;
1086
+ let acquiredToken = null;
1087
+ let leaseRefreshTimer;
1069
1088
  try {
1089
+ // Re-read at the gate so a peer-created milestone is not treated as fresh.
1090
+ const milestoneExists = getMilestone(params.milestoneId) !== null;
1091
+ if (milestoneExists) {
1092
+ const heldLease = getMilestoneLease(params.milestoneId);
1093
+ if (heldLease?.status === "held" && Date.parse(heldLease.expires_at) > Date.now()) {
1094
+ const holder = getAutoWorker(heldLease.worker_id);
1095
+ // Let the one-shot claim path recover stale same-process worker rows.
1096
+ const projectRoot = normalizeRealPath(basePath);
1097
+ const isOurAutoLease = isAutoActive() && heldLease.worker_id === autoSession.workerId;
1098
+ const holderIsOneShotReentrantPeer = !isAutoActive()
1099
+ && !!holder
1100
+ && holder.host === hostname()
1101
+ && holder.pid === process.pid
1102
+ && holder.project_root_realpath === projectRoot;
1103
+ if (holder?.status === "active" && !isOurAutoLease && !holderIsOneShotReentrantPeer) {
1104
+ return milestoneLeaseConflictResult(params.milestoneId, heldLease.worker_id, heldLease.expires_at);
1105
+ }
1106
+ }
1107
+ }
1108
+ // Fresh creation cannot claim a lease because the FK row does not exist.
1109
+ // In-process auto already owns its lease; re-claiming would bump its token.
1110
+ if (!isAutoActive() && milestoneExists) {
1111
+ workerId = registerAutoWorker({ projectRootRealpath: normalizeRealPath(basePath) });
1112
+ const lease = claimMilestoneLease(workerId, params.milestoneId);
1113
+ if (!lease.ok) {
1114
+ return milestoneLeaseConflictResult(params.milestoneId, lease.byWorker, lease.expiresAt);
1115
+ }
1116
+ acquiredToken = lease.token;
1117
+ const leaseRefreshMs = (milestoneLeaseTtlSeconds() / 2) * 1000;
1118
+ leaseRefreshTimer = setInterval(() => {
1119
+ if (acquiredToken !== null && workerId !== null) {
1120
+ refreshMilestoneLease(workerId, params.milestoneId, acquiredToken);
1121
+ }
1122
+ }, leaseRefreshMs);
1123
+ }
1070
1124
  const result = await handlePlanMilestone(params, basePath);
1071
1125
  if ("error" in result) {
1072
1126
  return {
@@ -1093,6 +1147,17 @@ export async function executePlanMilestone(params, basePath = process.cwd()) {
1093
1147
  isError: true,
1094
1148
  };
1095
1149
  }
1150
+ finally {
1151
+ if (leaseRefreshTimer !== undefined) {
1152
+ clearInterval(leaseRefreshTimer);
1153
+ }
1154
+ if (workerId !== null && acquiredToken !== null) {
1155
+ releaseMilestoneLease(workerId, params.milestoneId, acquiredToken);
1156
+ }
1157
+ if (workerId !== null) {
1158
+ markWorkerStopping(workerId);
1159
+ }
1160
+ }
1096
1161
  }
1097
1162
  export async function executePlanSlice(params, basePath = process.cwd()) {
1098
1163
  const dbAvailable = await ensureDbOpen(basePath);
@@ -38,6 +38,22 @@ function compareSemverLocal(a, b) {
38
38
  function parseGsdBrowserVersion(output) {
39
39
  return output.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] ?? null;
40
40
  }
41
+ function splitCommandLine(commandLine) {
42
+ const parts = commandLine.match(/(?:"[^"]*"|'[^']*'|[^\s"']+)/g) ?? [];
43
+ return parts.map((part) => {
44
+ const quote = part[0];
45
+ if ((quote === '"' || quote === "'") && part.endsWith(quote)) {
46
+ return part.slice(1, -1);
47
+ }
48
+ return part;
49
+ });
50
+ }
51
+ function buildPathGsdBrowserVersionInvocation(platform) {
52
+ if (platform === "win32") {
53
+ return { command: "cmd", args: ["/d", "/s", "/c", "gsd-browser", "--version"] };
54
+ }
55
+ return { command: "gsd-browser", args: ["--version"] };
56
+ }
41
57
  function isRecord(value) {
42
58
  return !!value && typeof value === "object" && !Array.isArray(value);
43
59
  }
@@ -67,7 +83,8 @@ function resolvePathGsdBrowserVersion(env) {
67
83
  if (cachedPathProbeVersion !== undefined)
68
84
  return cachedPathProbeVersion;
69
85
  try {
70
- cachedPathProbeVersion = parseGsdBrowserVersion(execFileSync("gsd-browser", ["--version"], {
86
+ const invocation = buildPathGsdBrowserVersionInvocation(process.platform);
87
+ cachedPathProbeVersion = parseGsdBrowserVersion(execFileSync(invocation.command, invocation.args, {
71
88
  encoding: "utf-8",
72
89
  env,
73
90
  stdio: ["ignore", "pipe", "ignore"],
@@ -176,7 +193,8 @@ export function resolveGsdBrowserMcpLaunchConfig(projectRoot, env = process.env,
176
193
  const serverName = env.GSD_BROWSER_MCP_NAME?.trim() || GSD_BROWSER_MCP_SERVER_NAME;
177
194
  const explicitArgs = parseJsonEnv(env, "GSD_BROWSER_MCP_ARGS");
178
195
  const explicitEnv = parseJsonEnv(env, "GSD_BROWSER_MCP_ENV");
179
- const explicitCommand = env.GSD_BROWSER_MCP_COMMAND?.trim();
196
+ const explicitCommandLine = env.GSD_BROWSER_MCP_COMMAND?.trim();
197
+ const [explicitCommand, ...explicitCommandArgs] = explicitCommandLine ? splitCommandLine(explicitCommandLine) : [];
180
198
  const explicitCliPath = resolveExplicitGsdBrowserCliPath(env);
181
199
  const preferPathCli = !explicitCommand && !explicitCliPath && shouldPreferPathGsdBrowser(env);
182
200
  const bundledCliPath = !explicitCommand && !explicitCliPath && !preferPathCli
@@ -198,6 +216,7 @@ export function resolveGsdBrowserMcpLaunchConfig(projectRoot, env = process.env,
198
216
  const args = Array.isArray(explicitArgs) && explicitArgs.length > 0
199
217
  ? explicitArgs.map(String)
200
218
  : [
219
+ ...explicitCommandArgs,
201
220
  ...(bundledCliPath ? [bundledCliPath] : []),
202
221
  "mcp",
203
222
  "--session",
@@ -0,0 +1,214 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { chmodSync, copyFileSync, existsSync, lstatSync, readlinkSync, realpathSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, isAbsolute, join, delimiter as pathDelimiter, resolve as resolvePath } from 'node:path';
5
+ import { isPnpmInstall, pathStartsWith } from './package-manager-detection.js';
6
+ function isBunGlobalInstall(argv1, env) {
7
+ if ('bun' in process.versions)
8
+ return true;
9
+ if (!argv1)
10
+ return false;
11
+ const bunBinDirs = [];
12
+ if (env.BUN_INSTALL)
13
+ bunBinDirs.push(join(env.BUN_INSTALL, 'bin'));
14
+ bunBinDirs.push(join(homedir(), '.bun', 'bin'));
15
+ return bunBinDirs.some((dir) => pathStartsWith(argv1, dir));
16
+ }
17
+ function gsdBrowserBinaryName(platform) {
18
+ return platform === 'win32' ? 'gsd-browser.cmd' : 'gsd-browser';
19
+ }
20
+ function tryResolveFromBinDir(binDir, platform) {
21
+ const primary = join(binDir, gsdBrowserBinaryName(platform));
22
+ if (existsSync(primary))
23
+ return primary;
24
+ if (platform === 'win32') {
25
+ const fallback = join(binDir, 'gsd-browser');
26
+ if (existsSync(fallback))
27
+ return fallback;
28
+ }
29
+ return null;
30
+ }
31
+ function tryResolveFromPackageRoot(rootDir, platform) {
32
+ const candidate = join(rootDir, '@opengsd', 'gsd-browser', 'bin', gsdBrowserBinaryName(platform));
33
+ if (existsSync(candidate))
34
+ return candidate;
35
+ if (platform === 'win32') {
36
+ const fallback = join(rootDir, '@opengsd', 'gsd-browser', 'bin', 'gsd-browser');
37
+ if (existsSync(fallback))
38
+ return fallback;
39
+ }
40
+ return null;
41
+ }
42
+ function tryExecLookup(command, args, env, platform, resolve) {
43
+ try {
44
+ const dir = execFileSync(command, args, {
45
+ encoding: 'utf-8',
46
+ env,
47
+ stdio: ['ignore', 'pipe', 'ignore'],
48
+ timeout: 5000,
49
+ }).trim();
50
+ return resolve(dir, platform);
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ function resolvePathBinary(env, platform) {
57
+ if (platform === 'win32') {
58
+ try {
59
+ const out = execFileSync('where', ['gsd-browser'], {
60
+ encoding: 'utf-8',
61
+ env,
62
+ stdio: ['ignore', 'pipe', 'ignore'],
63
+ timeout: 5000,
64
+ }).trim();
65
+ const first = out.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
66
+ return first && existsSync(first) ? first : null;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ for (const entry of (env.PATH ?? '').split(pathDelimiter)) {
73
+ if (!entry)
74
+ continue;
75
+ const candidate = join(entry, 'gsd-browser');
76
+ if (existsSync(candidate))
77
+ return candidate;
78
+ }
79
+ return null;
80
+ }
81
+ function resolveRealPath(pathValue) {
82
+ try {
83
+ return realpathSync(pathValue);
84
+ }
85
+ catch {
86
+ return resolvePath(pathValue);
87
+ }
88
+ }
89
+ function resolveSymlinkTarget(pathCli) {
90
+ try {
91
+ const stat = lstatSync(pathCli);
92
+ if (!stat.isSymbolicLink())
93
+ return pathCli;
94
+ const target = readlinkSync(pathCli);
95
+ return isAbsolute(target) ? target : resolvePath(dirname(pathCli), target);
96
+ }
97
+ catch {
98
+ // PATH entry vanished or is inaccessible between resolution and sync.
99
+ // Fall back to the original path; subsequent sync will surface a useful
100
+ // error rather than escaping as an unhandled throw.
101
+ return pathCli;
102
+ }
103
+ }
104
+ function resolveHomeDir(env) {
105
+ const fromEnv = env.HOME?.trim() || env.USERPROFILE?.trim();
106
+ return resolvePath(fromEnv || homedir());
107
+ }
108
+ function canAutoSyncTarget(targetPath, env) {
109
+ const home = resolveHomeDir(env);
110
+ const resolved = resolvePath(targetPath);
111
+ return pathStartsWith(resolved, home);
112
+ }
113
+ function syncBinary(installedCli, targetPath, platform) {
114
+ const source = resolveRealPath(installedCli);
115
+ copyFileSync(source, targetPath);
116
+ if (platform !== 'win32') {
117
+ chmodSync(targetPath, 0o755);
118
+ }
119
+ }
120
+ /**
121
+ * Resolve the gsd-browser binary installed by the active global package manager.
122
+ */
123
+ export function resolveGlobalGsdBrowserCliPath(options = {}) {
124
+ const env = options.env ?? process.env;
125
+ const argv1 = options.argv1 ?? process.argv[1];
126
+ const platform = options.platform ?? process.platform;
127
+ if (isBunGlobalInstall(argv1, env)) {
128
+ return (tryExecLookup('bun', ['pm', 'bin', '-g'], env, platform, tryResolveFromBinDir)
129
+ ?? (env.BUN_INSTALL ? tryResolveFromBinDir(join(env.BUN_INSTALL, 'bin'), platform) : null)
130
+ ?? tryResolveFromBinDir(join(homedir(), '.bun', 'bin'), platform));
131
+ }
132
+ if (isPnpmInstall(argv1, env)) {
133
+ return (tryExecLookup('pnpm', ['bin', '-g'], env, platform, tryResolveFromBinDir)
134
+ ?? tryExecLookup('pnpm', ['root', '-g'], env, platform, tryResolveFromPackageRoot));
135
+ }
136
+ return (tryExecLookup('npm', ['bin', '-g'], env, platform, tryResolveFromBinDir)
137
+ ?? tryExecLookup('npm', ['root', '-g'], env, platform, tryResolveFromPackageRoot));
138
+ }
139
+ /**
140
+ * Resolve the gsd-browser binary that wins on PATH (`command -v` / `where`).
141
+ */
142
+ export function resolveGsdBrowserOnPath(env = process.env, platform = process.platform) {
143
+ return resolvePathBinary(env, platform);
144
+ }
145
+ /**
146
+ * After a global gsd-browser install, ensure the PATH-resolved binary matches
147
+ * the freshly installed global binary when an older copy is shadowing it.
148
+ */
149
+ export function reconcileGsdBrowserPathAfterInstall(options) {
150
+ const env = options.env ?? process.env;
151
+ const argv1 = options.argv1 ?? process.argv[1];
152
+ const platform = options.platform ?? process.platform;
153
+ const installedCli = resolveGlobalGsdBrowserCliPath({ env, argv1, platform });
154
+ if (!installedCli) {
155
+ return { action: 'none' };
156
+ }
157
+ const pathCli = resolveGsdBrowserOnPath(env, platform);
158
+ const installedReal = resolveRealPath(installedCli);
159
+ if (pathCli && resolveRealPath(pathCli) === installedReal) {
160
+ return { action: 'none', pathCli, installedCli };
161
+ }
162
+ const pathVersion = options.resolvePathVersion(env);
163
+ if (pathVersion && options.compareSemver(pathVersion, options.latestVersion) >= 0) {
164
+ return { action: 'none', pathCli: pathCli ?? undefined, installedCli };
165
+ }
166
+ if (!pathCli) {
167
+ return {
168
+ action: 'shadowed',
169
+ installedCli,
170
+ message: 'Installed gsd-browser globally, but no gsd-browser was found on PATH. Add your package manager global bin directory to PATH.',
171
+ };
172
+ }
173
+ const syncTarget = resolveSymlinkTarget(pathCli);
174
+ if (!canAutoSyncTarget(syncTarget, env)) {
175
+ return {
176
+ action: 'shadowed',
177
+ pathCli,
178
+ installedCli,
179
+ syncTarget,
180
+ message: `PATH resolves gsd-browser to ${pathCli}, but the updated global install is at ${installedCli}. ` +
181
+ 'Move your package manager global bin directory ahead of the stale location on PATH, or update the stale binary manually.',
182
+ };
183
+ }
184
+ let syncSucceeded = false;
185
+ try {
186
+ syncBinary(installedCli, syncTarget, platform);
187
+ syncSucceeded = true;
188
+ }
189
+ catch {
190
+ // Fall through to shadowed guidance.
191
+ }
192
+ if (syncSucceeded) {
193
+ const refreshedVersion = options.resolvePathVersion(env);
194
+ const verified = refreshedVersion !== null
195
+ && options.compareSemver(refreshedVersion, options.latestVersion) >= 0;
196
+ return {
197
+ action: 'synced',
198
+ pathCli,
199
+ installedCli,
200
+ syncTarget,
201
+ message: verified
202
+ ? `Synced PATH-resolved gsd-browser at ${syncTarget} to the updated global install.`
203
+ : `Synced PATH-resolved gsd-browser at ${syncTarget} to the updated global install. Could not verify the new version on PATH; restart your shell or rerun if it still reports the old version.`,
204
+ };
205
+ }
206
+ return {
207
+ action: 'shadowed',
208
+ pathCli,
209
+ installedCli,
210
+ syncTarget,
211
+ message: `PATH resolves gsd-browser to ${pathCli}, but the updated global install is at ${installedCli}. ` +
212
+ 'Move your package manager global bin directory ahead of the stale location on PATH, or update the stale binary manually.',
213
+ };
214
+ }
@@ -9,7 +9,7 @@ function hasPnpmPath(value) {
9
9
  normalized.endsWith('/pnpm.cjs') ||
10
10
  normalized.endsWith('/pnpm.js'));
11
11
  }
12
- function pathStartsWith(pathValue, dir) {
12
+ export function pathStartsWith(pathValue, dir) {
13
13
  if (!pathValue)
14
14
  return false;
15
15
  const resolvedPath = resolvePath(pathValue);