@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.
- package/dist/mcp-server.js +2 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/orchestrator.js +28 -10
- package/dist/resources/extensions/gsd/auto-model-selection.js +11 -7
- package/dist/resources/extensions/gsd/auto.js +7 -0
- package/dist/resources/extensions/gsd/blocked-models.js +28 -0
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +26 -6
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
- package/dist/resources/extensions/gsd/closeout-wizard.js +92 -0
- package/dist/resources/extensions/gsd/commands-handlers.js +46 -3
- package/dist/resources/extensions/gsd/consent-question.js +16 -0
- package/dist/resources/extensions/gsd/doctor-git-checks.js +2 -18
- package/dist/resources/extensions/gsd/gsd-command-home.js +22 -12
- package/dist/resources/extensions/gsd/gsd-db.js +2 -1
- package/dist/resources/extensions/gsd/milestone-closeout.js +73 -2
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +67 -2
- package/dist/resources/extensions/shared/gsd-browser-cli.js +21 -2
- package/dist/resources/shared/gsd-browser-path-sync.js +214 -0
- package/dist/resources/shared/package-manager-detection.js +1 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/update-check.d.ts +2 -0
- package/dist/update-check.js +24 -1
- package/dist/update-cmd.js +20 -3
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
- package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/moonshot-tool-schema.d.ts +29 -0
- package/packages/mcp-server/dist/moonshot-tool-schema.d.ts.map +1 -0
- package/packages/mcp-server/dist/moonshot-tool-schema.js +50 -0
- package/packages/mcp-server/dist/moonshot-tool-schema.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +4 -0
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +18 -18
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +99 -38
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +5 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/index.d.ts +2 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +2 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +12 -7
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.d.ts +5 -0
- package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.js +12 -3
- package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +7 -3
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts +9 -0
- package/packages/pi-ai/dist/utils/moonshot-tool-schema.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/moonshot-tool-schema.js +34 -0
- package/packages/pi-ai/dist/utils/moonshot-tool-schema.js.map +1 -0
- package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js +6 -2
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +11 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +28 -10
- package/src/resources/extensions/gsd/auto-model-selection.ts +16 -7
- package/src/resources/extensions/gsd/auto.ts +8 -0
- package/src/resources/extensions/gsd/blocked-models.ts +49 -0
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +34 -5
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
- package/src/resources/extensions/gsd/closeout-wizard.ts +102 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +46 -3
- package/src/resources/extensions/gsd/consent-question.ts +15 -0
- package/src/resources/extensions/gsd/doctor-git-checks.ts +2 -19
- package/src/resources/extensions/gsd/gsd-command-home.ts +13 -3
- package/src/resources/extensions/gsd/gsd-db.ts +4 -3
- package/src/resources/extensions/gsd/milestone-closeout.ts +97 -2
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +97 -0
- package/src/resources/extensions/gsd/tests/blocked-models.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/consent-question.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/doctor-git-checks-terminal.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/gsd-command-home.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +95 -4
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +273 -38
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +81 -2
- package/src/resources/extensions/shared/gsd-browser-cli.ts +23 -2
- package/src/resources/shared/gsd-browser-path-sync.ts +273 -0
- package/src/resources/shared/package-manager-detection.ts +1 -1
- /package/dist/web/standalone/.next/static/{jmTLg6xZmAuq_LIqKOxrH → LDHRKiRBIVZmiuMjrL1Vy}/_buildManifest.js +0 -0
- /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
|
-
:
|
|
36
|
-
? "
|
|
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
|
-
:
|
|
47
|
-
? "
|
|
48
|
-
:
|
|
49
|
-
? "
|
|
50
|
-
:
|
|
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
|
-
:
|
|
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
|
-
:
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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);
|