@opengsd/gsd-pi 1.1.1-dev.75048e7 → 1.1.1-dev.9f86580
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +18 -2
- package/dist/resources/extensions/browser-tools/engine/selection.js +1 -1
- package/dist/resources/extensions/browser-tools/extension-manifest.json +1 -1
- package/dist/resources/extensions/browser-tools/index.js +29 -2
- package/dist/resources/extensions/browser-tools/web-app-detect.js +52 -0
- package/dist/resources/extensions/gsd/auto/phases.js +45 -3
- package/dist/resources/extensions/gsd/auto/session.js +2 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +10 -2
- package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
- package/dist/resources/extensions/gsd/auto-timers.js +24 -10
- package/dist/resources/extensions/gsd/auto.js +26 -4
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +29 -21
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -0
- package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
- package/dist/resources/extensions/gsd/config-overlay.js +1 -0
- package/dist/resources/extensions/gsd/context-masker.js +129 -5
- package/dist/resources/extensions/gsd/guided-flow.js +4 -1
- package/dist/resources/extensions/gsd/planner-handoff.js +98 -0
- package/dist/resources/extensions/gsd/preferences-models.js +1 -0
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
- package/dist/resources/extensions/gsd/tool-contract.js +1 -1
- package/dist/resources/extensions/gsd/tool-presentation-plan.js +19 -2
- package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +32 -4
- package/dist/resources/extensions/gsd/unit-tool-contracts.js +38 -14
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -3
- package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
- package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
- package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
- 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 +1 -1
- 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/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/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +158 -2
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +149 -9
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
- package/packages/pi-ai/dist/providers/transform-messages.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 +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/scripts/install/handoff.js +16 -3
- package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
- package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
- package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
- package/src/resources/extensions/browser-tools/index.ts +36 -5
- package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
- package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
- package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
- package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
- package/src/resources/extensions/gsd/auto/phases.ts +48 -6
- package/src/resources/extensions/gsd/auto/session.ts +2 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +34 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
- package/src/resources/extensions/gsd/auto-timers.ts +25 -9
- package/src/resources/extensions/gsd/auto.ts +28 -4
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +40 -21
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -0
- package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
- package/src/resources/extensions/gsd/config-overlay.ts +1 -0
- package/src/resources/extensions/gsd/context-masker.ts +152 -5
- package/src/resources/extensions/gsd/guided-flow.ts +4 -1
- package/src/resources/extensions/gsd/planner-handoff.ts +149 -0
- package/src/resources/extensions/gsd/preferences-models.ts +1 -0
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +2 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
- package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +4 -0
- package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +118 -0
- package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/planner-handoff.test.ts +100 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +113 -1
- package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +131 -2
- package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
- package/src/resources/extensions/gsd/tool-contract.ts +1 -1
- package/src/resources/extensions/gsd/tool-presentation-plan.ts +21 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +46 -4
- package/src/resources/extensions/gsd/unit-tool-contracts.ts +38 -14
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -3
- package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
- package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
- package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
- /package/dist/web/standalone/.next/static/{h4TGni4xJzlZjGkxaT6uU → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{h4TGni4xJzlZjGkxaT6uU → zzYMrKpPGfRQRxSFO32Jr}/_ssgManifest.js +0 -0
|
@@ -5,13 +5,36 @@
|
|
|
5
5
|
* Reduces context bloat between compactions with zero LLM overhead.
|
|
6
6
|
* Preserves message ordering, roles, and all assistant/user messages.
|
|
7
7
|
*
|
|
8
|
-
* Operates on
|
|
8
|
+
* Operates on provider payloads after convertToLlm:
|
|
9
|
+
*
|
|
10
|
+
* pi-ai Message[] payloads:
|
|
9
11
|
* - toolResult messages: { role: "toolResult", content: TextContent[] }
|
|
10
12
|
* - bash results are already converted to: { role: "user", content: [{type:"text",text:"..."}] }
|
|
11
13
|
* and start with "Ran `" from bashExecutionToText.
|
|
14
|
+
*
|
|
15
|
+
* OpenAI/Codex Responses payloads:
|
|
16
|
+
* - conversation items live in `input`, not `messages`
|
|
17
|
+
* - tool results are { type: "function_call_output", output: string | content[] }
|
|
18
|
+
* - bash results are user items with input_text content starting with "Ran `"
|
|
12
19
|
*/
|
|
13
20
|
const MASK_PLACEHOLDER = "[result masked — within summarized history]";
|
|
14
21
|
const MASK_CONTENT_BLOCK = [{ type: "text", text: MASK_PLACEHOLDER }];
|
|
22
|
+
const RESPONSES_MASK_CONTENT_BLOCK = [{ type: "input_text", text: MASK_PLACEHOLDER }];
|
|
23
|
+
const TRUNCATION_MARKER = "\n…[truncated]";
|
|
24
|
+
function isTextLikeBlock(block) {
|
|
25
|
+
return Boolean(block && typeof block === "object" && "text" in block);
|
|
26
|
+
}
|
|
27
|
+
function firstTextFromContent(content) {
|
|
28
|
+
if (typeof content === "string")
|
|
29
|
+
return content;
|
|
30
|
+
if (!Array.isArray(content))
|
|
31
|
+
return undefined;
|
|
32
|
+
const first = content.find(isTextLikeBlock);
|
|
33
|
+
return typeof first?.text === "string" ? first.text : undefined;
|
|
34
|
+
}
|
|
35
|
+
function isBashResultText(text) {
|
|
36
|
+
return typeof text === "string" && text.startsWith("Ran `");
|
|
37
|
+
}
|
|
15
38
|
function findTurnBoundary(messages, keepRecentTurns) {
|
|
16
39
|
let turnsSeen = 0;
|
|
17
40
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -35,11 +58,9 @@ function findTurnBoundary(messages, keepRecentTurns) {
|
|
|
35
58
|
* The bashExecutionToText format always starts with "Ran `".
|
|
36
59
|
*/
|
|
37
60
|
function isBashResultUserMessage(m) {
|
|
38
|
-
if (m.role !== "user"
|
|
61
|
+
if (m.role !== "user")
|
|
39
62
|
return false;
|
|
40
|
-
|
|
41
|
-
return first && typeof first === "object" && "text" in first &&
|
|
42
|
-
typeof first.text === "string" && first.text.startsWith("Ran `");
|
|
63
|
+
return isBashResultText(firstTextFromContent(m.content));
|
|
43
64
|
}
|
|
44
65
|
function isMaskableMessage(m) {
|
|
45
66
|
// Tool result messages (role: "toolResult" in pi-ai format)
|
|
@@ -66,3 +87,106 @@ export function createObservationMask(keepRecentTurns = 8) {
|
|
|
66
87
|
});
|
|
67
88
|
};
|
|
68
89
|
}
|
|
90
|
+
function isResponsesBashResultUserItem(item) {
|
|
91
|
+
if (item.role !== "user")
|
|
92
|
+
return false;
|
|
93
|
+
return isBashResultText(firstTextFromContent(item.content));
|
|
94
|
+
}
|
|
95
|
+
function findResponsesTurnBoundary(items, keepRecentTurns) {
|
|
96
|
+
let turnsSeen = 0;
|
|
97
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
98
|
+
const item = items[i];
|
|
99
|
+
if (item.role === "user" && !isResponsesBashResultUserItem(item)) {
|
|
100
|
+
turnsSeen++;
|
|
101
|
+
if (turnsSeen >= keepRecentTurns)
|
|
102
|
+
return i;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Observation masking for OpenAI/Codex Responses API payloads.
|
|
109
|
+
*
|
|
110
|
+
* Responses payloads store the conversation under `input` instead of
|
|
111
|
+
* `messages`, with tool results as `function_call_output` items. Keep this
|
|
112
|
+
* separate from createObservationMask so each payload shape stays explicit.
|
|
113
|
+
*/
|
|
114
|
+
export function createResponsesInputObservationMask(keepRecentTurns = 8) {
|
|
115
|
+
return (items) => {
|
|
116
|
+
const boundary = findResponsesTurnBoundary(items, keepRecentTurns);
|
|
117
|
+
if (boundary === 0)
|
|
118
|
+
return items;
|
|
119
|
+
return items.map((item, i) => {
|
|
120
|
+
if (i >= boundary)
|
|
121
|
+
return item;
|
|
122
|
+
if (item.type === "function_call_output") {
|
|
123
|
+
return { ...item, output: MASK_PLACEHOLDER };
|
|
124
|
+
}
|
|
125
|
+
if (isResponsesBashResultUserItem(item)) {
|
|
126
|
+
return { ...item, content: RESPONSES_MASK_CONTENT_BLOCK };
|
|
127
|
+
}
|
|
128
|
+
return item;
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function truncateText(text, maxChars) {
|
|
133
|
+
if (text.length <= maxChars)
|
|
134
|
+
return text;
|
|
135
|
+
return text.slice(0, maxChars) + TRUNCATION_MARKER;
|
|
136
|
+
}
|
|
137
|
+
function truncateTextBlocks(content, maxChars) {
|
|
138
|
+
if (typeof content === "string") {
|
|
139
|
+
return truncateText(content, maxChars);
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(content))
|
|
142
|
+
return content;
|
|
143
|
+
let remaining = maxChars;
|
|
144
|
+
let didTruncate = false;
|
|
145
|
+
const nextBlocks = [];
|
|
146
|
+
for (const block of content) {
|
|
147
|
+
if (!isTextLikeBlock(block) || typeof block.text !== "string") {
|
|
148
|
+
nextBlocks.push(block);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (remaining <= 0) {
|
|
152
|
+
didTruncate = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const text = block.text;
|
|
156
|
+
if (text.length <= remaining) {
|
|
157
|
+
nextBlocks.push(block);
|
|
158
|
+
remaining -= text.length;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
nextBlocks.push({ ...block, text: truncateText(text, remaining) });
|
|
162
|
+
remaining = 0;
|
|
163
|
+
didTruncate = true;
|
|
164
|
+
}
|
|
165
|
+
return didTruncate ? nextBlocks : content;
|
|
166
|
+
}
|
|
167
|
+
function normalizedMaxChars(maxChars) {
|
|
168
|
+
return Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : 800;
|
|
169
|
+
}
|
|
170
|
+
export function truncateContextResultMessages(messages, maxChars = 800) {
|
|
171
|
+
const limit = normalizedMaxChars(maxChars);
|
|
172
|
+
return messages.map((message) => {
|
|
173
|
+
if (!isMaskableMessage(message))
|
|
174
|
+
return message;
|
|
175
|
+
const content = truncateTextBlocks(message.content, limit);
|
|
176
|
+
return content === message.content ? message : { ...message, content };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export function truncateResponsesInputResultItems(items, maxChars = 800) {
|
|
180
|
+
const limit = normalizedMaxChars(maxChars);
|
|
181
|
+
return items.map((item) => {
|
|
182
|
+
if (item.type === "function_call_output") {
|
|
183
|
+
const output = truncateTextBlocks(item.output, limit);
|
|
184
|
+
return output === item.output ? item : { ...item, output };
|
|
185
|
+
}
|
|
186
|
+
if (isResponsesBashResultUserItem(item)) {
|
|
187
|
+
const content = truncateTextBlocks(item.content, limit);
|
|
188
|
+
return content === item.content ? item : { ...item, content };
|
|
189
|
+
}
|
|
190
|
+
return item;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -865,7 +865,10 @@ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType,
|
|
|
865
865
|
? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider)
|
|
866
866
|
: undefined,
|
|
867
867
|
baseUrl: result.appliedModel?.baseUrl ?? ctx.model?.baseUrl,
|
|
868
|
-
|
|
868
|
+
// Guided flow starts the MCP workflow server as part of dispatch, so the
|
|
869
|
+
// parent session's activeTools doesn't include MCP tools yet. The MCP
|
|
870
|
+
// launch config check (detectWorkflowMcpLaunchConfig) is the right gate
|
|
871
|
+
// here — not whether MCP tools are pre-registered in the parent session.
|
|
869
872
|
});
|
|
870
873
|
if (compatibilityError) {
|
|
871
874
|
ctx.ui.notify(compatibilityError, "error");
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Optional gsd-planner handoff after milestone planning.
|
|
3
|
+
import { spawn as spawnChild } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { gsdRoot } from "./paths.js";
|
|
7
|
+
export const PLANNER_HANDOFF_RULE_NAME = "planning review handoff -> gsd-planner";
|
|
8
|
+
export const GSD_PLANNER_COMMAND = "gsd-planner";
|
|
9
|
+
function handoffDir(basePath) {
|
|
10
|
+
return join(gsdRoot(basePath), "runtime", "planner-handoffs");
|
|
11
|
+
}
|
|
12
|
+
function safeMilestoneFileSegment(milestoneId) {
|
|
13
|
+
return milestoneId.replace(/[^A-Za-z0-9._-]/g, "_") || "unknown";
|
|
14
|
+
}
|
|
15
|
+
function handoffMarkerPath(basePath, milestoneId) {
|
|
16
|
+
return join(handoffDir(basePath), `${safeMilestoneFileSegment(milestoneId)}.json`);
|
|
17
|
+
}
|
|
18
|
+
export function hasPlannerHandoffBeenOffered(basePath, milestoneId) {
|
|
19
|
+
return existsSync(handoffMarkerPath(basePath, milestoneId));
|
|
20
|
+
}
|
|
21
|
+
export function markPlannerHandoffOffered(basePath, milestoneId, source = "auto") {
|
|
22
|
+
mkdirSync(handoffDir(basePath), { recursive: true });
|
|
23
|
+
writeFileSync(handoffMarkerPath(basePath, milestoneId), JSON.stringify({
|
|
24
|
+
milestoneId,
|
|
25
|
+
source,
|
|
26
|
+
offeredAt: new Date().toISOString(),
|
|
27
|
+
}, null, 2) + "\n", "utf-8");
|
|
28
|
+
}
|
|
29
|
+
export function buildGsdPlannerSpawnPlan(input) {
|
|
30
|
+
const args = ["--project", input.basePath];
|
|
31
|
+
const milestoneId = input.milestoneId?.trim();
|
|
32
|
+
if (milestoneId)
|
|
33
|
+
args.push("--milestone", milestoneId);
|
|
34
|
+
args.push(...(input.extraArgs ?? []));
|
|
35
|
+
return {
|
|
36
|
+
command: GSD_PLANNER_COMMAND,
|
|
37
|
+
args,
|
|
38
|
+
cwd: input.basePath,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function quoteArg(arg) {
|
|
42
|
+
return /^[A-Za-z0-9_./:=@+-]+$/.test(arg) ? arg : JSON.stringify(arg);
|
|
43
|
+
}
|
|
44
|
+
export function formatGsdPlannerCommand(plan) {
|
|
45
|
+
return [plan.command, ...plan.args].map(quoteArg).join(" ");
|
|
46
|
+
}
|
|
47
|
+
export async function launchGsdPlanner(input, deps = {}) {
|
|
48
|
+
const plan = buildGsdPlannerSpawnPlan(input);
|
|
49
|
+
const spawn = deps.spawn ?? spawnChild;
|
|
50
|
+
let child;
|
|
51
|
+
try {
|
|
52
|
+
child = spawn(plan.command, plan.args, {
|
|
53
|
+
cwd: plan.cwd,
|
|
54
|
+
detached: true,
|
|
55
|
+
stdio: "ignore",
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return {
|
|
61
|
+
status: "failed",
|
|
62
|
+
plan,
|
|
63
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
let settled = false;
|
|
68
|
+
const settle = (result) => {
|
|
69
|
+
if (settled)
|
|
70
|
+
return;
|
|
71
|
+
settled = true;
|
|
72
|
+
resolve(result);
|
|
73
|
+
};
|
|
74
|
+
child.once("error", (err) => {
|
|
75
|
+
settle({
|
|
76
|
+
status: "failed",
|
|
77
|
+
plan,
|
|
78
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
child.once("spawn", () => {
|
|
82
|
+
child.unref();
|
|
83
|
+
settle({ status: "launched", plan });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export function formatPlannerHandoffPauseReason(milestoneId) {
|
|
88
|
+
return [
|
|
89
|
+
`Milestone ${milestoneId} is planned. Review or customize the plan before implementation if needed.`,
|
|
90
|
+
`Run /gsd planner to launch ${GSD_PLANNER_COMMAND}, or run /gsd auto to continue without planner changes.`,
|
|
91
|
+
].join(" ");
|
|
92
|
+
}
|
|
93
|
+
export function formatPlannerLaunchUnavailable(plan, error) {
|
|
94
|
+
return [
|
|
95
|
+
`Could not launch ${GSD_PLANNER_COMMAND}: ${error.message}`,
|
|
96
|
+
`Install ${GSD_PLANNER_COMMAND} or run it manually: ${formatGsdPlannerCommand(plan)}`,
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
@@ -339,6 +339,7 @@ export function resolveAutoSupervisorConfig() {
|
|
|
339
339
|
soft_timeout_minutes: configured.soft_timeout_minutes ?? 20,
|
|
340
340
|
idle_timeout_minutes: configured.idle_timeout_minutes ?? 10,
|
|
341
341
|
hard_timeout_minutes: configured.hard_timeout_minutes ?? 30,
|
|
342
|
+
stalled_tool_timeout_minutes: configured.stalled_tool_timeout_minutes ?? 5,
|
|
342
343
|
...(configured.model ? { model: configured.model } : {}),
|
|
343
344
|
};
|
|
344
345
|
}
|
|
@@ -14,7 +14,7 @@ All relevant context is preloaded below. Start immediately without re-reading th
|
|
|
14
14
|
|
|
15
15
|
## Already Planned? Soft Brake
|
|
16
16
|
|
|
17
|
-
If `{{outputPath}}` exists with at least one slice line (e.g. `- [ ] **S01:`) AND `
|
|
17
|
+
If `{{outputPath}}` exists with at least one slice line (e.g. `- [ ] **S01:`) AND `gsd_milestone_status` reports slice rows for this milestone, a prior `gsd_plan_milestone` call already persisted the plan. Do **not** re-call it; its UPSERT could overwrite existing planning. Skip to the ready phrase.
|
|
18
18
|
|
|
19
19
|
If only the file or only DB rows exist, the prior write was incomplete; plan normally so the tool reconciles both.
|
|
20
20
|
|
|
@@ -27,7 +27,7 @@ You are the UAT runner. Execute every check defined in `{{uatPath}}` as deeply a
|
|
|
27
27
|
### Automation rules by mode
|
|
28
28
|
|
|
29
29
|
- `artifact-driven` — verify with shell commands, scripts, file reads, and artifact structure checks.
|
|
30
|
-
- `browser-executable` — use
|
|
30
|
+
- `browser-executable` — use browser tools to navigate to the target URL and verify expected behavior. Prefer direct `browser_*` tools when available. Capture screenshots as evidence. Record pass/fail with specific assertions.
|
|
31
31
|
- `runtime-executable` — execute the specified command or script. Capture stdout/stderr as evidence. Record pass/fail based on exit code and output.
|
|
32
32
|
- `live-runtime` — exercise the real runtime path. Start or connect to the app/service if needed, use browser/runtime/network checks, and verify observable behavior.
|
|
33
33
|
- `mixed` — run all automatable artifact-driven and live-runtime checks. Separate any remaining human-only checks explicitly.
|
|
@@ -48,7 +48,7 @@ Choose the lightest tool that proves the check honestly:
|
|
|
48
48
|
- Run `node` / other script invocations
|
|
49
49
|
- Read files and verify their contents
|
|
50
50
|
- Check that expected artifacts exist and have correct structure
|
|
51
|
-
- For live/runtime/UI checks, exercise the real flow with
|
|
51
|
+
- For live/runtime/UI checks, exercise the real flow with browser tools when applicable and inspect runtime/network/console state
|
|
52
52
|
- When a check cannot be honestly automated, gather the best objective evidence you can and mark it `NEEDS-HUMAN`
|
|
53
53
|
|
|
54
54
|
For each check, record:
|
|
@@ -118,7 +118,7 @@ Templates are in `{{templatesDir}}`.
|
|
|
118
118
|
|
|
119
119
|
**Secrets:** Use `secure_env_collect`. Never ask the user to edit `.env` files or paste secrets.
|
|
120
120
|
|
|
121
|
-
**Browser verification:** Verify frontend work against a running app with
|
|
121
|
+
**Browser verification:** Verify frontend work against a running app with browser tools by default. Use `browser_find`/`browser_snapshot_refs` for discovery, refs/selectors -> `browser_batch` for actions, `browser_assert` for verification, and `browser_diff` -> console/network logs -> full inspection as last resort. If browser tools are MCP-namespaced, use that host-provided browser surface. Retry only with a new hypothesis.
|
|
122
122
|
|
|
123
123
|
**Database:** Never query `.gsd/gsd.db` directly via `sqlite3`, `better-sqlite3`, or `node -e require('better-sqlite3')`; the engine owns a single-writer WAL connection. Use `gsd_milestone_status`, `gsd_journal_query`, or other `gsd_*` tools.
|
|
124
124
|
|
|
@@ -114,6 +114,18 @@ const UNIT_TYPE_SKILL_MANIFEST = {
|
|
|
114
114
|
"review",
|
|
115
115
|
"accessibility",
|
|
116
116
|
],
|
|
117
|
+
// Slice closeout — the "closer" role: verify assembled task work, write the
|
|
118
|
+
// downstream-ready summary + UAT, optionally drive reviewer/security/tester
|
|
119
|
+
// subagents. Predictable skill set, mirrors `complete-milestone`.
|
|
120
|
+
"complete-slice": [
|
|
121
|
+
"verify-before-complete",
|
|
122
|
+
"test",
|
|
123
|
+
"review",
|
|
124
|
+
"security-review",
|
|
125
|
+
"write-docs",
|
|
126
|
+
"observability",
|
|
127
|
+
"handoff",
|
|
128
|
+
],
|
|
117
129
|
// `execute-task` intentionally omitted — implementation hot path covers a
|
|
118
130
|
// wide surface of technologies; wildcard fallback preserves today's
|
|
119
131
|
// behavior until per-task skill hints can be derived from task-plan
|
|
@@ -16,7 +16,7 @@ export function compileUnitToolContract(unitType) {
|
|
|
16
16
|
const requiredWorkflowTools = getRequiredWorkflowToolsForAutoUnit(unitType);
|
|
17
17
|
const forbiddenWorkflowTools = Object.entries(surfaceContract?.forbiddenGsdTools ?? {})
|
|
18
18
|
.map(([name, reason]) => ({ name, reason }));
|
|
19
|
-
const closeoutTools = requiredWorkflowTools.filter((tool) => /^gsd_(?:task|slice|milestone|complete|validate|save|summary)/.test(tool));
|
|
19
|
+
const closeoutTools = requiredWorkflowTools.filter((tool) => /^gsd_(?:task|slice|milestone|complete|validate|save|summary|uat)/.test(tool));
|
|
20
20
|
if (requiresCloseoutTool(unitType) && closeoutTools.length === 0) {
|
|
21
21
|
return {
|
|
22
22
|
ok: false,
|
|
@@ -73,11 +73,28 @@ export function buildRunUatCanonicalToolNames(options = {}) {
|
|
|
73
73
|
...(options.includeBrowserTools ?? []),
|
|
74
74
|
]);
|
|
75
75
|
}
|
|
76
|
+
// UAT modes whose run-uat instructions direct the runner to exercise the live
|
|
77
|
+
// app in a browser. These modes receive the browser tool surface so the runner
|
|
78
|
+
// can actually drive the page instead of silently deferring browser checks to a
|
|
79
|
+
// human. See run-uat.md automation rules: `browser-executable`, `live-runtime`,
|
|
80
|
+
// and `mixed` are all told to drive a browser/runtime path, and
|
|
81
|
+
// `human-experience` is told to capture screenshots. Without this, a webpage
|
|
82
|
+
// UAT classified as anything but `browser-executable` had no browser tools and
|
|
83
|
+
// downgraded its live checks to NEEDS-HUMAN (M001/S03 regression).
|
|
84
|
+
export const BROWSER_INCLUSIVE_UAT_TYPES = [
|
|
85
|
+
"browser-executable",
|
|
86
|
+
"live-runtime",
|
|
87
|
+
"mixed",
|
|
88
|
+
"human-experience",
|
|
89
|
+
];
|
|
90
|
+
function uatTypeIncludesBrowser(uatType) {
|
|
91
|
+
return uatType !== undefined && BROWSER_INCLUSIVE_UAT_TYPES.includes(uatType);
|
|
92
|
+
}
|
|
76
93
|
export function runUatBrowserToolsForType(uatType) {
|
|
77
|
-
return uatType
|
|
94
|
+
return uatTypeIncludesBrowser(uatType) ? RUN_UAT_BROWSER_TOOL_NAMES : [];
|
|
78
95
|
}
|
|
79
96
|
export function runUatPresentationSurfaceForType(uatType) {
|
|
80
|
-
return uatType
|
|
97
|
+
return uatTypeIncludesBrowser(uatType) ? "hybrid" : "mcp";
|
|
81
98
|
}
|
|
82
99
|
export function buildRunUatPresentationForType(uatType, options = {}) {
|
|
83
100
|
return buildRunUatResultPresentation({
|
|
@@ -17,7 +17,8 @@ import { getGatesForTurn } from "../gate-registry.js";
|
|
|
17
17
|
import { gsdProjectionRoot, clearPathCache, resolveMilestoneFile } from "../paths.js";
|
|
18
18
|
import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
|
|
19
19
|
import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
|
|
20
|
-
import { saveFile, clearParseCache } from "../files.js";
|
|
20
|
+
import { saveFile, clearParseCache, extractUatType } from "../files.js";
|
|
21
|
+
import { hasBrowserRequiredText } from "../browser-evidence.js";
|
|
21
22
|
import { invalidateStateCache } from "../state.js";
|
|
22
23
|
import { renderRoadmapFromDb } from "../markdown-renderer.js";
|
|
23
24
|
import { parseRoadmap } from "../parsers-legacy.js";
|
|
@@ -267,6 +268,32 @@ export async function handleCompleteSlice(params, basePath) {
|
|
|
267
268
|
if (BLOCKED_SIGNALS.test(params.verification || "") || BLOCKED_SIGNALS.test(params.uatContent || "")) {
|
|
268
269
|
return { error: `slice verification indicates blocked/failed state — do not complete a slice that has not passed verification. Address the blockers and re-verify first.` };
|
|
269
270
|
}
|
|
271
|
+
// ── Browser/web UAT classification gate ────────────────────────────────
|
|
272
|
+
// A UAT that drives a running web UI (opening a page in a browser,
|
|
273
|
+
// navigating to a page/localhost) must declare a browser-capable mode so the
|
|
274
|
+
// run-uat runner surfaces browser tools and actually launches a browser.
|
|
275
|
+
// Otherwise the browser checks get silently deferred to a human and the slice
|
|
276
|
+
// passes on static checks alone (M001/S03 regression). `browser-executable`,
|
|
277
|
+
// `live-runtime`, and `mixed` all receive browser tools (see
|
|
278
|
+
// BROWSER_INCLUSIVE_UAT_TYPES); only the non-browser modes are rejected here.
|
|
279
|
+
//
|
|
280
|
+
// Reuse the canonical hasBrowserRequiredText detector (also used by dispatch
|
|
281
|
+
// and milestone validation): it skips Not-Proven/Out-of-Scope disclaimer
|
|
282
|
+
// sections and only treats verbs like navigate/open as web when they sit next
|
|
283
|
+
// to browser/page/localhost — avoiding false positives on CLI/file/API steps.
|
|
284
|
+
//
|
|
285
|
+
// Only `artifact-driven` is gated. It is the one mode that performs no
|
|
286
|
+
// execution at all (static/file checks), so a browser-requiring UAT under it
|
|
287
|
+
// genuinely defers verification to a human. Every other mode has a real
|
|
288
|
+
// verification path: `runtime-executable` runs browser test commands like
|
|
289
|
+
// `npx playwright test` via gsd_uat_exec, and live-runtime/mixed/
|
|
290
|
+
// browser-executable receive browser tools (BROWSER_INCLUSIVE_UAT_TYPES).
|
|
291
|
+
const declaredUatMode = extractUatType(params.uatContent || "") ?? "artifact-driven";
|
|
292
|
+
if (declaredUatMode === "artifact-driven" && hasBrowserRequiredText(params.uatContent || "")) {
|
|
293
|
+
return {
|
|
294
|
+
error: `UAT requires browser verification (opening a page in a browser, navigating to a page or localhost, screenshots) but declares "UAT mode: artifact-driven", which only runs static/file checks and would defer the browser work to a human. Use a mode that actually verifies the UI: "browser-executable" (interactive browser tools), "runtime-executable" (a browser test command such as playwright), or a browser-inclusive "mixed"/"live-runtime". Re-author the UAT Type section and complete the slice again.`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
270
297
|
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
271
298
|
const completedAt = new Date().toISOString();
|
|
272
299
|
let guardError = null;
|
|
@@ -6,8 +6,9 @@ import { loadWriteGateSnapshot, shouldBlockContextArtifactSaveInSnapshot, should
|
|
|
6
6
|
import { getActiveRequirements, insertMilestone, getMilestone, getSliceStatusSummary, getSliceTaskCounts, 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, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
|
|
9
|
+
import { clearPathCache, relSliceFile, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "../paths.js";
|
|
10
10
|
import { saveFile, clearParseCache } from "../files.js";
|
|
11
|
+
import { buildManualValidationGuidance, resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
|
|
11
12
|
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
12
13
|
import { isAbsolute, join, resolve } from "node:path";
|
|
13
14
|
import { handleCompleteMilestone } from "./complete-milestone.js";
|
|
@@ -1106,7 +1107,7 @@ function escapeMarkdownTableCell(value) {
|
|
|
1106
1107
|
.replace(/[\\|]/g, (char) => `\\${char}`)
|
|
1107
1108
|
.replace(/\r?\n/g, "<br>");
|
|
1108
1109
|
}
|
|
1109
|
-
function renderUatAssessment(params, attempt, gateVerdict) {
|
|
1110
|
+
function renderUatAssessment(params, attempt, gateVerdict, basePath) {
|
|
1110
1111
|
const lines = [
|
|
1111
1112
|
"---",
|
|
1112
1113
|
`sliceId: ${params.sliceId}`,
|
|
@@ -1141,6 +1142,18 @@ function renderUatAssessment(params, attempt, gateVerdict) {
|
|
|
1141
1142
|
"",
|
|
1142
1143
|
`Aggregate UAT gate saved as ${gateVerdict}.`,
|
|
1143
1144
|
];
|
|
1145
|
+
// When any check still needs a human, point them at the exact checkout to
|
|
1146
|
+
// validate — critical for worktree milestones whose code sits under a hidden
|
|
1147
|
+
// `.gsd/worktrees/` path the reviewer would otherwise have to hunt for.
|
|
1148
|
+
const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
|
|
1149
|
+
if (hasHuman) {
|
|
1150
|
+
const guidance = buildManualValidationGuidance(basePath, params.milestoneId, {
|
|
1151
|
+
uatPath: relSliceFile(basePath, params.milestoneId, params.sliceId, "UAT"),
|
|
1152
|
+
});
|
|
1153
|
+
if (guidance) {
|
|
1154
|
+
lines.push("", "## Manual Validation", "", "One or more checks are marked `NEEDS-HUMAN` and require a person to validate:", "", ...guidance.split("\n").map((line) => `- ${line}`));
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1144
1157
|
return `${lines.join("\n")}\n`;
|
|
1145
1158
|
}
|
|
1146
1159
|
async function saveUatAttemptArtifact(basePath, params, attempt) {
|
|
@@ -1190,7 +1203,7 @@ export async function executeUatResultSave(params, basePath = process.cwd()) {
|
|
|
1190
1203
|
}
|
|
1191
1204
|
const gateVerdict = params.verdict === "PASS" ? "pass" : "flag";
|
|
1192
1205
|
const rationale = params.notes ?? `UAT ${params.verdict} for ${params.sliceId}.`;
|
|
1193
|
-
const assessment = renderUatAssessment(params, attempt, gateVerdict);
|
|
1206
|
+
const assessment = renderUatAssessment(params, attempt, gateVerdict, basePath);
|
|
1194
1207
|
const summary = await executeSummarySave({
|
|
1195
1208
|
milestone_id: params.milestoneId,
|
|
1196
1209
|
slice_id: params.sliceId,
|
|
@@ -1232,8 +1245,20 @@ export async function executeUatResultSave(params, basePath = process.cwd()) {
|
|
|
1232
1245
|
evaluatedAt,
|
|
1233
1246
|
});
|
|
1234
1247
|
invalidateStateCache();
|
|
1248
|
+
// Surface where to validate when checks are left for a human, so the path
|
|
1249
|
+
// (often a buried worktree checkout) reaches the reviewer, not just the file.
|
|
1250
|
+
const hasHuman = params.checks.some((check) => check.result === "NEEDS-HUMAN");
|
|
1251
|
+
const manualGuidance = hasHuman
|
|
1252
|
+
? buildManualValidationGuidance(basePath, params.milestoneId, {
|
|
1253
|
+
uatPath: relSliceFile(basePath, params.milestoneId, params.sliceId, "UAT"),
|
|
1254
|
+
})
|
|
1255
|
+
: null;
|
|
1256
|
+
const savedText = `UAT result saved for ${params.milestoneId}/${params.sliceId}: ${params.verdict}`;
|
|
1235
1257
|
return {
|
|
1236
|
-
content: [{
|
|
1258
|
+
content: [{
|
|
1259
|
+
type: "text",
|
|
1260
|
+
text: manualGuidance ? `${savedText}\n\nManual validation needed:\n${manualGuidance}` : savedText,
|
|
1261
|
+
}],
|
|
1237
1262
|
details: {
|
|
1238
1263
|
operation: "save_uat_result",
|
|
1239
1264
|
milestoneId: params.milestoneId,
|
|
@@ -1243,6 +1268,9 @@ export async function executeUatResultSave(params, basePath = process.cwd()) {
|
|
|
1243
1268
|
attempt,
|
|
1244
1269
|
attemptPath,
|
|
1245
1270
|
recommendedNextUnit: params.verdict === "PASS" ? null : "reactive-execute",
|
|
1271
|
+
...(hasHuman
|
|
1272
|
+
? { manualValidationPath: resolveCanonicalMilestoneRoot(basePath, params.milestoneId) }
|
|
1273
|
+
: {}),
|
|
1246
1274
|
},
|
|
1247
1275
|
};
|
|
1248
1276
|
}
|
|
@@ -41,8 +41,14 @@ export const UNIT_TOOL_CONTRACTS = {
|
|
|
41
41
|
requiredWorkflowTools: ["gsd_summary_save"],
|
|
42
42
|
},
|
|
43
43
|
"plan-milestone": {
|
|
44
|
-
allowedGsdTools: [
|
|
45
|
-
|
|
44
|
+
allowedGsdTools: [
|
|
45
|
+
"gsd_milestone_status",
|
|
46
|
+
"gsd_plan_milestone",
|
|
47
|
+
"gsd_plan_slice",
|
|
48
|
+
"gsd_decision_save",
|
|
49
|
+
"gsd_requirement_update",
|
|
50
|
+
],
|
|
51
|
+
requiredWorkflowTools: ["gsd_milestone_status", "gsd_plan_milestone", "gsd_plan_slice"],
|
|
46
52
|
},
|
|
47
53
|
"discuss-milestone": {
|
|
48
54
|
allowedGsdTools: [
|
|
@@ -66,27 +72,38 @@ export const UNIT_TOOL_CONTRACTS = {
|
|
|
66
72
|
requiredWorkflowTools: ["gsd_summary_save"],
|
|
67
73
|
},
|
|
68
74
|
"validate-milestone": {
|
|
69
|
-
allowedGsdTools: ["gsd_validate_milestone", "gsd_reassess_roadmap", "subagent"],
|
|
75
|
+
allowedGsdTools: ["gsd_milestone_status", "gsd_validate_milestone", "gsd_reassess_roadmap", "subagent"],
|
|
70
76
|
requiredWorkflowTools: ["gsd_milestone_status", "gsd_validate_milestone", "gsd_reassess_roadmap"],
|
|
71
77
|
},
|
|
72
78
|
"complete-milestone": {
|
|
73
|
-
allowedGsdTools: [
|
|
74
|
-
|
|
79
|
+
allowedGsdTools: [
|
|
80
|
+
"gsd_milestone_status",
|
|
81
|
+
"gsd_requirement_update",
|
|
82
|
+
"gsd_summary_save",
|
|
83
|
+
"gsd_complete_milestone",
|
|
84
|
+
"subagent",
|
|
85
|
+
],
|
|
86
|
+
requiredWorkflowTools: [
|
|
87
|
+
"gsd_milestone_status",
|
|
88
|
+
"gsd_requirement_update",
|
|
89
|
+
"gsd_summary_save",
|
|
90
|
+
"gsd_complete_milestone",
|
|
91
|
+
],
|
|
75
92
|
},
|
|
76
93
|
"research-slice": {
|
|
77
94
|
allowedGsdTools: ["gsd_summary_save", "gsd_decision_save"],
|
|
78
95
|
requiredWorkflowTools: ["gsd_summary_save"],
|
|
79
96
|
},
|
|
80
97
|
"plan-slice": {
|
|
81
|
-
allowedGsdTools: ["gsd_plan_slice", "
|
|
82
|
-
requiredWorkflowTools: ["gsd_plan_slice"],
|
|
98
|
+
allowedGsdTools: ["gsd_plan_slice", "gsd_reassess_roadmap", "gsd_decision_save"],
|
|
99
|
+
requiredWorkflowTools: ["gsd_plan_slice", "gsd_reassess_roadmap"],
|
|
83
100
|
},
|
|
84
101
|
"refine-slice": {
|
|
85
|
-
allowedGsdTools: ["gsd_plan_slice", "
|
|
86
|
-
requiredWorkflowTools: [],
|
|
102
|
+
allowedGsdTools: ["gsd_plan_slice", "gsd_decision_save"],
|
|
103
|
+
requiredWorkflowTools: ["gsd_plan_slice"],
|
|
87
104
|
},
|
|
88
105
|
"replan-slice": {
|
|
89
|
-
allowedGsdTools: ["gsd_replan_slice", "
|
|
106
|
+
allowedGsdTools: ["gsd_replan_slice", "gsd_decision_save"],
|
|
90
107
|
requiredWorkflowTools: ["gsd_replan_slice"],
|
|
91
108
|
},
|
|
92
109
|
"complete-slice": {
|
|
@@ -96,15 +113,22 @@ export const UNIT_TOOL_CONTRACTS = {
|
|
|
96
113
|
"gsd_replan_slice",
|
|
97
114
|
"gsd_decision_save",
|
|
98
115
|
"gsd_requirement_update",
|
|
116
|
+
"gsd_summary_save",
|
|
99
117
|
"subagent",
|
|
100
118
|
],
|
|
101
|
-
requiredWorkflowTools: [
|
|
119
|
+
requiredWorkflowTools: [
|
|
120
|
+
"gsd_slice_complete",
|
|
121
|
+
"gsd_task_reopen",
|
|
122
|
+
"gsd_replan_slice",
|
|
123
|
+
"gsd_requirement_update",
|
|
124
|
+
"gsd_summary_save",
|
|
125
|
+
],
|
|
102
126
|
forbiddenGsdTools: {
|
|
103
127
|
gsd_uat_result_save: "Run UAT owns persisted UAT Assessment.",
|
|
104
128
|
},
|
|
105
129
|
},
|
|
106
130
|
"reassess-roadmap": {
|
|
107
|
-
allowedGsdTools: ["gsd_reassess_roadmap"],
|
|
131
|
+
allowedGsdTools: ["gsd_milestone_status", "gsd_reassess_roadmap"],
|
|
108
132
|
requiredWorkflowTools: ["gsd_milestone_status", "gsd_reassess_roadmap"],
|
|
109
133
|
},
|
|
110
134
|
"execute-task": {
|
|
@@ -116,8 +140,8 @@ export const UNIT_TOOL_CONTRACTS = {
|
|
|
116
140
|
requiredWorkflowTools: ["gsd_task_complete"],
|
|
117
141
|
},
|
|
118
142
|
"reactive-execute": {
|
|
119
|
-
allowedGsdTools: ["gsd_task_complete", "gsd_decision_save"],
|
|
120
|
-
requiredWorkflowTools: ["gsd_task_complete"],
|
|
143
|
+
allowedGsdTools: ["gsd_task_complete", "gsd_summary_save", "gsd_decision_save"],
|
|
144
|
+
requiredWorkflowTools: ["gsd_task_complete", "gsd_summary_save"],
|
|
121
145
|
},
|
|
122
146
|
"run-uat": {
|
|
123
147
|
allowedGsdTools: [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent"],
|
|
@@ -409,10 +409,9 @@ export function getWorkflowTransportSupportError(provider, requiredTools, option
|
|
|
409
409
|
return `Provider ${providerLabel} cannot run ${surface}${unitLabel}: the GSD workflow MCP server is not configured or discoverable. Detected Claude Code model but no workflow MCP. Please run /gsd mcp init . from your project root. You can also configure GSD_WORKFLOW_MCP_COMMAND, build packages/mcp-server/dist/cli.js, or install gsd-mcp-server on PATH.`;
|
|
410
410
|
}
|
|
411
411
|
const uniqueRequired = [...new Set(requiredTools)];
|
|
412
|
-
const piRuntimeRequired = uniqueRequired.filter((tool) => !MCP_WORKFLOW_TOOL_SURFACE.has(tool));
|
|
413
412
|
const missing = (options.activeTools && options.activeTools.length > 0)
|
|
414
|
-
?
|
|
415
|
-
:
|
|
413
|
+
? uniqueRequired.filter((tool) => !hasRequiredTool(tool, options.activeTools))
|
|
414
|
+
: uniqueRequired.filter((tool) => !MCP_WORKFLOW_TOOL_SURFACE.has(tool));
|
|
416
415
|
if (missing.length === 0)
|
|
417
416
|
return null;
|
|
418
417
|
if (options.activeTools && options.activeTools.length > 0) {
|
|
@@ -185,6 +185,32 @@ export function resolveCanonicalMilestoneRoot(basePath, milestoneId) {
|
|
|
185
185
|
}
|
|
186
186
|
return wtPath;
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Build human-facing guidance for manually validating a milestone's work.
|
|
190
|
+
*
|
|
191
|
+
* When a milestone runs in a git worktree, its checkout lives under the hidden
|
|
192
|
+
* `.gsd/worktrees/<MID>/` path that a human can't easily discover. The UAT
|
|
193
|
+
* pause/handoff and the saved assessment use this to tell the human exactly
|
|
194
|
+
* where to `cd` to run or inspect the app before signing off on NEEDS-HUMAN
|
|
195
|
+
* checks, rather than leaving them to hunt for a buried path.
|
|
196
|
+
*
|
|
197
|
+
* Returns null when no milestone id is available.
|
|
198
|
+
*/
|
|
199
|
+
export function buildManualValidationGuidance(basePath, milestoneId, opts = {}) {
|
|
200
|
+
if (!milestoneId)
|
|
201
|
+
return null;
|
|
202
|
+
const validationRoot = resolveCanonicalMilestoneRoot(basePath, milestoneId);
|
|
203
|
+
const inWorktree = validationRoot.includes(`${sep}.gsd${sep}worktrees${sep}`);
|
|
204
|
+
const lines = [`Validate the work here: ${validationRoot}`];
|
|
205
|
+
if (inWorktree) {
|
|
206
|
+
lines.push("This milestone runs in a git worktree, so the code lives under the hidden " +
|
|
207
|
+
`\`.gsd/worktrees/\` path. Open it with: cd "${validationRoot}"`);
|
|
208
|
+
}
|
|
209
|
+
if (opts.uatPath) {
|
|
210
|
+
lines.push(`Follow the UAT checklist at: ${opts.uatPath}`);
|
|
211
|
+
}
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
188
214
|
// ─── Core Operations ───────────────────────────────────────────────────────
|
|
189
215
|
/**
|
|
190
216
|
* Create a new git worktree under .gsd/worktrees/<name>/ with branch worktree/<name>.
|