gsd-pi 2.22.0 → 2.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/dist/cli.js +74 -7
- package/dist/headless.d.ts +25 -0
- package/dist/headless.js +454 -0
- package/dist/help-text.js +47 -0
- package/dist/mcp-server.d.ts +20 -3
- package/dist/mcp-server.js +21 -1
- package/dist/models-resolver.d.ts +32 -0
- package/dist/models-resolver.js +50 -0
- package/dist/resource-loader.js +64 -9
- package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/dist/resources/extensions/bg-shell/types.ts +33 -1
- package/dist/resources/extensions/browser-tools/capture.ts +18 -16
- package/dist/resources/extensions/browser-tools/index.ts +20 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +51 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/dist/resources/extensions/gsd/auto.ts +560 -52
- package/dist/resources/extensions/gsd/captures.ts +49 -0
- package/dist/resources/extensions/gsd/commands.ts +194 -11
- package/dist/resources/extensions/gsd/complexity.ts +1 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +54 -2
- package/dist/resources/extensions/gsd/diff-context.ts +73 -80
- package/dist/resources/extensions/gsd/doctor.ts +76 -12
- package/dist/resources/extensions/gsd/exit-command.ts +2 -2
- package/dist/resources/extensions/gsd/forensics.ts +95 -52
- package/dist/resources/extensions/gsd/gitignore.ts +1 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +85 -5
- package/dist/resources/extensions/gsd/index.ts +34 -1
- package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
- package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +65 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
- package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
- package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
- package/dist/resources/extensions/gsd/state.ts +72 -30
- package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/dist/resources/extensions/gsd/types.ts +15 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
- package/dist/resources/extensions/subagent/index.ts +5 -0
- package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
- package/dist/update-check.d.ts +9 -0
- package/dist/update-check.js +97 -0
- package/package.json +6 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +16 -7
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
- package/packages/pi-ai/dist/providers/google-vertex.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 +12 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +21 -8
- package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
- package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
- package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
- package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
- package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
- package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/scripts/postinstall.js +7 -109
- package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/src/resources/extensions/bg-shell/types.ts +33 -1
- package/src/resources/extensions/browser-tools/capture.ts +18 -16
- package/src/resources/extensions/browser-tools/index.ts +20 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +51 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/src/resources/extensions/gsd/auto.ts +560 -52
- package/src/resources/extensions/gsd/captures.ts +49 -0
- package/src/resources/extensions/gsd/commands.ts +194 -11
- package/src/resources/extensions/gsd/complexity.ts +1 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +54 -2
- package/src/resources/extensions/gsd/diff-context.ts +73 -80
- package/src/resources/extensions/gsd/doctor.ts +76 -12
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/forensics.ts +95 -52
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +85 -5
- package/src/resources/extensions/gsd/index.ts +34 -1
- package/src/resources/extensions/gsd/mcp-server.ts +33 -12
- package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +65 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
- package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/src/resources/extensions/gsd/session-forensics.ts +36 -2
- package/src/resources/extensions/gsd/session-status-io.ts +197 -0
- package/src/resources/extensions/gsd/state.ts +72 -30
- package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/src/resources/extensions/gsd/types.ts +15 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/src/resources/extensions/gsd/workspace-index.ts +34 -6
- package/src/resources/extensions/subagent/index.ts +5 -0
- package/src/resources/extensions/subagent/worker-registry.ts +99 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { ToolDeps } from "../state.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* State persistence tools — save/restore cookies, localStorage, sessionStorage.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const STATE_DIR = ".gsd/browser-state";
|
|
10
|
+
|
|
11
|
+
export function registerStatePersistenceTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
12
|
+
// -------------------------------------------------------------------------
|
|
13
|
+
// browser_save_state
|
|
14
|
+
// -------------------------------------------------------------------------
|
|
15
|
+
pi.registerTool({
|
|
16
|
+
name: "browser_save_state",
|
|
17
|
+
label: "Browser Save State",
|
|
18
|
+
description:
|
|
19
|
+
"Save cookies, localStorage, and sessionStorage to disk so authenticated sessions survive browser restarts. " +
|
|
20
|
+
"State files are written to .gsd/browser-state/ and should be gitignored (may contain auth tokens). " +
|
|
21
|
+
"Never displays secret values in output.",
|
|
22
|
+
parameters: Type.Object({
|
|
23
|
+
name: Type.Optional(
|
|
24
|
+
Type.String({ description: "Name for the state file (default: 'default'). Used as the filename stem." }),
|
|
25
|
+
),
|
|
26
|
+
}),
|
|
27
|
+
|
|
28
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
29
|
+
try {
|
|
30
|
+
const { context: ctx, page: p } = await deps.ensureBrowser();
|
|
31
|
+
const name = deps.sanitizeArtifactName(params.name ?? "default", "default");
|
|
32
|
+
|
|
33
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
34
|
+
const path = await import("node:path");
|
|
35
|
+
const stateDir = path.resolve(process.cwd(), STATE_DIR);
|
|
36
|
+
await mkdir(stateDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
// 1. Playwright storageState: cookies + localStorage
|
|
39
|
+
const storageState = await ctx.storageState();
|
|
40
|
+
|
|
41
|
+
// 2. sessionStorage: must be extracted per-origin via page.evaluate
|
|
42
|
+
const sessionStorageData: Record<string, Record<string, string>> = {};
|
|
43
|
+
try {
|
|
44
|
+
const origin = new URL(p.url()).origin;
|
|
45
|
+
const ssData = await p.evaluate(() => {
|
|
46
|
+
const data: Record<string, string> = {};
|
|
47
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
48
|
+
const key = sessionStorage.key(i);
|
|
49
|
+
if (key) data[key] = sessionStorage.getItem(key) ?? "";
|
|
50
|
+
}
|
|
51
|
+
return data;
|
|
52
|
+
});
|
|
53
|
+
if (Object.keys(ssData).length > 0) {
|
|
54
|
+
sessionStorageData[origin] = ssData;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Page may not have a valid origin (about:blank, etc.)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const combined = {
|
|
61
|
+
storageState,
|
|
62
|
+
sessionStorage: sessionStorageData,
|
|
63
|
+
savedAt: new Date().toISOString(),
|
|
64
|
+
url: p.url(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const filePath = path.join(stateDir, `${name}.json`);
|
|
68
|
+
await writeFile(filePath, JSON.stringify(combined, null, 2));
|
|
69
|
+
|
|
70
|
+
// Ensure .gitignore covers the state dir
|
|
71
|
+
const gitignorePath = path.resolve(process.cwd(), STATE_DIR, ".gitignore");
|
|
72
|
+
await writeFile(gitignorePath, "*\n!.gitignore\n").catch(() => {});
|
|
73
|
+
|
|
74
|
+
const cookieCount = storageState.cookies?.length ?? 0;
|
|
75
|
+
const localStorageOrigins = storageState.origins?.length ?? 0;
|
|
76
|
+
const sessionStorageOrigins = Object.keys(sessionStorageData).length;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
content: [{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: `State saved: ${filePath}\nCookies: ${cookieCount}\nlocalStorage origins: ${localStorageOrigins}\nsessionStorage origins: ${sessionStorageOrigins}`,
|
|
82
|
+
}],
|
|
83
|
+
details: {
|
|
84
|
+
path: filePath,
|
|
85
|
+
cookieCount,
|
|
86
|
+
localStorageOrigins,
|
|
87
|
+
sessionStorageOrigins,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
} catch (err: any) {
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text", text: `Save state failed: ${err.message}` }],
|
|
93
|
+
details: { error: err.message },
|
|
94
|
+
isError: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// -------------------------------------------------------------------------
|
|
101
|
+
// browser_restore_state
|
|
102
|
+
// -------------------------------------------------------------------------
|
|
103
|
+
pi.registerTool({
|
|
104
|
+
name: "browser_restore_state",
|
|
105
|
+
label: "Browser Restore State",
|
|
106
|
+
description:
|
|
107
|
+
"Restore cookies, localStorage, and sessionStorage from a previously saved state file. " +
|
|
108
|
+
"Injects cookies via context.addCookies() and storage via page.evaluate(). " +
|
|
109
|
+
"For full fidelity, restore before navigating to the target site.",
|
|
110
|
+
parameters: Type.Object({
|
|
111
|
+
name: Type.Optional(
|
|
112
|
+
Type.String({ description: "Name of the state file to restore (default: 'default')." }),
|
|
113
|
+
),
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
117
|
+
try {
|
|
118
|
+
const { context: ctx, page: p } = await deps.ensureBrowser();
|
|
119
|
+
const name = deps.sanitizeArtifactName(params.name ?? "default", "default");
|
|
120
|
+
|
|
121
|
+
const { readFile } = await import("node:fs/promises");
|
|
122
|
+
const path = await import("node:path");
|
|
123
|
+
const filePath = path.join(process.cwd(), STATE_DIR, `${name}.json`);
|
|
124
|
+
|
|
125
|
+
let raw: string;
|
|
126
|
+
try {
|
|
127
|
+
raw = await readFile(filePath, "utf-8");
|
|
128
|
+
} catch {
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: "text", text: `State file not found: ${filePath}` }],
|
|
131
|
+
details: { error: "file_not_found", path: filePath },
|
|
132
|
+
isError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const combined = JSON.parse(raw);
|
|
137
|
+
const storageState = combined.storageState;
|
|
138
|
+
const sessionStorageData: Record<string, Record<string, string>> = combined.sessionStorage ?? {};
|
|
139
|
+
|
|
140
|
+
// 1. Restore cookies
|
|
141
|
+
let cookieCount = 0;
|
|
142
|
+
if (storageState?.cookies?.length) {
|
|
143
|
+
await ctx.addCookies(storageState.cookies);
|
|
144
|
+
cookieCount = storageState.cookies.length;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 2. Restore localStorage via page.evaluate
|
|
148
|
+
let localStorageOrigins = 0;
|
|
149
|
+
if (storageState?.origins?.length) {
|
|
150
|
+
for (const origin of storageState.origins) {
|
|
151
|
+
try {
|
|
152
|
+
await p.evaluate((items: Array<{ name: string; value: string }>) => {
|
|
153
|
+
for (const { name, value } of items) {
|
|
154
|
+
localStorage.setItem(name, value);
|
|
155
|
+
}
|
|
156
|
+
}, origin.localStorage ?? []);
|
|
157
|
+
localStorageOrigins++;
|
|
158
|
+
} catch {
|
|
159
|
+
// Origin mismatch — localStorage can only be set on matching origin
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 3. Restore sessionStorage via page.evaluate
|
|
165
|
+
let sessionStorageOrigins = 0;
|
|
166
|
+
for (const [_origin, data] of Object.entries(sessionStorageData)) {
|
|
167
|
+
try {
|
|
168
|
+
await p.evaluate((items: Record<string, string>) => {
|
|
169
|
+
for (const [key, value] of Object.entries(items)) {
|
|
170
|
+
sessionStorage.setItem(key, value);
|
|
171
|
+
}
|
|
172
|
+
}, data);
|
|
173
|
+
sessionStorageOrigins++;
|
|
174
|
+
} catch {
|
|
175
|
+
// Origin mismatch
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
content: [{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: `State restored from: ${filePath}\nCookies: ${cookieCount}\nlocalStorage origins: ${localStorageOrigins}\nsessionStorage origins: ${sessionStorageOrigins}\nSaved at: ${combined.savedAt ?? "unknown"}`,
|
|
183
|
+
}],
|
|
184
|
+
details: {
|
|
185
|
+
path: filePath,
|
|
186
|
+
cookieCount,
|
|
187
|
+
localStorageOrigins,
|
|
188
|
+
sessionStorageOrigins,
|
|
189
|
+
savedAt: combined.savedAt,
|
|
190
|
+
savedUrl: combined.url,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
} catch (err: any) {
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: "text", text: `Restore state failed: ${err.message}` }],
|
|
196
|
+
details: { error: err.message },
|
|
197
|
+
isError: true,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { ToolDeps } from "../state.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Visual regression diffing — compare current page screenshot against a stored baseline.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BASELINE_DIR = ".gsd/browser-baselines";
|
|
10
|
+
|
|
11
|
+
export function registerVisualDiffTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
12
|
+
pi.registerTool({
|
|
13
|
+
name: "browser_visual_diff",
|
|
14
|
+
label: "Browser Visual Diff",
|
|
15
|
+
description:
|
|
16
|
+
"Compare current page screenshot against a stored baseline pixel-by-pixel. " +
|
|
17
|
+
"Returns similarity score (0–1), diff pixel count, and optionally generates a diff image highlighting changes. " +
|
|
18
|
+
"On first run with no baseline, saves the current screenshot as the baseline. " +
|
|
19
|
+
"Baselines are stored in .gsd/browser-baselines/ (gitignored, environment-specific).",
|
|
20
|
+
parameters: Type.Object({
|
|
21
|
+
name: Type.Optional(
|
|
22
|
+
Type.String({
|
|
23
|
+
description:
|
|
24
|
+
"Baseline name (default: auto-generated from URL + viewport). " +
|
|
25
|
+
"Use consistent names to compare the same view across runs.",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
selector: Type.Optional(
|
|
29
|
+
Type.String({
|
|
30
|
+
description: "CSS selector to scope comparison to a specific element instead of full viewport.",
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
threshold: Type.Optional(
|
|
34
|
+
Type.Number({
|
|
35
|
+
description:
|
|
36
|
+
"Pixel matching threshold 0–1 (default: 0.1). " +
|
|
37
|
+
"Higher values are more tolerant of anti-aliasing and rendering differences.",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
updateBaseline: Type.Optional(
|
|
41
|
+
Type.Boolean({
|
|
42
|
+
description: "If true, overwrite the existing baseline with the current screenshot (default: false).",
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
48
|
+
try {
|
|
49
|
+
const { page: p } = await deps.ensureBrowser();
|
|
50
|
+
const { mkdir, readFile, writeFile } = await import("node:fs/promises");
|
|
51
|
+
const pathMod = await import("node:path");
|
|
52
|
+
|
|
53
|
+
const baselineDir = pathMod.resolve(process.cwd(), BASELINE_DIR);
|
|
54
|
+
await mkdir(baselineDir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
// Ensure .gitignore
|
|
57
|
+
const gitignorePath = pathMod.join(baselineDir, ".gitignore");
|
|
58
|
+
await writeFile(gitignorePath, "*\n!.gitignore\n").catch(() => {});
|
|
59
|
+
|
|
60
|
+
// Generate baseline name
|
|
61
|
+
const url = p.url();
|
|
62
|
+
const viewport = p.viewportSize();
|
|
63
|
+
const vpSuffix = viewport ? `${viewport.width}x${viewport.height}` : "unknown";
|
|
64
|
+
const autoName = deps.sanitizeArtifactName(
|
|
65
|
+
`${new URL(url).pathname.replace(/\//g, "-")}-${vpSuffix}`,
|
|
66
|
+
`baseline-${vpSuffix}`,
|
|
67
|
+
);
|
|
68
|
+
const name = deps.sanitizeArtifactName(params.name ?? autoName, autoName);
|
|
69
|
+
|
|
70
|
+
const baselinePath = pathMod.join(baselineDir, `${name}.png`);
|
|
71
|
+
const diffPath = pathMod.join(baselineDir, `${name}-diff.png`);
|
|
72
|
+
|
|
73
|
+
// Capture current screenshot as PNG (needed for pixel comparison)
|
|
74
|
+
let currentBuffer: Buffer;
|
|
75
|
+
if (params.selector) {
|
|
76
|
+
const locator = p.locator(params.selector).first();
|
|
77
|
+
currentBuffer = await locator.screenshot({ type: "png" });
|
|
78
|
+
} else {
|
|
79
|
+
currentBuffer = await p.screenshot({ type: "png", fullPage: false });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if baseline exists
|
|
83
|
+
let baselineBuffer: Buffer | null = null;
|
|
84
|
+
try {
|
|
85
|
+
baselineBuffer = await readFile(baselinePath) as Buffer;
|
|
86
|
+
} catch {
|
|
87
|
+
// No baseline yet
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!baselineBuffer || params.updateBaseline) {
|
|
91
|
+
// Save as new baseline
|
|
92
|
+
await writeFile(baselinePath, currentBuffer);
|
|
93
|
+
return {
|
|
94
|
+
content: [{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: baselineBuffer
|
|
97
|
+
? `Baseline updated: ${baselinePath}\nSize: ${(currentBuffer.length / 1024).toFixed(1)} KB`
|
|
98
|
+
: `Baseline created (first run): ${baselinePath}\nSize: ${(currentBuffer.length / 1024).toFixed(1)} KB\nRe-run to compare against this baseline.`,
|
|
99
|
+
}],
|
|
100
|
+
details: {
|
|
101
|
+
baselinePath,
|
|
102
|
+
baselineCreated: !baselineBuffer,
|
|
103
|
+
baselineUpdated: !!baselineBuffer,
|
|
104
|
+
sizeBytes: currentBuffer.length,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Perform pixel comparison using sharp for PNG decoding
|
|
110
|
+
const sharp = (await import("sharp")).default;
|
|
111
|
+
|
|
112
|
+
const baselineMeta = await sharp(baselineBuffer).metadata();
|
|
113
|
+
const currentMeta = await sharp(currentBuffer).metadata();
|
|
114
|
+
|
|
115
|
+
const bWidth = baselineMeta.width ?? 0;
|
|
116
|
+
const bHeight = baselineMeta.height ?? 0;
|
|
117
|
+
const cWidth = currentMeta.width ?? 0;
|
|
118
|
+
const cHeight = currentMeta.height ?? 0;
|
|
119
|
+
|
|
120
|
+
// If dimensions differ, report mismatch
|
|
121
|
+
if (bWidth !== cWidth || bHeight !== cHeight) {
|
|
122
|
+
return {
|
|
123
|
+
content: [{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: `Dimension mismatch: baseline is ${bWidth}x${bHeight}, current is ${cWidth}x${cHeight}. Cannot compare.\nUse updateBaseline: true to reset.`,
|
|
126
|
+
}],
|
|
127
|
+
details: {
|
|
128
|
+
match: false,
|
|
129
|
+
dimensionMismatch: true,
|
|
130
|
+
baselineDimensions: { width: bWidth, height: bHeight },
|
|
131
|
+
currentDimensions: { width: cWidth, height: cHeight },
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Extract raw RGBA pixel data
|
|
137
|
+
const baselineRaw = await sharp(baselineBuffer).ensureAlpha().raw().toBuffer();
|
|
138
|
+
const currentRaw = await sharp(currentBuffer).ensureAlpha().raw().toBuffer();
|
|
139
|
+
|
|
140
|
+
const width = bWidth;
|
|
141
|
+
const height = bHeight;
|
|
142
|
+
const totalPixels = width * height;
|
|
143
|
+
const threshold = params.threshold ?? 0.1;
|
|
144
|
+
|
|
145
|
+
// Simple pixel-by-pixel comparison (avoiding pixelmatch dependency)
|
|
146
|
+
const diffData = Buffer.alloc(width * height * 4);
|
|
147
|
+
let diffPixels = 0;
|
|
148
|
+
const thresholdSq = threshold * threshold * 255 * 255 * 3;
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < totalPixels; i++) {
|
|
151
|
+
const offset = i * 4;
|
|
152
|
+
const dr = baselineRaw[offset] - currentRaw[offset];
|
|
153
|
+
const dg = baselineRaw[offset + 1] - currentRaw[offset + 1];
|
|
154
|
+
const db = baselineRaw[offset + 2] - currentRaw[offset + 2];
|
|
155
|
+
const distSq = dr * dr + dg * dg + db * db;
|
|
156
|
+
|
|
157
|
+
if (distSq > thresholdSq) {
|
|
158
|
+
diffPixels++;
|
|
159
|
+
// Mark diff pixels as red
|
|
160
|
+
diffData[offset] = 255; // R
|
|
161
|
+
diffData[offset + 1] = 0; // G
|
|
162
|
+
diffData[offset + 2] = 0; // B
|
|
163
|
+
diffData[offset + 3] = 255; // A
|
|
164
|
+
} else {
|
|
165
|
+
// Dim unchanged pixels
|
|
166
|
+
diffData[offset] = currentRaw[offset] >> 1;
|
|
167
|
+
diffData[offset + 1] = currentRaw[offset + 1] >> 1;
|
|
168
|
+
diffData[offset + 2] = currentRaw[offset + 2] >> 1;
|
|
169
|
+
diffData[offset + 3] = 255;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const similarity = 1 - (diffPixels / totalPixels);
|
|
174
|
+
const match = diffPixels === 0;
|
|
175
|
+
|
|
176
|
+
// Save diff image
|
|
177
|
+
await sharp(diffData, { raw: { width, height, channels: 4 } })
|
|
178
|
+
.png()
|
|
179
|
+
.toFile(diffPath);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
content: [{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: match
|
|
185
|
+
? `Visual diff: MATCH (100% similar)\nBaseline: ${baselinePath}`
|
|
186
|
+
: `Visual diff: ${(similarity * 100).toFixed(2)}% similar\nDiff pixels: ${diffPixels} of ${totalPixels} (${((diffPixels / totalPixels) * 100).toFixed(2)}%)\nDiff image: ${diffPath}\nBaseline: ${baselinePath}`,
|
|
187
|
+
}],
|
|
188
|
+
details: {
|
|
189
|
+
match,
|
|
190
|
+
similarity,
|
|
191
|
+
diffPixels,
|
|
192
|
+
totalPixels,
|
|
193
|
+
diffPercentage: (diffPixels / totalPixels) * 100,
|
|
194
|
+
dimensions: { width, height },
|
|
195
|
+
baselinePath,
|
|
196
|
+
diffImagePath: match ? undefined : diffPath,
|
|
197
|
+
threshold,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
} catch (err: any) {
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text: `Visual diff failed: ${err.message}` }],
|
|
203
|
+
details: { error: err.message },
|
|
204
|
+
isError: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { ToolDeps } from "../state.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Region zoom / high-res capture — capture and upscale specific page regions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function registerZoomTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
10
|
+
pi.registerTool({
|
|
11
|
+
name: "browser_zoom_region",
|
|
12
|
+
label: "Browser Zoom Region",
|
|
13
|
+
description:
|
|
14
|
+
"Capture and optionally upscale a specific rectangular region of the page for detailed inspection. " +
|
|
15
|
+
"Useful for dense UIs where full-page screenshots have text too small to read. " +
|
|
16
|
+
"Returns the region as an inline image, same as browser_screenshot.",
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
x: Type.Number({ description: "Left coordinate of the region in CSS pixels." }),
|
|
19
|
+
y: Type.Number({ description: "Top coordinate of the region in CSS pixels." }),
|
|
20
|
+
width: Type.Number({ description: "Width of the region in CSS pixels." }),
|
|
21
|
+
height: Type.Number({ description: "Height of the region in CSS pixels." }),
|
|
22
|
+
scale: Type.Optional(
|
|
23
|
+
Type.Number({
|
|
24
|
+
description: "Upscale factor (default: 2). Use 1 for native resolution, 2-4 for zoomed detail.",
|
|
25
|
+
}),
|
|
26
|
+
),
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
30
|
+
try {
|
|
31
|
+
const { page: p } = await deps.ensureBrowser();
|
|
32
|
+
const { x, y, width, height } = params;
|
|
33
|
+
const scale = params.scale ?? 2;
|
|
34
|
+
|
|
35
|
+
// Validate dimensions
|
|
36
|
+
if (width <= 0 || height <= 0) {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: "Width and height must be positive." }],
|
|
39
|
+
details: { error: "invalid_dimensions" },
|
|
40
|
+
isError: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Capture the region using Playwright's clip option
|
|
45
|
+
const regionBuffer = await p.screenshot({
|
|
46
|
+
type: "png",
|
|
47
|
+
clip: { x, y, width, height },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
let outputBuffer: Buffer = regionBuffer;
|
|
51
|
+
let outputMime = "image/png";
|
|
52
|
+
|
|
53
|
+
// Upscale if scale > 1
|
|
54
|
+
if (scale > 1) {
|
|
55
|
+
const sharp = (await import("sharp")).default;
|
|
56
|
+
const targetWidth = Math.round(width * scale);
|
|
57
|
+
const targetHeight = Math.round(height * scale);
|
|
58
|
+
|
|
59
|
+
outputBuffer = await sharp(regionBuffer)
|
|
60
|
+
.resize(targetWidth, targetHeight, {
|
|
61
|
+
kernel: "lanczos3",
|
|
62
|
+
fit: "fill",
|
|
63
|
+
})
|
|
64
|
+
.png()
|
|
65
|
+
.toBuffer();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const base64Data = outputBuffer.toString("base64");
|
|
69
|
+
const title = await p.title();
|
|
70
|
+
const url = p.url();
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: `Region capture: ${width}x${height} at (${x},${y})${scale > 1 ? ` upscaled ${scale}x to ${Math.round(width * scale)}x${Math.round(height * scale)}` : ""}\nPage: ${title}\nURL: ${url}`,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: "image",
|
|
80
|
+
data: base64Data,
|
|
81
|
+
mimeType: outputMime,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
details: {
|
|
85
|
+
region: { x, y, width, height },
|
|
86
|
+
scale,
|
|
87
|
+
outputDimensions: {
|
|
88
|
+
width: Math.round(width * scale),
|
|
89
|
+
height: Math.round(height * scale),
|
|
90
|
+
},
|
|
91
|
+
title,
|
|
92
|
+
url,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: `Region zoom failed: ${err.message}` }],
|
|
98
|
+
details: { error: err.message },
|
|
99
|
+
isError: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -41,6 +41,8 @@ export interface AutoDashboardData {
|
|
|
41
41
|
profileDowngraded?: boolean;
|
|
42
42
|
/** Number of pending captures awaiting triage (0 if none or file missing) */
|
|
43
43
|
pendingCaptureCount: number;
|
|
44
|
+
/** Cross-process: another auto-mode session detected via auto.lock (PID, startedAt) */
|
|
45
|
+
remoteSession?: { pid: number; startedAt: string; unitType: string; unitId: string };
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// ─── Unit Description Helpers ─────────────────────────────────────────────────
|
|
@@ -14,9 +14,11 @@ import type { GSDPreferences } from "./preferences.js";
|
|
|
14
14
|
import type { UatType } from "./files.js";
|
|
15
15
|
import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
|
|
16
16
|
import {
|
|
17
|
-
resolveMilestoneFile, resolveSliceFile,
|
|
18
|
-
relSliceFile,
|
|
17
|
+
resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile,
|
|
18
|
+
relSliceFile, buildMilestoneFileName,
|
|
19
19
|
} from "./paths.js";
|
|
20
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
20
22
|
import {
|
|
21
23
|
buildResearchMilestonePrompt,
|
|
22
24
|
buildPlanMilestonePrompt,
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
buildExecuteTaskPrompt,
|
|
26
28
|
buildCompleteSlicePrompt,
|
|
27
29
|
buildCompleteMilestonePrompt,
|
|
30
|
+
buildValidateMilestonePrompt,
|
|
28
31
|
buildReplanSlicePrompt,
|
|
29
32
|
buildRunUatPrompt,
|
|
30
33
|
buildReassessRoadmapPrompt,
|
|
@@ -246,6 +249,20 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
246
249
|
const sTitle = state.activeSlice!.title;
|
|
247
250
|
const tid = state.activeTask.id;
|
|
248
251
|
const tTitle = state.activeTask.title;
|
|
252
|
+
|
|
253
|
+
// Guard: refuse to dispatch execute-task when the task plan file is missing.
|
|
254
|
+
// This prevents the agent from running blind after a failed plan-slice that
|
|
255
|
+
// wrote S{sid}-PLAN.md but omitted the individual T{tid}-PLAN.md files.
|
|
256
|
+
// (See issue #739 — missing task plan caused runaway execution and EPIPE crash.)
|
|
257
|
+
const taskPlanPath = resolveTaskFile(basePath, mid, sid, tid, "PLAN");
|
|
258
|
+
if (!taskPlanPath || !existsSync(taskPlanPath)) {
|
|
259
|
+
return {
|
|
260
|
+
action: "stop",
|
|
261
|
+
reason: `Task plan ${tid}-PLAN.md is missing for ${mid}/${sid}/${tid}. Re-run plan-slice to regenerate task plans, or create the file manually and resume.`,
|
|
262
|
+
level: "error",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
249
266
|
return {
|
|
250
267
|
action: "dispatch",
|
|
251
268
|
unitType: "execute-task",
|
|
@@ -254,6 +271,38 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
254
271
|
};
|
|
255
272
|
},
|
|
256
273
|
},
|
|
274
|
+
{
|
|
275
|
+
name: "validating-milestone → validate-milestone",
|
|
276
|
+
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
277
|
+
if (state.phase !== "validating-milestone") return null;
|
|
278
|
+
// Skip preference: write a minimal pass-through VALIDATION file
|
|
279
|
+
if (prefs?.phases?.skip_milestone_validation) {
|
|
280
|
+
const mDir = resolveMilestonePath(basePath, mid);
|
|
281
|
+
if (mDir) {
|
|
282
|
+
if (!existsSync(mDir)) mkdirSync(mDir, { recursive: true });
|
|
283
|
+
const validationPath = join(mDir, buildMilestoneFileName(mid, "VALIDATION"));
|
|
284
|
+
const content = [
|
|
285
|
+
"---",
|
|
286
|
+
"verdict: pass",
|
|
287
|
+
"remediation_round: 0",
|
|
288
|
+
"---",
|
|
289
|
+
"",
|
|
290
|
+
"# Milestone Validation (skipped by preference)",
|
|
291
|
+
"",
|
|
292
|
+
"Milestone validation was skipped via `skip_milestone_validation` preference.",
|
|
293
|
+
].join("\n");
|
|
294
|
+
writeFileSync(validationPath, content, "utf-8");
|
|
295
|
+
}
|
|
296
|
+
return { action: "skip" };
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
action: "dispatch",
|
|
300
|
+
unitType: "validate-milestone",
|
|
301
|
+
unitId: mid,
|
|
302
|
+
prompt: await buildValidateMilestonePrompt(mid, midTitle, basePath),
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
},
|
|
257
306
|
{
|
|
258
307
|
name: "completing-milestone → complete-milestone",
|
|
259
308
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
@@ -855,6 +855,79 @@ export async function buildCompleteMilestonePrompt(
|
|
|
855
855
|
});
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
+
export async function buildValidateMilestonePrompt(
|
|
859
|
+
mid: string, midTitle: string, base: string, level?: InlineLevel,
|
|
860
|
+
): Promise<string> {
|
|
861
|
+
const inlineLevel = level ?? resolveInlineLevel();
|
|
862
|
+
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
863
|
+
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
864
|
+
|
|
865
|
+
const inlined: string[] = [];
|
|
866
|
+
inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
|
|
867
|
+
|
|
868
|
+
// Inline all slice summaries and UAT results
|
|
869
|
+
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
870
|
+
if (roadmapContent) {
|
|
871
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
872
|
+
const seenSlices = new Set<string>();
|
|
873
|
+
for (const slice of roadmap.slices) {
|
|
874
|
+
if (seenSlices.has(slice.id)) continue;
|
|
875
|
+
seenSlices.add(slice.id);
|
|
876
|
+
const summaryPath = resolveSliceFile(base, mid, slice.id, "SUMMARY");
|
|
877
|
+
const summaryRel = relSliceFile(base, mid, slice.id, "SUMMARY");
|
|
878
|
+
inlined.push(await inlineFile(summaryPath, summaryRel, `${slice.id} Summary`));
|
|
879
|
+
|
|
880
|
+
const uatPath = resolveSliceFile(base, mid, slice.id, "UAT-RESULT");
|
|
881
|
+
const uatRel = relSliceFile(base, mid, slice.id, "UAT-RESULT");
|
|
882
|
+
const uatInline = await inlineFileOptional(uatPath, uatRel, `${slice.id} UAT Result`);
|
|
883
|
+
if (uatInline) inlined.push(uatInline);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Inline existing VALIDATION file if this is a re-validation round
|
|
888
|
+
const validationPath = resolveMilestoneFile(base, mid, "VALIDATION");
|
|
889
|
+
const validationRel = relMilestoneFile(base, mid, "VALIDATION");
|
|
890
|
+
const validationContent = validationPath ? await loadFile(validationPath) : null;
|
|
891
|
+
let remediationRound = 0;
|
|
892
|
+
if (validationContent) {
|
|
893
|
+
const roundMatch = validationContent.match(/remediation_round:\s*(\d+)/);
|
|
894
|
+
remediationRound = roundMatch ? parseInt(roundMatch[1], 10) + 1 : 1;
|
|
895
|
+
inlined.push(`### Previous Validation (re-validation round ${remediationRound})\nSource: \`${validationRel}\`\n\n${validationContent.trim()}`);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Inline root GSD files
|
|
899
|
+
if (inlineLevel !== "minimal") {
|
|
900
|
+
const requirementsInline = await inlineRequirementsFromDb(base);
|
|
901
|
+
if (requirementsInline) inlined.push(requirementsInline);
|
|
902
|
+
const decisionsInline = await inlineDecisionsFromDb(base, mid);
|
|
903
|
+
if (decisionsInline) inlined.push(decisionsInline);
|
|
904
|
+
const projectInline = await inlineProjectFromDb(base);
|
|
905
|
+
if (projectInline) inlined.push(projectInline);
|
|
906
|
+
}
|
|
907
|
+
const knowledgeInline = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
908
|
+
if (knowledgeInline) inlined.push(knowledgeInline);
|
|
909
|
+
// Inline milestone context file
|
|
910
|
+
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
911
|
+
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
|
912
|
+
const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
|
|
913
|
+
if (contextInline) inlined.push(contextInline);
|
|
914
|
+
|
|
915
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
916
|
+
|
|
917
|
+
const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
|
|
918
|
+
const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
|
|
919
|
+
|
|
920
|
+
return loadPrompt("validate-milestone", {
|
|
921
|
+
workingDirectory: base,
|
|
922
|
+
milestoneId: mid,
|
|
923
|
+
milestoneTitle: midTitle,
|
|
924
|
+
roadmapPath: roadmapOutputPath,
|
|
925
|
+
inlinedContext,
|
|
926
|
+
validationPath: validationOutputPath,
|
|
927
|
+
remediationRound: String(remediationRound),
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
858
931
|
export async function buildReplanSlicePrompt(
|
|
859
932
|
mid: string, midTitle: string, sid: string, sTitle: string, base: string,
|
|
860
933
|
): Promise<string> {
|