gsd-pi 2.80.0-dev.488219eb6 → 2.80.0-dev.4edec4620
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +11 -3
- package/dist/resources/extensions/gsd/auto-dashboard.js +18 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -10
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +19 -2
- package/dist/resources/extensions/gsd/guided-flow.js +6 -0
- package/dist/resources/extensions/gsd/health-widget.js +3 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -0
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/types.ts +3 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +12 -1
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +7 -2
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +15 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +11 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +7 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -3
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +41 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +20 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +20 -10
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +19 -2
- package/src/resources/extensions/gsd/guided-flow.ts +6 -0
- package/src/resources/extensions/gsd/health-widget.ts +3 -0
- package/src/resources/extensions/gsd/tests/tui-header-lifecycle.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +40 -1
- /package/dist/web/standalone/.next/static/{G68KoLv5claox_Dyd4pZn → eeMbUJZnoHp7l3SZHeXY9}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{G68KoLv5claox_Dyd4pZn → eeMbUJZnoHp7l3SZHeXY9}/_ssgManifest.js +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD2 - Claude Code CLI provider stream adapter
|
|
1
2
|
/**
|
|
2
3
|
* Stream adapter: bridges the Claude Agent SDK into GSD's streamSimple contract.
|
|
3
4
|
*
|
|
@@ -61,6 +62,11 @@ interface ClaudeCodeStreamOptions extends SimpleStreamOptions {
|
|
|
61
62
|
extensionUIContext?: ExtensionUIContext;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/** Resolve the workspace root for local Claude Code process execution. */
|
|
66
|
+
export function resolveClaudeCodeCwd(options?: SimpleStreamOptions): string {
|
|
67
|
+
return options?.cwd && options.cwd.trim().length > 0 ? options.cwd : process.cwd();
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
/** A single selectable option within an SDK elicitation schema field. */
|
|
65
71
|
interface SdkElicitationRequestOption {
|
|
66
72
|
const?: string;
|
|
@@ -1307,8 +1313,9 @@ export function buildSdkOptions(
|
|
|
1307
1313
|
overrides?: { permissionMode?: "bypassPermissions" | "acceptEdits" | "default" | "plan" },
|
|
1308
1314
|
extraOptions: Record<string, unknown> & { reasoning?: ThinkingLevel } = {},
|
|
1309
1315
|
): Record<string, unknown> {
|
|
1310
|
-
const { reasoning, ...sdkExtraOptions } = extraOptions;
|
|
1311
|
-
const
|
|
1316
|
+
const { reasoning, cwd, ...sdkExtraOptions } = extraOptions;
|
|
1317
|
+
const sdkCwd = typeof cwd === "string" && cwd.trim().length > 0 ? cwd : process.cwd();
|
|
1318
|
+
const mcpServers = buildWorkflowMcpServers(sdkCwd);
|
|
1312
1319
|
const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
|
|
1313
1320
|
// Globally unblock the tools GSD expects Claude Code to run. When the
|
|
1314
1321
|
// workflow MCP server is available, prefer its `ask_user_questions` tool over
|
|
@@ -1349,7 +1356,7 @@ export function buildSdkOptions(
|
|
|
1349
1356
|
model: modelId,
|
|
1350
1357
|
includePartialMessages: true,
|
|
1351
1358
|
persistSession: true,
|
|
1352
|
-
cwd:
|
|
1359
|
+
cwd: sdkCwd,
|
|
1353
1360
|
permissionMode,
|
|
1354
1361
|
allowDangerouslySkipPermissions: permissionMode === "bypassPermissions",
|
|
1355
1362
|
settingSources: ["project"],
|
|
@@ -1661,6 +1668,7 @@ async function pumpSdkMessages(
|
|
|
1661
1668
|
const queryPrompt = buildSdkQueryPrompt(context, prompt);
|
|
1662
1669
|
const permissionMode = await resolveClaudePermissionMode();
|
|
1663
1670
|
const uiContext = (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext;
|
|
1671
|
+
const cwd = resolveClaudeCodeCwd(options);
|
|
1664
1672
|
const canUseToolHandler = createClaudeCodeCanUseToolHandler(uiContext);
|
|
1665
1673
|
// When no UI is available (headless / auto-mode), auto-approve all
|
|
1666
1674
|
// tool requests. This replaces the old bypassPermissions workaround.
|
|
@@ -1672,6 +1680,7 @@ async function pumpSdkMessages(
|
|
|
1672
1680
|
prompt,
|
|
1673
1681
|
{ permissionMode },
|
|
1674
1682
|
{
|
|
1683
|
+
cwd,
|
|
1675
1684
|
reasoning: options?.reasoning,
|
|
1676
1685
|
canUseTool: canUseToolFallback,
|
|
1677
1686
|
...(uiContext
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD2 - Claude Code stream adapter regression tests
|
|
1
2
|
import { describe, test } from "node:test";
|
|
2
3
|
import assert from "node:assert/strict";
|
|
3
4
|
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
buildPromptFromContext,
|
|
16
17
|
buildSdkQueryPrompt,
|
|
17
18
|
buildSdkOptions,
|
|
19
|
+
resolveClaudeCodeCwd,
|
|
18
20
|
createClaudeCodeCanUseToolHandler,
|
|
19
21
|
buildBashPermissionPattern,
|
|
20
22
|
buildBashPermissionPatternOptions,
|
|
@@ -671,6 +673,45 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
671
673
|
assert.equal(options.model, "claude-sonnet-4-20250514");
|
|
672
674
|
});
|
|
673
675
|
|
|
676
|
+
test("buildSdkOptions prefers explicit cwd over process cwd for local SDK execution", () => {
|
|
677
|
+
const explicitCwd = "/tmp/gsd-session-root";
|
|
678
|
+
const options = buildSdkOptions("claude-sonnet-4-20250514", "hello world", undefined, { cwd: explicitCwd });
|
|
679
|
+
assert.equal(options.cwd, explicitCwd);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test("buildSdkOptions uses explicit cwd when auto-detecting workflow MCP launch config", () => {
|
|
683
|
+
const explicitCwd = realpathSync(mkdtempSync(join(tmpdir(), "claude-sdk-cwd-")));
|
|
684
|
+
const restore = setWorkflowMcpEnv({});
|
|
685
|
+
try {
|
|
686
|
+
delete process.env.GSD_WORKFLOW_MCP_COMMAND;
|
|
687
|
+
delete process.env.GSD_WORKFLOW_MCP_NAME;
|
|
688
|
+
delete process.env.GSD_WORKFLOW_MCP_ARGS;
|
|
689
|
+
delete process.env.GSD_WORKFLOW_MCP_ENV;
|
|
690
|
+
delete process.env.GSD_WORKFLOW_MCP_CWD;
|
|
691
|
+
|
|
692
|
+
const distDir = join(explicitCwd, "packages", "mcp-server", "dist");
|
|
693
|
+
mkdirSync(distDir, { recursive: true });
|
|
694
|
+
writeFileSync(join(distDir, "cli.js"), "#!/usr/bin/env node\n");
|
|
695
|
+
|
|
696
|
+
const options = buildSdkOptions("claude-sonnet-4-20250514", "hello world", undefined, { cwd: explicitCwd });
|
|
697
|
+
const mcpServers = options.mcpServers as Record<string, any>;
|
|
698
|
+
assert.equal(mcpServers["gsd-workflow"].cwd, explicitCwd);
|
|
699
|
+
assert.equal(mcpServers["gsd-workflow"].env.GSD_WORKFLOW_PROJECT_ROOT, explicitCwd);
|
|
700
|
+
} finally {
|
|
701
|
+
restore();
|
|
702
|
+
rmSync(explicitCwd, { recursive: true, force: true });
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test("resolveClaudeCodeCwd falls back to process cwd when no stream cwd is provided", () => {
|
|
707
|
+
assert.equal(resolveClaudeCodeCwd(), process.cwd());
|
|
708
|
+
assert.equal(resolveClaudeCodeCwd({ cwd: " " }), process.cwd());
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("resolveClaudeCodeCwd returns stream cwd when provided", () => {
|
|
712
|
+
assert.equal(resolveClaudeCodeCwd({ cwd: "/tmp/current-session" }), "/tmp/current-session");
|
|
713
|
+
});
|
|
714
|
+
|
|
674
715
|
test("buildSdkOptions enables betas for sonnet models", () => {
|
|
675
716
|
const sonnetOpts = buildSdkOptions("claude-sonnet-4-20250514", "test");
|
|
676
717
|
assert.ok(
|
|
@@ -602,6 +602,21 @@ export function updateProgressWidget(
|
|
|
602
602
|
): void {
|
|
603
603
|
if (!ctx.hasUI) return;
|
|
604
604
|
|
|
605
|
+
// Welcome header is a startup-only banner — permanently suppress it once
|
|
606
|
+
// auto-mode activates. The dashboard widget owns all status from here.
|
|
607
|
+
// Note: setHeader(undefined) restores the built-in header (logo +
|
|
608
|
+
// instructions). To actually render zero lines, install an empty header.
|
|
609
|
+
if (typeof ctx.ui?.setHeader === "function") {
|
|
610
|
+
ctx.ui.setHeader(() => ({
|
|
611
|
+
render(): string[] { return []; },
|
|
612
|
+
invalidate(): void {},
|
|
613
|
+
}));
|
|
614
|
+
}
|
|
615
|
+
// Clear wizard step badge — auto-mode owns the UI from this point
|
|
616
|
+
if (typeof ctx.ui?.setStatus === "function") {
|
|
617
|
+
ctx.ui.setStatus("gsd-step", undefined);
|
|
618
|
+
}
|
|
619
|
+
|
|
605
620
|
const verb = unitVerb(unitType);
|
|
606
621
|
const phaseLabel = unitPhaseLabel(unitType);
|
|
607
622
|
const mid = state.activeMilestone;
|
|
@@ -1028,6 +1043,11 @@ export function updateProgressWidget(
|
|
|
1028
1043
|
? lastCommit.message.slice(0, maxCommitLen - 1) + "…"
|
|
1029
1044
|
: lastCommit.message
|
|
1030
1045
|
: "";
|
|
1046
|
+
// Step-mode guidance — shown above keyboard hints when auto is paused
|
|
1047
|
+
if (accessors.isStepMode()) {
|
|
1048
|
+
lines.push(`${pad}${theme.fg("accent", "→")} ${theme.fg("dim", "Ctrl+N to advance to next step · /gsd status for overview")}`);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1031
1051
|
// Hints line
|
|
1032
1052
|
const hintParts: string[] = [];
|
|
1033
1053
|
hintParts.push("esc pause");
|
|
@@ -84,16 +84,26 @@ async function installWelcomeHeader(ctx: ExtensionContext): Promise<void> {
|
|
|
84
84
|
if (rc) remoteChannel = rc.channel;
|
|
85
85
|
} catch { /* non-fatal */ }
|
|
86
86
|
|
|
87
|
-
ctx.ui.setHeader(() =>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
width
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
ctx.ui.setHeader(() => {
|
|
88
|
+
let cachedLines: string[] | undefined;
|
|
89
|
+
let cachedWidth: number | undefined;
|
|
90
|
+
return {
|
|
91
|
+
render(width: number): string[] {
|
|
92
|
+
if (cachedLines !== undefined && cachedWidth === width) return cachedLines;
|
|
93
|
+
cachedLines = welcome.buildWelcomeScreenLines({
|
|
94
|
+
version: process.env.GSD_VERSION || "0.0.0",
|
|
95
|
+
remoteChannel,
|
|
96
|
+
width,
|
|
97
|
+
});
|
|
98
|
+
cachedWidth = width;
|
|
99
|
+
return cachedLines;
|
|
100
|
+
},
|
|
101
|
+
invalidate(): void {
|
|
102
|
+
cachedLines = undefined;
|
|
103
|
+
cachedWidth = undefined;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
});
|
|
97
107
|
} catch {
|
|
98
108
|
/* non-fatal */
|
|
99
109
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// GSD2 - Write gate runtime persistence and policy guards.
|
|
2
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
4
|
|
|
4
5
|
import { minimatch } from "minimatch";
|
|
@@ -147,6 +148,22 @@ function writeGateSnapshotPath(basePath: string): string {
|
|
|
147
148
|
return join(basePath, ".gsd", "runtime", "write-gate-state.json");
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
function ensureWriteGateSnapshotDirectory(basePath: string): void {
|
|
152
|
+
const gsdPath = join(basePath, ".gsd");
|
|
153
|
+
if (!existsSync(gsdPath)) {
|
|
154
|
+
try {
|
|
155
|
+
const stat = lstatSync(gsdPath);
|
|
156
|
+
if (stat.isSymbolicLink()) {
|
|
157
|
+
const target = readlinkSync(gsdPath);
|
|
158
|
+
mkdirSync(isAbsolute(target) ? target : resolve(basePath, target), { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// If .gsd truly does not exist, the runtime mkdir below will create it.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
mkdirSync(join(gsdPath, "runtime"), { recursive: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
150
167
|
function currentWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSnapshot {
|
|
151
168
|
const state = getWriteGateState(basePath);
|
|
152
169
|
return {
|
|
@@ -160,7 +177,7 @@ function currentWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSn
|
|
|
160
177
|
function persistWriteGateSnapshot(basePath: string): void {
|
|
161
178
|
if (!shouldPersistWriteGateSnapshot()) return;
|
|
162
179
|
const path = writeGateSnapshotPath(basePath);
|
|
163
|
-
|
|
180
|
+
ensureWriteGateSnapshotDirectory(basePath);
|
|
164
181
|
const tempPath = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
165
182
|
writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(basePath), null, 2), "utf-8");
|
|
166
183
|
try {
|
|
@@ -2218,6 +2218,7 @@ export async function showSmartEntry(
|
|
|
2218
2218
|
|
|
2219
2219
|
if (isFirst) {
|
|
2220
2220
|
// First ever — skip wizard, just ask directly
|
|
2221
|
+
ctx.ui.setStatus("gsd-step", "New Milestone · answer the questions above to plan");
|
|
2221
2222
|
setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
|
|
2222
2223
|
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
|
2223
2224
|
`New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`,
|
|
@@ -2246,6 +2247,7 @@ export async function showSmartEntry(
|
|
|
2246
2247
|
if (choice === "quick_task") {
|
|
2247
2248
|
await runQuickTaskChoice(ctx, pi);
|
|
2248
2249
|
} else if (choice === "new_milestone") {
|
|
2250
|
+
ctx.ui.setStatus("gsd-step", "New Milestone · answer the questions above to plan");
|
|
2249
2251
|
setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
|
|
2250
2252
|
await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId,
|
|
2251
2253
|
`New milestone ${nextId}.`,
|
|
@@ -2447,6 +2449,7 @@ export async function showSmartEntry(
|
|
|
2447
2449
|
if (choice === "quick_task") {
|
|
2448
2450
|
await runQuickTaskChoice(ctx, pi);
|
|
2449
2451
|
} else if (choice === "plan") {
|
|
2452
|
+
ctx.ui.setStatus("gsd-step", "Planning Milestone · decomposing into slices");
|
|
2450
2453
|
setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
|
|
2451
2454
|
await dispatchWorkflow(
|
|
2452
2455
|
pi,
|
|
@@ -2579,6 +2582,7 @@ export async function showSmartEntry(
|
|
|
2579
2582
|
});
|
|
2580
2583
|
|
|
2581
2584
|
if (choice === "plan") {
|
|
2585
|
+
ctx.ui.setStatus("gsd-step", "Slice Planning · answer the questions above");
|
|
2582
2586
|
await dispatchWorkflow(
|
|
2583
2587
|
pi,
|
|
2584
2588
|
await buildPlanSlicePrompt(milestoneId, milestoneTitle, sliceId, sliceTitle, basePath),
|
|
@@ -2641,6 +2645,7 @@ export async function showSmartEntry(
|
|
|
2641
2645
|
});
|
|
2642
2646
|
|
|
2643
2647
|
if (choice === "complete") {
|
|
2648
|
+
ctx.ui.setStatus("gsd-step", "Completing Slice · review changes above");
|
|
2644
2649
|
await dispatchWorkflow(
|
|
2645
2650
|
pi,
|
|
2646
2651
|
await buildCompleteSlicePrompt(milestoneId, milestoneTitle, sliceId, sliceTitle, basePath),
|
|
@@ -2709,6 +2714,7 @@ export async function showSmartEntry(
|
|
|
2709
2714
|
}
|
|
2710
2715
|
|
|
2711
2716
|
if (choice === "execute") {
|
|
2717
|
+
ctx.ui.setStatus("gsd-step", "Executing Task · follow progress above");
|
|
2712
2718
|
if (hasInterrupted) {
|
|
2713
2719
|
await dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
|
|
2714
2720
|
milestoneId,
|
|
@@ -135,6 +135,9 @@ export function initHealthWidget(ctx: ExtensionContext): void {
|
|
|
135
135
|
render(width: number): string[] {
|
|
136
136
|
if (!cachedLines || cachedWidth !== width) {
|
|
137
137
|
cachedLines = buildHealthLines(data, width);
|
|
138
|
+
if (data.projectState === "active") {
|
|
139
|
+
cachedLines = [...cachedLines, _theme.fg("dim", " /gsd auto to run · /gsd status for overview · /gsd help")];
|
|
140
|
+
}
|
|
138
141
|
cachedWidth = width;
|
|
139
142
|
}
|
|
140
143
|
return cachedLines;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Regression tests for the TUI header lifecycle fixes —
|
|
3
|
+
// header is suppressed (zero lines) when auto-mode activates, the wizard
|
|
4
|
+
// step status badge is cleared, the NEXT-mode footer hint renders when
|
|
5
|
+
// step mode is active, and the health widget appends guidance for active
|
|
6
|
+
// projects.
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
|
|
14
|
+
import { updateProgressWidget } from "../auto-dashboard.ts";
|
|
15
|
+
import type { GSDState } from "../types.ts";
|
|
16
|
+
|
|
17
|
+
interface CapturedSetHeader {
|
|
18
|
+
factory: ((tui: unknown, theme: unknown) => { render(width: number): string[]; invalidate(): void }) | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeTempDir(prefix: string): string {
|
|
22
|
+
return join(
|
|
23
|
+
tmpdir(),
|
|
24
|
+
`gsd-tui-lifecycle-test-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cleanup(dir: string): void {
|
|
29
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const baseState: GSDState = {
|
|
33
|
+
phase: "executing",
|
|
34
|
+
activeMilestone: { id: "M001", title: "Milestone" },
|
|
35
|
+
activeSlice: { id: "S01", title: "Slice" },
|
|
36
|
+
activeTask: { id: "T01", title: "Task" },
|
|
37
|
+
} as unknown as GSDState;
|
|
38
|
+
|
|
39
|
+
const baseAccessors = {
|
|
40
|
+
getAutoStartTime: () => 0,
|
|
41
|
+
isStepMode: () => false,
|
|
42
|
+
getCmdCtx: () => null,
|
|
43
|
+
getBasePath: () => "/tmp",
|
|
44
|
+
isVerbose: () => false,
|
|
45
|
+
isSessionSwitching: () => false,
|
|
46
|
+
getCurrentDispatchedModelId: () => null,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ── Header lifecycle ────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
test("updateProgressWidget installs an EMPTY-rendering header (not undefined) — addresses codex P1 finding that setHeader(undefined) restores the built-in logo+instructions header", (t) => {
|
|
52
|
+
const dir = makeTempDir("empty-header");
|
|
53
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
54
|
+
t.after(() => cleanup(dir));
|
|
55
|
+
|
|
56
|
+
const captured: CapturedSetHeader = { factory: undefined };
|
|
57
|
+
let setHeaderCallCount = 0;
|
|
58
|
+
|
|
59
|
+
updateProgressWidget(
|
|
60
|
+
{
|
|
61
|
+
hasUI: true,
|
|
62
|
+
ui: {
|
|
63
|
+
setWidget() {},
|
|
64
|
+
setHeader(factory: any) {
|
|
65
|
+
setHeaderCallCount++;
|
|
66
|
+
captured.factory = factory;
|
|
67
|
+
},
|
|
68
|
+
setStatus() {},
|
|
69
|
+
},
|
|
70
|
+
} as any,
|
|
71
|
+
"execute-task",
|
|
72
|
+
"M001/S01/T01",
|
|
73
|
+
baseState,
|
|
74
|
+
{ ...baseAccessors, getBasePath: () => dir },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
assert.equal(setHeaderCallCount, 1, "setHeader must be called exactly once when widget installs");
|
|
78
|
+
assert.notEqual(captured.factory, undefined, "factory must NOT be undefined — undefined restores the built-in logo+instructions header (codex P1)");
|
|
79
|
+
assert.equal(typeof captured.factory, "function", "factory must be a component-creating function");
|
|
80
|
+
|
|
81
|
+
const component = captured.factory!(null, null);
|
|
82
|
+
const rendered = component.render(80);
|
|
83
|
+
assert.deepEqual(rendered, [], "empty header component must render zero lines so auto-mode actually suppresses the welcome banner");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("updateProgressWidget clears the gsd-step wizard badge when auto-mode activates", (t) => {
|
|
87
|
+
const dir = makeTempDir("step-badge");
|
|
88
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
89
|
+
t.after(() => cleanup(dir));
|
|
90
|
+
|
|
91
|
+
const statusCalls: Array<[string, string | undefined]> = [];
|
|
92
|
+
|
|
93
|
+
updateProgressWidget(
|
|
94
|
+
{
|
|
95
|
+
hasUI: true,
|
|
96
|
+
ui: {
|
|
97
|
+
setWidget() {},
|
|
98
|
+
setHeader() {},
|
|
99
|
+
setStatus(key: string, value: string | undefined) { statusCalls.push([key, value]); },
|
|
100
|
+
},
|
|
101
|
+
} as any,
|
|
102
|
+
"execute-task",
|
|
103
|
+
"M001/S01/T01",
|
|
104
|
+
baseState,
|
|
105
|
+
{ ...baseAccessors, getBasePath: () => dir },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
assert.ok(
|
|
109
|
+
statusCalls.some(([key, value]) => key === "gsd-step" && value === undefined),
|
|
110
|
+
`expected setStatus("gsd-step", undefined) to be called; got ${JSON.stringify(statusCalls)}`,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("updateProgressWidget gracefully no-ops when ctx.ui lacks setHeader/setStatus (RPC mode)", (t) => {
|
|
115
|
+
const dir = makeTempDir("rpc-mode");
|
|
116
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
117
|
+
t.after(() => cleanup(dir));
|
|
118
|
+
|
|
119
|
+
// ctx.ui without setHeader / setStatus — must not throw.
|
|
120
|
+
assert.doesNotThrow(() => {
|
|
121
|
+
updateProgressWidget(
|
|
122
|
+
{
|
|
123
|
+
hasUI: true,
|
|
124
|
+
ui: { setWidget() {} },
|
|
125
|
+
} as any,
|
|
126
|
+
"execute-task",
|
|
127
|
+
"M001/S01/T01",
|
|
128
|
+
baseState,
|
|
129
|
+
{ ...baseAccessors, getBasePath: () => dir },
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── NEXT-mode footer guidance ───────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
test("auto-dashboard widget render output includes Ctrl+N guidance when isStepMode is true", (t) => {
|
|
137
|
+
const dir = makeTempDir("step-hint");
|
|
138
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
139
|
+
t.after(() => cleanup(dir));
|
|
140
|
+
|
|
141
|
+
let widgetFactory: ((tui: unknown, theme: unknown) => any) | undefined;
|
|
142
|
+
|
|
143
|
+
updateProgressWidget(
|
|
144
|
+
{
|
|
145
|
+
hasUI: true,
|
|
146
|
+
ui: {
|
|
147
|
+
setWidget(_key: string, factory: any) { widgetFactory = factory; },
|
|
148
|
+
setHeader() {},
|
|
149
|
+
setStatus() {},
|
|
150
|
+
},
|
|
151
|
+
} as any,
|
|
152
|
+
"execute-task",
|
|
153
|
+
"M001/S01/T01",
|
|
154
|
+
baseState,
|
|
155
|
+
{ ...baseAccessors, getBasePath: () => dir, isStepMode: () => true },
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
assert.ok(widgetFactory, "widget factory must be installed");
|
|
159
|
+
|
|
160
|
+
const fakeTui = { requestRender() {} };
|
|
161
|
+
const fakeTheme = {
|
|
162
|
+
fg: (_color: string, text: string) => text,
|
|
163
|
+
bold: (text: string) => text,
|
|
164
|
+
};
|
|
165
|
+
const component = widgetFactory!(fakeTui, fakeTheme);
|
|
166
|
+
const lines = component.render(120);
|
|
167
|
+
|
|
168
|
+
const hasStepHint = lines.some((line: string) => line.includes("Ctrl+N to advance"));
|
|
169
|
+
assert.ok(hasStepHint, `expected step-mode hint in render output; got:\n${lines.join("\n")}`);
|
|
170
|
+
|
|
171
|
+
if (component.dispose) component.dispose();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("auto-dashboard widget render output omits Ctrl+N guidance when isStepMode is false", (t) => {
|
|
175
|
+
const dir = makeTempDir("no-step-hint");
|
|
176
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
177
|
+
t.after(() => cleanup(dir));
|
|
178
|
+
|
|
179
|
+
let widgetFactory: ((tui: unknown, theme: unknown) => any) | undefined;
|
|
180
|
+
|
|
181
|
+
updateProgressWidget(
|
|
182
|
+
{
|
|
183
|
+
hasUI: true,
|
|
184
|
+
ui: {
|
|
185
|
+
setWidget(_key: string, factory: any) { widgetFactory = factory; },
|
|
186
|
+
setHeader() {},
|
|
187
|
+
setStatus() {},
|
|
188
|
+
},
|
|
189
|
+
} as any,
|
|
190
|
+
"execute-task",
|
|
191
|
+
"M001/S01/T01",
|
|
192
|
+
baseState,
|
|
193
|
+
{ ...baseAccessors, getBasePath: () => dir, isStepMode: () => false },
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
assert.ok(widgetFactory);
|
|
197
|
+
|
|
198
|
+
const fakeTui = { requestRender() {} };
|
|
199
|
+
const fakeTheme = {
|
|
200
|
+
fg: (_color: string, text: string) => text,
|
|
201
|
+
bold: (text: string) => text,
|
|
202
|
+
};
|
|
203
|
+
const component = widgetFactory!(fakeTui, fakeTheme);
|
|
204
|
+
const lines = component.render(120);
|
|
205
|
+
|
|
206
|
+
const hasStepHint = lines.some((line: string) => line.includes("Ctrl+N to advance"));
|
|
207
|
+
assert.equal(hasStepHint, false, "step-mode hint must NOT appear when isStepMode is false");
|
|
208
|
+
|
|
209
|
+
if (component.dispose) component.dispose();
|
|
210
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD2 - Write gate regression tests.
|
|
1
2
|
/**
|
|
2
3
|
* Unit tests for the CONTEXT.md write-gate (D031 guard chain).
|
|
3
4
|
*
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
|
|
12
13
|
import test, { afterEach } from 'node:test';
|
|
13
14
|
import assert from 'node:assert/strict';
|
|
14
|
-
import { mkdirSync, writeFileSync, unlinkSync, existsSync, rmSync } from 'node:fs';
|
|
15
|
+
import { mkdirSync, writeFileSync, unlinkSync, existsSync, rmSync, symlinkSync } from 'node:fs';
|
|
15
16
|
import { join } from 'node:path';
|
|
16
17
|
import { tmpdir } from 'node:os';
|
|
17
18
|
import { randomUUID } from 'node:crypto';
|
|
@@ -685,3 +686,41 @@ test('write-gate: loadWriteGateSnapshot returns empty default when persist file
|
|
|
685
686
|
} catch { /* swallow */ }
|
|
686
687
|
}
|
|
687
688
|
});
|
|
689
|
+
|
|
690
|
+
// ─── Scenario 30: write-gate persistence recreates dangling external .gsd target ──
|
|
691
|
+
|
|
692
|
+
test('write-gate: resetWriteGateState persists through dangling .gsd symlink', () => {
|
|
693
|
+
const base = join(tmpdir(), `gsd-write-gate-dangling-${randomUUID()}`);
|
|
694
|
+
const externalState = join(tmpdir(), `gsd-write-gate-external-${randomUUID()}`);
|
|
695
|
+
const stateFilePath = join(base, '.gsd', 'runtime', 'write-gate-state.json');
|
|
696
|
+
const originalEnv = process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = '1';
|
|
700
|
+
mkdirSync(base, { recursive: true });
|
|
701
|
+
symlinkSync(externalState, join(base, '.gsd'), 'junction');
|
|
702
|
+
assert.strictEqual(existsSync(join(base, '.gsd')), false, 'precondition: .gsd symlink target is missing');
|
|
703
|
+
|
|
704
|
+
resetWriteGateState(base);
|
|
705
|
+
|
|
706
|
+
assert.ok(existsSync(externalState), 'missing external state target was recreated');
|
|
707
|
+
assert.ok(existsSync(stateFilePath), 'write-gate snapshot persisted under .gsd/runtime');
|
|
708
|
+
assert.deepEqual(loadWriteGateSnapshot(base), {
|
|
709
|
+
verifiedDepthMilestones: [],
|
|
710
|
+
verifiedApprovalGates: [],
|
|
711
|
+
activeQueuePhase: false,
|
|
712
|
+
pendingGateId: null,
|
|
713
|
+
});
|
|
714
|
+
} finally {
|
|
715
|
+
if (originalEnv === undefined) {
|
|
716
|
+
delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
717
|
+
} else {
|
|
718
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
|
|
719
|
+
}
|
|
720
|
+
clearDiscussionFlowState(base);
|
|
721
|
+
try {
|
|
722
|
+
rmSync(base, { recursive: true, force: true });
|
|
723
|
+
rmSync(externalState, { recursive: true, force: true });
|
|
724
|
+
} catch { /* swallow */ }
|
|
725
|
+
}
|
|
726
|
+
});
|
|
File without changes
|
|
File without changes
|