gsd-pi 2.46.0 → 2.46.1-dev.79664f2
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 +1 -0
- package/dist/resources/extensions/claude-code-cli/index.js +25 -0
- package/dist/resources/extensions/claude-code-cli/models.js +40 -0
- package/dist/resources/extensions/claude-code-cli/package.json +11 -0
- package/dist/resources/extensions/claude-code-cli/partial-builder.js +223 -0
- package/dist/resources/extensions/claude-code-cli/readiness.js +26 -0
- package/dist/resources/extensions/claude-code-cli/sdk-types.js +8 -0
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +293 -0
- package/dist/resources/extensions/gsd/auto-start.js +7 -7
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +2 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -2
- package/dist/resources/extensions/gsd/workflow-events.js +1 -1
- package/dist/resources/extensions/remote-questions/config.js +42 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- 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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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 +3 -3
- 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/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +5 -5
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/chunks/229.js +1 -1
- package/dist/web/standalone/.next/server/chunks/471.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-6654a8cca61a3d1c.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +2 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/index.ts +28 -0
- package/src/resources/extensions/claude-code-cli/models.ts +42 -0
- package/src/resources/extensions/claude-code-cli/package.json +11 -0
- package/src/resources/extensions/claude-code-cli/partial-builder.ts +258 -0
- package/src/resources/extensions/claude-code-cli/readiness.ts +30 -0
- package/src/resources/extensions/claude-code-cli/sdk-types.ts +149 -0
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +355 -0
- package/src/resources/extensions/gsd/auto-start.ts +8 -8
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +3 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/run-uat.md +2 -2
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +84 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +25 -0
- package/src/resources/extensions/gsd/workflow-events.ts +1 -1
- package/src/resources/extensions/remote-questions/config.ts +45 -0
- package/dist/web/standalone/.next/static/chunks/app/page-12dd5ece0df4badc.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{8zT99piZz8u3xAU3Omz2g → vP6aj-TThZymVNx5Pi2AN}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{8zT99piZz8u3xAU3Omz2g → vP6aj-TThZymVNx5Pi2AN}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream adapter: bridges the Claude Agent SDK into GSD's streamSimple contract.
|
|
3
|
+
*
|
|
4
|
+
* The SDK runs the full agentic loop (multi-turn, tool execution, compaction)
|
|
5
|
+
* in one call. This adapter translates the SDK's streaming output into
|
|
6
|
+
* AssistantMessageEvents for TUI rendering, then strips tool-call blocks from
|
|
7
|
+
* the final AssistantMessage so GSD's agent loop doesn't try to dispatch them.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
AssistantMessage,
|
|
12
|
+
AssistantMessageEvent,
|
|
13
|
+
AssistantMessageEventStream,
|
|
14
|
+
Context,
|
|
15
|
+
Model,
|
|
16
|
+
SimpleStreamOptions,
|
|
17
|
+
} from "@gsd/pi-ai";
|
|
18
|
+
import { EventStream } from "@gsd/pi-ai";
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js";
|
|
21
|
+
import type {
|
|
22
|
+
SDKAssistantMessage,
|
|
23
|
+
SDKMessage,
|
|
24
|
+
SDKPartialAssistantMessage,
|
|
25
|
+
SDKResultMessage,
|
|
26
|
+
SDKSystemMessage,
|
|
27
|
+
SDKStatusMessage,
|
|
28
|
+
SDKUserMessage,
|
|
29
|
+
} from "./sdk-types.js";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Stream factory
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Construct an AssistantMessageEventStream using EventStream directly.
|
|
37
|
+
* (The class itself is only re-exported as a type from the @gsd/pi-ai barrel.)
|
|
38
|
+
*/
|
|
39
|
+
function createAssistantStream(): AssistantMessageEventStream {
|
|
40
|
+
return new EventStream<AssistantMessageEvent, AssistantMessage>(
|
|
41
|
+
(event) => event.type === "done" || event.type === "error",
|
|
42
|
+
(event) => {
|
|
43
|
+
if (event.type === "done") return event.message;
|
|
44
|
+
if (event.type === "error") return event.error;
|
|
45
|
+
throw new Error("Unexpected event type for final result");
|
|
46
|
+
},
|
|
47
|
+
) as AssistantMessageEventStream;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Claude binary resolution
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
let cachedClaudePath: string | null = null;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the path to the system-installed `claude` binary.
|
|
58
|
+
* The SDK defaults to a bundled cli.js which doesn't exist when
|
|
59
|
+
* installed as a library — we need to point it at the real CLI.
|
|
60
|
+
*/
|
|
61
|
+
function getClaudePath(): string {
|
|
62
|
+
if (cachedClaudePath) return cachedClaudePath;
|
|
63
|
+
try {
|
|
64
|
+
cachedClaudePath = execSync("which claude", { timeout: 5_000, stdio: "pipe" })
|
|
65
|
+
.toString()
|
|
66
|
+
.trim();
|
|
67
|
+
} catch {
|
|
68
|
+
cachedClaudePath = "claude"; // fall back to PATH resolution
|
|
69
|
+
}
|
|
70
|
+
return cachedClaudePath;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Prompt extraction
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extract the last user prompt text from GSD's context messages.
|
|
79
|
+
* The SDK manages its own conversation history — we only send
|
|
80
|
+
* the latest user message as the prompt.
|
|
81
|
+
*/
|
|
82
|
+
function extractLastUserPrompt(context: Context): string {
|
|
83
|
+
for (let i = context.messages.length - 1; i >= 0; i--) {
|
|
84
|
+
const msg = context.messages[i];
|
|
85
|
+
if (msg.role === "user") {
|
|
86
|
+
if (typeof msg.content === "string") return msg.content;
|
|
87
|
+
if (Array.isArray(msg.content)) {
|
|
88
|
+
const textParts = msg.content
|
|
89
|
+
.filter((part: any) => part.type === "text")
|
|
90
|
+
.map((part: any) => part.text);
|
|
91
|
+
if (textParts.length > 0) return textParts.join("\n");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Error helper
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
function makeErrorMessage(model: string, errorMsg: string): AssistantMessage {
|
|
103
|
+
return {
|
|
104
|
+
role: "assistant",
|
|
105
|
+
content: [{ type: "text", text: `Claude Code error: ${errorMsg}` }],
|
|
106
|
+
api: "anthropic-messages",
|
|
107
|
+
provider: "claude-code",
|
|
108
|
+
model,
|
|
109
|
+
usage: { ...ZERO_USAGE },
|
|
110
|
+
stopReason: "error",
|
|
111
|
+
errorMessage: errorMsg,
|
|
112
|
+
timestamp: Date.now(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// streamSimple implementation
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* GSD streamSimple function that delegates to the Claude Agent SDK.
|
|
122
|
+
*
|
|
123
|
+
* Emits AssistantMessageEvent deltas for real-time TUI rendering
|
|
124
|
+
* (thinking, text, tool calls). The final AssistantMessage has tool-call
|
|
125
|
+
* blocks stripped so the agent loop ends the turn without local dispatch.
|
|
126
|
+
*/
|
|
127
|
+
export function streamViaClaudeCode(
|
|
128
|
+
model: Model<any>,
|
|
129
|
+
context: Context,
|
|
130
|
+
options?: SimpleStreamOptions,
|
|
131
|
+
): AssistantMessageEventStream {
|
|
132
|
+
const stream = createAssistantStream();
|
|
133
|
+
|
|
134
|
+
void pumpSdkMessages(model, context, options, stream);
|
|
135
|
+
|
|
136
|
+
return stream;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function pumpSdkMessages(
|
|
140
|
+
model: Model<any>,
|
|
141
|
+
context: Context,
|
|
142
|
+
options: SimpleStreamOptions | undefined,
|
|
143
|
+
stream: AssistantMessageEventStream,
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const modelId = model.id;
|
|
146
|
+
let builder: PartialMessageBuilder | null = null;
|
|
147
|
+
/** Track the last text content seen across all assistant turns for the final message. */
|
|
148
|
+
let lastTextContent = "";
|
|
149
|
+
let lastThinkingContent = "";
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Dynamic import — the SDK is an optional dependency.
|
|
153
|
+
const sdkModule = "@anthropic-ai/claude-agent-sdk";
|
|
154
|
+
const sdk = (await import(/* webpackIgnore: true */ sdkModule)) as {
|
|
155
|
+
query: (args: {
|
|
156
|
+
prompt: string | AsyncIterable<unknown>;
|
|
157
|
+
options?: Record<string, unknown>;
|
|
158
|
+
}) => AsyncIterable<SDKMessage>;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Bridge GSD's AbortSignal to SDK's AbortController
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
if (options?.signal) {
|
|
164
|
+
options.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const prompt = extractLastUserPrompt(context);
|
|
168
|
+
|
|
169
|
+
const queryResult = sdk.query({
|
|
170
|
+
prompt,
|
|
171
|
+
options: {
|
|
172
|
+
pathToClaudeCodeExecutable: getClaudePath(),
|
|
173
|
+
model: modelId,
|
|
174
|
+
includePartialMessages: true,
|
|
175
|
+
persistSession: false,
|
|
176
|
+
abortController: controller,
|
|
177
|
+
cwd: process.cwd(),
|
|
178
|
+
permissionMode: "bypassPermissions",
|
|
179
|
+
allowDangerouslySkipPermissions: true,
|
|
180
|
+
settingSources: ["project"],
|
|
181
|
+
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
182
|
+
betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Emit start with an empty partial
|
|
187
|
+
const initialPartial: AssistantMessage = {
|
|
188
|
+
role: "assistant",
|
|
189
|
+
content: [],
|
|
190
|
+
api: "anthropic-messages",
|
|
191
|
+
provider: "claude-code",
|
|
192
|
+
model: modelId,
|
|
193
|
+
usage: { ...ZERO_USAGE },
|
|
194
|
+
stopReason: "stop",
|
|
195
|
+
timestamp: Date.now(),
|
|
196
|
+
};
|
|
197
|
+
stream.push({ type: "start", partial: initialPartial });
|
|
198
|
+
|
|
199
|
+
for await (const msg of queryResult as AsyncIterable<SDKMessage>) {
|
|
200
|
+
if (options?.signal?.aborted) break;
|
|
201
|
+
|
|
202
|
+
switch (msg.type) {
|
|
203
|
+
// -- Init --
|
|
204
|
+
case "system": {
|
|
205
|
+
// Nothing to emit — the stream is already started.
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// -- Streaming partial messages --
|
|
210
|
+
case "stream_event": {
|
|
211
|
+
const partial = msg as SDKPartialAssistantMessage;
|
|
212
|
+
if (partial.parent_tool_use_id !== null) break; // skip subagent
|
|
213
|
+
|
|
214
|
+
const event = partial.event;
|
|
215
|
+
|
|
216
|
+
// New assistant turn starts with message_start
|
|
217
|
+
if (event.type === "message_start") {
|
|
218
|
+
builder = new PartialMessageBuilder(
|
|
219
|
+
(event as any).message?.model ?? modelId,
|
|
220
|
+
);
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!builder) break;
|
|
225
|
+
|
|
226
|
+
const assistantEvent = builder.handleEvent(event);
|
|
227
|
+
if (assistantEvent) {
|
|
228
|
+
stream.push(assistantEvent);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// -- Complete assistant message (non-streaming fallback) --
|
|
234
|
+
case "assistant": {
|
|
235
|
+
const sdkAssistant = msg as SDKAssistantMessage;
|
|
236
|
+
if (sdkAssistant.parent_tool_use_id !== null) break;
|
|
237
|
+
|
|
238
|
+
// Capture text content from complete messages
|
|
239
|
+
for (const block of sdkAssistant.message.content) {
|
|
240
|
+
if (block.type === "text") {
|
|
241
|
+
lastTextContent = block.text;
|
|
242
|
+
} else if (block.type === "thinking") {
|
|
243
|
+
lastThinkingContent = block.thinking;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// -- User message (synthetic tool result — signals turn boundary) --
|
|
250
|
+
case "user": {
|
|
251
|
+
const userMsg = msg as SDKUserMessage;
|
|
252
|
+
if (userMsg.parent_tool_use_id !== null) break;
|
|
253
|
+
|
|
254
|
+
// Capture accumulated text from the builder before resetting
|
|
255
|
+
if (builder) {
|
|
256
|
+
for (const block of builder.message.content) {
|
|
257
|
+
if (block.type === "text" && block.text) {
|
|
258
|
+
lastTextContent = block.text;
|
|
259
|
+
} else if (block.type === "thinking" && block.thinking) {
|
|
260
|
+
lastThinkingContent = block.thinking;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
builder = null;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// -- Result (terminal) --
|
|
269
|
+
case "result": {
|
|
270
|
+
const result = msg as SDKResultMessage;
|
|
271
|
+
|
|
272
|
+
// Build final message with text/thinking only (strip tool calls)
|
|
273
|
+
const finalContent: AssistantMessage["content"] = [];
|
|
274
|
+
|
|
275
|
+
// Use builder's accumulated content if available, falling back to captured text
|
|
276
|
+
if (builder) {
|
|
277
|
+
for (const block of builder.message.content) {
|
|
278
|
+
if (block.type === "text" && block.text) {
|
|
279
|
+
lastTextContent = block.text;
|
|
280
|
+
} else if (block.type === "thinking" && block.thinking) {
|
|
281
|
+
lastThinkingContent = block.thinking;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (lastThinkingContent) {
|
|
287
|
+
finalContent.push({ type: "thinking", thinking: lastThinkingContent });
|
|
288
|
+
}
|
|
289
|
+
if (lastTextContent) {
|
|
290
|
+
finalContent.push({ type: "text", text: lastTextContent });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Fallback: use the SDK's result text if we have no content
|
|
294
|
+
if (finalContent.length === 0 && result.subtype === "success" && result.result) {
|
|
295
|
+
finalContent.push({ type: "text", text: result.result });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const finalMessage: AssistantMessage = {
|
|
299
|
+
role: "assistant",
|
|
300
|
+
content: finalContent,
|
|
301
|
+
api: "anthropic-messages",
|
|
302
|
+
provider: "claude-code",
|
|
303
|
+
model: modelId,
|
|
304
|
+
usage: mapUsage(result.usage, result.total_cost_usd),
|
|
305
|
+
stopReason: result.is_error ? "error" : "stop",
|
|
306
|
+
timestamp: Date.now(),
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (result.is_error) {
|
|
310
|
+
const errText =
|
|
311
|
+
"errors" in result
|
|
312
|
+
? (result as any).errors?.join("; ")
|
|
313
|
+
: result.subtype;
|
|
314
|
+
finalMessage.errorMessage = errText;
|
|
315
|
+
stream.push({ type: "error", reason: "error", error: finalMessage });
|
|
316
|
+
} else {
|
|
317
|
+
stream.push({ type: "done", reason: "stop", message: finalMessage });
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
default:
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Generator exhausted without a result message (unexpected)
|
|
328
|
+
const fallbackContent: AssistantMessage["content"] = [];
|
|
329
|
+
if (lastTextContent) {
|
|
330
|
+
fallbackContent.push({ type: "text", text: lastTextContent });
|
|
331
|
+
}
|
|
332
|
+
if (fallbackContent.length === 0) {
|
|
333
|
+
fallbackContent.push({ type: "text", text: "(Claude Code session ended without a response)" });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const fallback: AssistantMessage = {
|
|
337
|
+
role: "assistant",
|
|
338
|
+
content: fallbackContent,
|
|
339
|
+
api: "anthropic-messages",
|
|
340
|
+
provider: "claude-code",
|
|
341
|
+
model: modelId,
|
|
342
|
+
usage: { ...ZERO_USAGE },
|
|
343
|
+
stopReason: "stop",
|
|
344
|
+
timestamp: Date.now(),
|
|
345
|
+
};
|
|
346
|
+
stream.push({ type: "done", reason: "stop", message: fallback });
|
|
347
|
+
} catch (err) {
|
|
348
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
349
|
+
stream.push({
|
|
350
|
+
type: "error",
|
|
351
|
+
reason: "error",
|
|
352
|
+
error: makeErrorMessage(modelId, errorMsg),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -549,17 +549,17 @@ export async function bootstrapAutoSession(
|
|
|
549
549
|
const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
|
|
550
550
|
const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
|
|
551
551
|
const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
552
|
+
try {
|
|
553
|
+
const { openDatabase: openDb } = await import("./gsd-db.js");
|
|
554
|
+
openDb(gsdDbPath);
|
|
555
|
+
if (hasDecisions || hasRequirements || hasMilestones) {
|
|
555
556
|
const { migrateFromMarkdown } = await import("./md-importer.js");
|
|
556
|
-
openDb(gsdDbPath);
|
|
557
557
|
migrateFromMarkdown(s.basePath);
|
|
558
|
-
} catch (err) {
|
|
559
|
-
process.stderr.write(
|
|
560
|
-
`gsd-migrate: auto-migration failed: ${(err as Error).message}\n`,
|
|
561
|
-
);
|
|
562
558
|
}
|
|
559
|
+
} catch (err) {
|
|
560
|
+
process.stderr.write(
|
|
561
|
+
`gsd-migrate: auto-migration failed: ${(err as Error).message}\n`,
|
|
562
|
+
);
|
|
563
563
|
}
|
|
564
564
|
}
|
|
565
565
|
if (existsSync(gsdDbPath) && !isDbAvailable()) {
|
|
@@ -32,6 +32,6 @@ Then:
|
|
|
32
32
|
11. Do not run git commands — the system commits your changes and handles any merge after this unit succeeds.
|
|
33
33
|
12. Update `.gsd/PROJECT.md` if it exists — refresh current state if needed.
|
|
34
34
|
|
|
35
|
-
**You MUST
|
|
35
|
+
**You MUST call `gsd_complete_slice` with the slice summary and UAT content before finishing. The tool persists to both DB and disk and renders `{{sliceSummaryPath}}` and `{{sliceUatPath}}` automatically.**
|
|
36
36
|
|
|
37
37
|
When done, say: "Slice {{sliceId}} complete."
|
|
@@ -10,10 +10,10 @@ Plan milestone {{milestoneId}} ("{{milestoneTitle}}"). Read `.gsd/DECISIONS.md`
|
|
|
10
10
|
## Planning Doctrine
|
|
11
11
|
|
|
12
12
|
- **Risk-first means proof-first.** The earliest slices should prove the hardest thing works by shipping the real feature through the uncertain path. If auth is the risk, the first slice ships a real login page with real session handling that a user can actually use — not a CLI command that returns "authenticated: true". Proof is the shipped feature working. There is no separate "proof" artifact. Do not plan spikes, proof-of-concept slices, or validation-only slices — the proof is the real feature, built through the risky path.
|
|
13
|
-
- **Every slice is vertical, demoable, and shippable.** Every slice ships real, user-facing functionality. "Demoable" means
|
|
13
|
+
- **Every slice is vertical, demoable, and shippable.** Every slice ships real, user-facing functionality. "Demoable" means the intended user can exercise the capability through its real interface — for a web app that's the UI, for a CLI tool that's the terminal, for an API that's a consuming client or curl. The test is: can someone *use* it, not just *assert* it passes. A slice that only proves something but doesn't ship real working code is not a slice — restructure it.
|
|
14
14
|
- **Brownfield bias.** When planning against an existing codebase, ground slices in existing modules, conventions, and seams. Prefer extending real patterns over inventing new ones.
|
|
15
15
|
- **Each slice should establish something downstream slices can depend on.** Think about what stable surface this slice creates for later work — an API, a data shape, a proven integration path.
|
|
16
|
-
- **Avoid foundation-only slices.** If a slice doesn't produce something demoable end-to-end, it's probably a layer, not a vertical slice. Restructure it.
|
|
16
|
+
- **Avoid foundation-only slices.** If a slice doesn't produce something demoable end-to-end, it's probably a layer, not a vertical slice. Restructure it. Exception: if the infrastructure *is* the product surface (a new protocol, extension API, or provider interface), the slice is vertical by definition — the downstream consumer is the demo.
|
|
17
17
|
- **Verification-first.** When planning slices, know what "done" looks like before detailing implementation. Each slice's demo line should describe concrete, verifiable evidence — not vague "it works" claims.
|
|
18
18
|
- **Plan for integrated reality, not just local proof.** Distinguish contract proof from live integration proof. If the milestone involves multiple runtime boundaries, one slice must explicitly prove the assembled system through the real entrypoint or runtime path.
|
|
19
19
|
- **Truthful demo lines only.** If a slice is proven by fixtures or tests only, say so. Do not phrase harness-level proof as if the user can already perform the live end-to-end behavior unless that has actually been exercised.
|
|
@@ -64,10 +64,10 @@ Then:
|
|
|
64
64
|
Apply these when decomposing and ordering slices:
|
|
65
65
|
|
|
66
66
|
- **Risk-first means proof-first.** The earliest slices should prove the hardest thing works by shipping the real feature through the uncertain path. If auth is the risk, the first slice ships a real login page with real session handling that a user can actually use — not a CLI command that returns "authenticated: true". Proof is the shipped feature working. There is no separate "proof" artifact. Do not plan spikes, proof-of-concept slices, or validation-only slices — the proof is the real feature, built through the risky path.
|
|
67
|
-
- **Every slice is vertical, demoable, and shippable.** Every slice ships real, user-facing functionality. "Demoable" means
|
|
67
|
+
- **Every slice is vertical, demoable, and shippable.** Every slice ships real, user-facing functionality. "Demoable" means the intended user can exercise the capability through its real interface — for a web app that's the UI, for a CLI tool that's the terminal, for an API that's a consuming client or curl. The test is: can someone *use* it, not just *assert* it passes. A slice that only proves something but doesn't ship real working code is not a slice — restructure it.
|
|
68
68
|
- **Brownfield bias.** When planning against an existing codebase, ground slices in existing modules, conventions, and seams. Prefer extending real patterns over inventing new ones.
|
|
69
69
|
- **Each slice should establish something downstream slices can depend on.** Think about what stable surface this slice creates for later work — an API, a data shape, a proven integration path.
|
|
70
|
-
- **Avoid foundation-only slices.** If a slice doesn't produce something demoable end-to-end, it's probably a layer, not a vertical slice. Restructure it.
|
|
70
|
+
- **Avoid foundation-only slices.** If a slice doesn't produce something demoable end-to-end, it's probably a layer, not a vertical slice. Restructure it. Exception: if the infrastructure *is* the product surface (a new protocol, extension API, or provider interface), the slice is vertical by definition — the downstream consumer is the demo.
|
|
71
71
|
- **Verification-first.** When planning slices, know what "done" looks like before detailing implementation. Each slice's demo line should describe concrete, verifiable evidence — not vague "it works" claims.
|
|
72
72
|
- **Plan for integrated reality, not just local proof.** Distinguish contract proof from live integration proof. If the milestone involves multiple runtime boundaries, one slice must explicitly prove the assembled system through the real entrypoint or runtime path.
|
|
73
73
|
- **Truthful demo lines only.** If a slice is proven by fixtures or tests only, say so. Do not phrase harness-level proof as if the user can already perform the live end-to-end behavior unless that has actually been exercised.
|
|
@@ -77,6 +77,6 @@ Then:
|
|
|
77
77
|
|
|
78
78
|
The slice directory and tasks/ subdirectory already exist. Do NOT mkdir. All work stays in your working directory: `{{workingDirectory}}`.
|
|
79
79
|
|
|
80
|
-
**You MUST
|
|
80
|
+
**You MUST call `gsd_plan_slice` to persist the planning state before finishing.**
|
|
81
81
|
|
|
82
82
|
When done, say: "Slice {{sliceId}} planned."
|
|
@@ -28,7 +28,7 @@ Then research the codebase and relevant technologies. Narrate key findings and s
|
|
|
28
28
|
5. **Web search budget:** You have a limited budget of web searches (max ~15 per session). Use them strategically — prefer `resolve_library` / `get_library_docs` for library documentation. Do NOT repeat the same or similar queries. If a search didn't find what you need, rephrase once or move on. Target 3-5 total web searches for a typical research unit.
|
|
29
29
|
6. Use the **Research** output template from the inlined context above — include only sections that have real content
|
|
30
30
|
7. If `.gsd/REQUIREMENTS.md` exists, research against it. Identify which Active requirements are table stakes, likely omissions, overbuilt risks, or domain-standard behaviors the user may or may not want.
|
|
31
|
-
8.
|
|
31
|
+
8. Call `gsd_summary_save` with `milestone_id: {{milestoneId}}`, `artifact_type: "RESEARCH"`, and the full research markdown as `content` — the tool computes the file path and persists to both DB and disk.
|
|
32
32
|
|
|
33
33
|
## Strategic Questions to Answer
|
|
34
34
|
|
|
@@ -42,6 +42,6 @@ Then research the codebase and relevant technologies. Narrate key findings and s
|
|
|
42
42
|
|
|
43
43
|
**Research is advisory, not auto-binding.** Surface candidate requirements clearly instead of silently expanding scope.
|
|
44
44
|
|
|
45
|
-
**You MUST
|
|
45
|
+
**You MUST call `gsd_summary_save` with the research content before finishing.**
|
|
46
46
|
|
|
47
47
|
When done, say: "Milestone {{milestoneId}} researched."
|
|
@@ -55,7 +55,7 @@ After running all checks, compute the **overall verdict**:
|
|
|
55
55
|
- `FAIL` — one or more checks failed
|
|
56
56
|
- `PARTIAL` — some checks passed, but one or more checks were skipped, inconclusive, or still require human judgment
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
Call `gsd_summary_save` with `milestone_id: {{milestoneId}}`, `slice_id: {{sliceId}}`, `artifact_type: "ASSESSMENT"`, and the full UAT result markdown as `content` — the tool computes the file path and persists to both DB and disk. The content should follow this format:
|
|
59
59
|
|
|
60
60
|
```markdown
|
|
61
61
|
---
|
|
@@ -84,6 +84,6 @@ date: <ISO 8601 timestamp>
|
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
|
87
|
-
**You MUST
|
|
87
|
+
**You MUST call `gsd_summary_save` with the UAT result content before finishing.**
|
|
88
88
|
|
|
89
89
|
When done, say: "UAT {{sliceId}} complete."
|
|
@@ -136,9 +136,10 @@ describe('ensure-db-open', () => {
|
|
|
136
136
|
// ensureDbOpen returns false for empty .gsd/ (no Markdown, no DB)
|
|
137
137
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
138
138
|
|
|
139
|
-
test('ensureDbOpen: empty .gsd/
|
|
139
|
+
test('ensureDbOpen: empty .gsd/ creates empty DB (#2510)', async () => {
|
|
140
140
|
const tmpDir = makeTmpDir();
|
|
141
|
-
|
|
141
|
+
const gsdDir = path.join(tmpDir, '.gsd');
|
|
142
|
+
fs.mkdirSync(gsdDir, { recursive: true });
|
|
142
143
|
// .gsd/ exists but no DECISIONS.md, REQUIREMENTS.md, or milestones/
|
|
143
144
|
|
|
144
145
|
try { closeDatabase(); } catch { /* ok */ }
|
|
@@ -148,9 +149,12 @@ describe('ensure-db-open', () => {
|
|
|
148
149
|
try {
|
|
149
150
|
const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
|
|
150
151
|
const result = await ensureDbOpen();
|
|
151
|
-
assert.ok(result ===
|
|
152
|
+
assert.ok(result === true, 'ensureDbOpen should create empty DB for fresh .gsd/');
|
|
153
|
+
assert.ok(fs.existsSync(path.join(gsdDir, 'gsd.db')), 'DB file should be created');
|
|
154
|
+
assert.ok(isDbAvailable(), 'DB should be available');
|
|
152
155
|
} finally {
|
|
153
156
|
process.cwd = origCwd;
|
|
157
|
+
closeDatabase();
|
|
154
158
|
cleanupDir(tmpDir);
|
|
155
159
|
}
|
|
156
160
|
});
|
|
@@ -61,6 +61,18 @@ test("plan-slice prompt: DB-backed tool names survive template substitution", ()
|
|
|
61
61
|
assert.ok(result.includes("canonical write path"), "canonical write path language should survive substitution");
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test("plan-slice prompt: footer references gsd_plan_slice tool, not direct write", () => {
|
|
65
|
+
const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit." });
|
|
66
|
+
assert.ok(
|
|
67
|
+
result.includes("MUST call `gsd_plan_slice`"),
|
|
68
|
+
"footer should instruct calling gsd_plan_slice tool",
|
|
69
|
+
);
|
|
70
|
+
assert.ok(
|
|
71
|
+
!result.includes("MUST write the file"),
|
|
72
|
+
"footer should not instruct direct file write",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
64
76
|
test("domain-work prompts use skillActivation placeholder", () => {
|
|
65
77
|
const prompts = [
|
|
66
78
|
"research-milestone",
|
|
@@ -174,6 +186,34 @@ test("research-milestone prompt substitutes skillActivation", () => {
|
|
|
174
186
|
assert.ok(!result.includes("{{skillActivation}}"));
|
|
175
187
|
});
|
|
176
188
|
|
|
189
|
+
test("research-milestone prompt references gsd_summary_save, not direct write", () => {
|
|
190
|
+
const result = loadPrompt("research-milestone", {
|
|
191
|
+
workingDirectory: "/tmp/test-project",
|
|
192
|
+
milestoneId: "M001",
|
|
193
|
+
milestoneTitle: "Test Milestone",
|
|
194
|
+
milestonePath: ".gsd/milestones/M001",
|
|
195
|
+
contextPath: ".gsd/milestones/M001/M001-CONTEXT.md",
|
|
196
|
+
outputPath: "/tmp/test-project/.gsd/milestones/M001/M001-RESEARCH.md",
|
|
197
|
+
inlinedContext: "Context",
|
|
198
|
+
skillDiscoveryMode: "manual",
|
|
199
|
+
skillDiscoveryInstructions: " Discover skills manually.",
|
|
200
|
+
skillActivation: "Load research skills first.",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
assert.ok(
|
|
204
|
+
result.includes("gsd_summary_save"),
|
|
205
|
+
"research-milestone should reference gsd_summary_save tool",
|
|
206
|
+
);
|
|
207
|
+
assert.ok(
|
|
208
|
+
result.includes('artifact_type: "RESEARCH"'),
|
|
209
|
+
"research-milestone should specify RESEARCH artifact type",
|
|
210
|
+
);
|
|
211
|
+
assert.ok(
|
|
212
|
+
!result.includes("MUST write the file"),
|
|
213
|
+
"research-milestone should not instruct direct file write",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
177
217
|
test("research-slice prompt substitutes skillActivation", () => {
|
|
178
218
|
const result = loadPrompt("research-slice", {
|
|
179
219
|
workingDirectory: "/tmp/test-project",
|
|
@@ -640,3 +640,87 @@ test("DiscordAdapter source-level: sendPrompt sets threadUrl in ref", () => {
|
|
|
640
640
|
"sendPrompt should set threadUrl to the constructed message URL",
|
|
641
641
|
);
|
|
642
642
|
});
|
|
643
|
+
|
|
644
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
645
|
+
// Auth.json Token Hydration Tests
|
|
646
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
647
|
+
|
|
648
|
+
test("config source-level: hydrateRemoteTokensFromAuth is called before env check in resolveRemoteConfig", () => {
|
|
649
|
+
const configSrc = readFileSync(
|
|
650
|
+
join(__dirname, "..", "..", "remote-questions", "config.ts"),
|
|
651
|
+
"utf-8",
|
|
652
|
+
);
|
|
653
|
+
// Find the body of resolveRemoteConfig by slicing from its declaration to the next export function.
|
|
654
|
+
const resolveStart = configSrc.indexOf("export function resolveRemoteConfig()");
|
|
655
|
+
const resolveEnd = configSrc.indexOf("\nexport function", resolveStart + 1);
|
|
656
|
+
const resolveFnBody = configSrc.slice(resolveStart, resolveEnd);
|
|
657
|
+
|
|
658
|
+
const hydrationIdx = resolveFnBody.indexOf("hydrateRemoteTokensFromAuth()");
|
|
659
|
+
const envCheckIdx = resolveFnBody.indexOf("process.env[ENV_KEYS[");
|
|
660
|
+
assert.ok(hydrationIdx !== -1, "hydrateRemoteTokensFromAuth() should be called inside resolveRemoteConfig");
|
|
661
|
+
assert.ok(envCheckIdx !== -1, "process.env[ENV_KEYS[ lookup should exist inside resolveRemoteConfig");
|
|
662
|
+
assert.ok(hydrationIdx < envCheckIdx, "hydration call should appear before the process.env env-key lookup");
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("config source-level: hydrateRemoteTokensFromAuth is called in getRemoteConfigStatus", () => {
|
|
666
|
+
const configSrc = readFileSync(
|
|
667
|
+
join(__dirname, "..", "..", "remote-questions", "config.ts"),
|
|
668
|
+
"utf-8",
|
|
669
|
+
);
|
|
670
|
+
const statusFnIdx = configSrc.indexOf("export function getRemoteConfigStatus()");
|
|
671
|
+
const hydrationInStatus = configSrc.indexOf("hydrateRemoteTokensFromAuth()", statusFnIdx);
|
|
672
|
+
assert.ok(hydrationInStatus > statusFnIdx, "hydrateRemoteTokensFromAuth should be called inside getRemoteConfigStatus");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("config source-level: AUTH_PROVIDER_ENV_MAP covers all three remote channels", () => {
|
|
676
|
+
const configSrc = readFileSync(
|
|
677
|
+
join(__dirname, "..", "..", "remote-questions", "config.ts"),
|
|
678
|
+
"utf-8",
|
|
679
|
+
);
|
|
680
|
+
assert.ok(configSrc.includes("discord_bot"), "AUTH_PROVIDER_ENV_MAP should include discord_bot");
|
|
681
|
+
assert.ok(configSrc.includes("slack_bot"), "AUTH_PROVIDER_ENV_MAP should include slack_bot");
|
|
682
|
+
assert.ok(configSrc.includes("telegram_bot"), "AUTH_PROVIDER_ENV_MAP should include telegram_bot");
|
|
683
|
+
assert.ok(configSrc.includes("DISCORD_BOT_TOKEN"), "should map discord_bot to DISCORD_BOT_TOKEN");
|
|
684
|
+
assert.ok(configSrc.includes("SLACK_BOT_TOKEN"), "should map slack_bot to SLACK_BOT_TOKEN");
|
|
685
|
+
assert.ok(configSrc.includes("TELEGRAM_BOT_TOKEN"), "should map telegram_bot to TELEGRAM_BOT_TOKEN");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("config source-level: hydration skips env vars already set", () => {
|
|
689
|
+
const configSrc = readFileSync(
|
|
690
|
+
join(__dirname, "..", "..", "remote-questions", "config.ts"),
|
|
691
|
+
"utf-8",
|
|
692
|
+
);
|
|
693
|
+
// The guard that skips already-set vars must be present.
|
|
694
|
+
assert.ok(
|
|
695
|
+
configSrc.includes("!process.env[envVar]"),
|
|
696
|
+
"hydrateRemoteTokensFromAuth should skip env vars that are already populated",
|
|
697
|
+
);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test("resolveRemoteConfig returns null when preferences are absent (no env side-effects)", () => {
|
|
701
|
+
// Guard: ensure that with no prefs configured, resolveRemoteConfig returns null cleanly.
|
|
702
|
+
// This exercises the hydration path without auth.json present (it should no-op silently).
|
|
703
|
+
const savedHome = process.env.HOME;
|
|
704
|
+
const savedUserProfile = process.env.USERPROFILE;
|
|
705
|
+
const savedDiscord = process.env.DISCORD_BOT_TOKEN;
|
|
706
|
+
const savedSlack = process.env.SLACK_BOT_TOKEN;
|
|
707
|
+
const savedTelegram = process.env.TELEGRAM_BOT_TOKEN;
|
|
708
|
+
try {
|
|
709
|
+
// Point HOME to a nonexistent dir so auth.json lookup finds nothing.
|
|
710
|
+
process.env.HOME = "/tmp/gsd-no-such-home-for-test";
|
|
711
|
+
process.env.USERPROFILE = "/tmp/gsd-no-such-home-for-test";
|
|
712
|
+
delete process.env.DISCORD_BOT_TOKEN;
|
|
713
|
+
delete process.env.SLACK_BOT_TOKEN;
|
|
714
|
+
delete process.env.TELEGRAM_BOT_TOKEN;
|
|
715
|
+
|
|
716
|
+
const result = resolveRemoteConfig();
|
|
717
|
+
// With no prefs file, result is null — not an exception.
|
|
718
|
+
assert.equal(result, null, "resolveRemoteConfig should return null when no preferences are configured");
|
|
719
|
+
} finally {
|
|
720
|
+
process.env.HOME = savedHome;
|
|
721
|
+
process.env.USERPROFILE = savedUserProfile;
|
|
722
|
+
if (savedDiscord !== undefined) process.env.DISCORD_BOT_TOKEN = savedDiscord;
|
|
723
|
+
if (savedSlack !== undefined) process.env.SLACK_BOT_TOKEN = savedSlack;
|
|
724
|
+
if (savedTelegram !== undefined) process.env.TELEGRAM_BOT_TOKEN = savedTelegram;
|
|
725
|
+
}
|
|
726
|
+
});
|