gsd-pi 2.53.0-dev.a67436f → 2.54.0-dev.16631ca
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/cli.js +19 -19
- package/dist/headless-ui.d.ts +27 -1
- package/dist/headless-ui.js +203 -13
- package/dist/headless.js +60 -3
- package/dist/resources/extensions/bg-shell/bg-shell-lifecycle.js +2 -2
- package/dist/resources/extensions/bg-shell/utilities.js +34 -5
- package/dist/resources/extensions/gsd/auto/phases.js +10 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +17 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +9 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -5
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +22 -22
- 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/react-loadable-manifest.json +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.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/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +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 +22 -22
- package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +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/4024.82f2e2a838908338.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-bca0e732db0dcec3.js → webpack-70adf6e3be5479ce.js} +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +14 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/model-registry.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts +2 -2
- package/src/resources/extensions/bg-shell/utilities.ts +39 -4
- package/src/resources/extensions/gsd/auto/phases.ts +14 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +21 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +15 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -6
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/plan-milestone-queue-context.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/register-extension-guard.test.ts +59 -0
- package/dist/web/standalone/.next/static/chunks/4024.87fd909ae0110f50.js +0 -9
- /package/dist/web/standalone/.next/static/{YO-PWFRitlHM-L-dotlmm → 8yiPxQ52ue_s6qdrrAxsH}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{YO-PWFRitlHM-L-dotlmm → 8yiPxQ52ue_s6qdrrAxsH}/_ssgManifest.js +0 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import type { AuthStorage } from "../../../core/auth-storage.js";
|
|
15
15
|
import { getDiscoverableProviders } from "../../../core/model-discovery.js";
|
|
16
16
|
import type { ModelRegistry } from "../../../core/model-registry.js";
|
|
17
|
+
import { ModelsJsonWriter } from "../../../core/models-json-writer.js";
|
|
17
18
|
import { theme } from "../theme/theme.js";
|
|
18
19
|
import { rawKeyHint } from "./keybinding-hints.js";
|
|
19
20
|
|
|
@@ -39,6 +40,7 @@ export class ProviderManagerComponent extends Container implements Focusable {
|
|
|
39
40
|
private tui: TUI;
|
|
40
41
|
private authStorage: AuthStorage;
|
|
41
42
|
private modelRegistry: ModelRegistry;
|
|
43
|
+
private modelsJsonWriter: ModelsJsonWriter;
|
|
42
44
|
private onDone: () => void;
|
|
43
45
|
private onDiscover: (provider: string) => void;
|
|
44
46
|
|
|
@@ -54,6 +56,7 @@ export class ProviderManagerComponent extends Container implements Focusable {
|
|
|
54
56
|
this.tui = tui;
|
|
55
57
|
this.authStorage = authStorage;
|
|
56
58
|
this.modelRegistry = modelRegistry;
|
|
59
|
+
this.modelsJsonWriter = new ModelsJsonWriter(this.modelRegistry.modelsJsonPath);
|
|
57
60
|
this.onDone = onDone;
|
|
58
61
|
this.onDiscover = onDiscover;
|
|
59
62
|
|
|
@@ -64,7 +67,7 @@ export class ProviderManagerComponent extends Container implements Focusable {
|
|
|
64
67
|
// Hints
|
|
65
68
|
const hints = [
|
|
66
69
|
rawKeyHint("d", "discover"),
|
|
67
|
-
rawKeyHint("r", "remove
|
|
70
|
+
rawKeyHint("r", "remove"),
|
|
68
71
|
rawKeyHint("esc", "close"),
|
|
69
72
|
].join(" ");
|
|
70
73
|
this.addChild(new Text(hints, 0, 0));
|
|
@@ -102,6 +105,15 @@ export class ProviderManagerComponent extends Container implements Focusable {
|
|
|
102
105
|
supportsDiscovery: discoverableSet.has(name),
|
|
103
106
|
modelCount: providerModelCounts.get(name) ?? 0,
|
|
104
107
|
}));
|
|
108
|
+
this.clampSelectedIndex();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private clampSelectedIndex(): void {
|
|
112
|
+
if (this.providers.length === 0) {
|
|
113
|
+
this.selectedIndex = 0;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.selectedIndex = Math.min(this.selectedIndex, this.providers.length - 1);
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
private updateList(): void {
|
|
@@ -152,8 +164,10 @@ export class ProviderManagerComponent extends Container implements Focusable {
|
|
|
152
164
|
}
|
|
153
165
|
} else if (keyData === "r" || keyData === "R") {
|
|
154
166
|
const provider = this.providers[this.selectedIndex];
|
|
155
|
-
if (provider
|
|
167
|
+
if (provider) {
|
|
156
168
|
this.authStorage.remove(provider.name);
|
|
169
|
+
this.modelsJsonWriter.removeProvider(provider.name);
|
|
170
|
+
this.modelRegistry.refresh();
|
|
157
171
|
this.loadProviders();
|
|
158
172
|
this.updateList();
|
|
159
173
|
this.tui.requestRender();
|
package/pkg/package.json
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
loadManifest,
|
|
23
23
|
pruneDeadProcesses,
|
|
24
24
|
} from "./process-manager.js";
|
|
25
|
-
import { formatUptime, resolveBgShellPersistenceCwd } from "./utilities.js";
|
|
25
|
+
import { formatUptime, getBgShellLiveCwd, resolveBgShellPersistenceCwd } from "./utilities.js";
|
|
26
26
|
import { formatTokenCount } from "../shared/format-utils.js";
|
|
27
27
|
|
|
28
28
|
import type { BgShellSharedState } from "./index.js";
|
|
@@ -213,7 +213,7 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS
|
|
|
213
213
|
return {
|
|
214
214
|
render(width: number): string[] {
|
|
215
215
|
// ── Line 1: pwd (branch) [session] ... bg status ──
|
|
216
|
-
let pwd =
|
|
216
|
+
let pwd = getBgShellLiveCwd(state.latestCtx?.cwd);
|
|
217
217
|
const home = process.env.HOME || process.env.USERPROFILE;
|
|
218
218
|
if (home && pwd.startsWith(home)) {
|
|
219
219
|
pwd = `~${pwd.slice(home.length)}`;
|
|
@@ -42,16 +42,51 @@ export function formatTimeAgo(timestamp: number): string {
|
|
|
42
42
|
return formatDuration(Date.now() - timestamp) + " ago";
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function deriveProjectRootFromAutoWorktree(cachedCwd?: string): string | undefined {
|
|
46
|
+
if (!cachedCwd) return undefined;
|
|
47
|
+
const match = cachedCwd.match(/^(.*?)[\\/]\.gsd[\\/]worktrees[\\/][^\\/]+(?:[\\/].*)?$/);
|
|
48
|
+
return match?.[1];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getBgShellLiveCwd(
|
|
52
|
+
cachedCwd?: string,
|
|
53
|
+
pathExists: (path: string) => boolean = existsSync,
|
|
54
|
+
getCwd: () => string = () => process.cwd(),
|
|
55
|
+
chdir: (path: string) => void = (path) => process.chdir(path),
|
|
56
|
+
): string {
|
|
57
|
+
try {
|
|
58
|
+
return getCwd();
|
|
59
|
+
} catch {
|
|
60
|
+
const projectRoot = deriveProjectRootFromAutoWorktree(cachedCwd);
|
|
61
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
62
|
+
const fallbacks = [projectRoot, cachedCwd, home, "/"].filter(
|
|
63
|
+
(candidate): candidate is string => Boolean(candidate),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
for (const candidate of fallbacks) {
|
|
67
|
+
if (candidate !== "/" && !pathExists(candidate)) continue;
|
|
68
|
+
try {
|
|
69
|
+
chdir(candidate);
|
|
70
|
+
} catch {
|
|
71
|
+
// Best-effort only. Returning a known-good fallback is enough to avoid crashes.
|
|
72
|
+
}
|
|
73
|
+
return candidate;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return "/";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
45
79
|
|
|
46
80
|
export function resolveBgShellPersistenceCwd(
|
|
47
81
|
cachedCwd: string,
|
|
48
|
-
liveCwd =
|
|
82
|
+
liveCwd: string | undefined = undefined,
|
|
49
83
|
pathExists: (path: string) => boolean = existsSync,
|
|
50
84
|
): string {
|
|
85
|
+
const resolvedLiveCwd = liveCwd ?? getBgShellLiveCwd(cachedCwd, pathExists);
|
|
51
86
|
const cachedIsAutoWorktree = /(?:^|[\\/])\.gsd[\\/]worktrees[\\/]/.test(cachedCwd);
|
|
52
87
|
if (!cachedIsAutoWorktree) return cachedCwd;
|
|
53
|
-
if (cachedCwd ===
|
|
54
|
-
if (!pathExists(cachedCwd)) return
|
|
55
|
-
if (
|
|
88
|
+
if (cachedCwd === resolvedLiveCwd && pathExists(cachedCwd)) return cachedCwd;
|
|
89
|
+
if (!pathExists(cachedCwd)) return resolvedLiveCwd;
|
|
90
|
+
if (resolvedLiveCwd !== cachedCwd) return resolvedLiveCwd;
|
|
56
91
|
return cachedCwd;
|
|
57
92
|
}
|
|
@@ -45,6 +45,17 @@ export function _resolveReportBasePath(s: Pick<AutoSession, "originalBasePath" |
|
|
|
45
45
|
return s.originalBasePath || s.basePath;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the authoritative project base for dispatch guards.
|
|
50
|
+
* Prior-milestone completion lives at the project root, even when the active
|
|
51
|
+
* unit is running inside an auto worktree.
|
|
52
|
+
*/
|
|
53
|
+
export function _resolveDispatchGuardBasePath(
|
|
54
|
+
s: Pick<AutoSession, "originalBasePath" | "basePath">,
|
|
55
|
+
): string {
|
|
56
|
+
return s.originalBasePath || s.basePath;
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
/**
|
|
49
60
|
* Generate and write an HTML milestone report snapshot.
|
|
50
61
|
* Extracted from the milestone-transition block in autoLoop.
|
|
@@ -667,9 +678,10 @@ export async function runDispatch(
|
|
|
667
678
|
prompt = preDispatchResult.prompt;
|
|
668
679
|
}
|
|
669
680
|
|
|
681
|
+
const guardBasePath = _resolveDispatchGuardBasePath(s);
|
|
670
682
|
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(
|
|
671
|
-
|
|
672
|
-
deps.getMainBranch(
|
|
683
|
+
guardBasePath,
|
|
684
|
+
deps.getMainBranch(guardBasePath),
|
|
673
685
|
unitType,
|
|
674
686
|
unitId,
|
|
675
687
|
);
|
|
@@ -200,7 +200,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
200
200
|
uatContent ?? "",
|
|
201
201
|
basePath,
|
|
202
202
|
),
|
|
203
|
-
pauseAfterDispatch: uatType !== "artifact-driven" && uatType !== "browser-executable" && uatType !== "runtime-executable",
|
|
203
|
+
pauseAfterDispatch: !process.env.GSD_HEADLESS && uatType !== "artifact-driven" && uatType !== "browser-executable" && uatType !== "runtime-executable",
|
|
204
204
|
};
|
|
205
205
|
},
|
|
206
206
|
},
|
|
@@ -18,6 +18,26 @@ export interface ModelSelectionResult {
|
|
|
18
18
|
routing: { tier: string; modelDowngraded: boolean } | null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function resolvePreferredModelConfig(
|
|
22
|
+
unitType: string,
|
|
23
|
+
autoModeStartModel: { provider: string; id: string } | null,
|
|
24
|
+
) {
|
|
25
|
+
const explicitConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
26
|
+
if (explicitConfig) return explicitConfig;
|
|
27
|
+
|
|
28
|
+
const routingConfig = resolveDynamicRoutingConfig();
|
|
29
|
+
if (!routingConfig.enabled || !routingConfig.tier_models) return undefined;
|
|
30
|
+
|
|
31
|
+
const ceilingModel = routingConfig.tier_models.heavy
|
|
32
|
+
?? (autoModeStartModel ? `${autoModeStartModel.provider}/${autoModeStartModel.id}` : undefined);
|
|
33
|
+
if (!ceilingModel) return undefined;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
primary: ceilingModel,
|
|
37
|
+
fallbacks: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
/**
|
|
22
42
|
* Select and apply the appropriate model for a unit dispatch.
|
|
23
43
|
* Handles: per-unit-type model preferences, dynamic complexity routing,
|
|
@@ -36,7 +56,7 @@ export async function selectAndApplyModel(
|
|
|
36
56
|
autoModeStartModel: { provider: string; id: string } | null,
|
|
37
57
|
retryContext?: { isRetry: boolean; previousTier?: string },
|
|
38
58
|
): Promise<ModelSelectionResult> {
|
|
39
|
-
const modelConfig =
|
|
59
|
+
const modelConfig = resolvePreferredModelConfig(unitType, autoModeStartModel);
|
|
40
60
|
let routing: { tier: string; modelDowngraded: boolean } | null = null;
|
|
41
61
|
|
|
42
62
|
if (modelConfig) {
|
|
@@ -87,6 +87,11 @@ function buildSourceFilePaths(
|
|
|
87
87
|
paths.push(`- **Decisions**: \`${relGsdRootFile("DECISIONS")}\``);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
const queuePath = resolveGsdRootFile(base, "QUEUE");
|
|
91
|
+
if (existsSync(queuePath)) {
|
|
92
|
+
paths.push(`- **Queue**: \`${relGsdRootFile("QUEUE")}\``);
|
|
93
|
+
}
|
|
94
|
+
|
|
90
95
|
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
91
96
|
if (contextPath) {
|
|
92
97
|
paths.push(`- **Milestone Context**: \`${relMilestoneFile(base, mid, "CONTEXT")}\``);
|
|
@@ -915,6 +920,16 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
|
|
|
915
920
|
const decisionsInline = await inlineDecisionsFromDb(base, mid, undefined, inlineLevel);
|
|
916
921
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
917
922
|
}
|
|
923
|
+
const queuePath = resolveGsdRootFile(base, "QUEUE");
|
|
924
|
+
if (existsSync(queuePath)) {
|
|
925
|
+
const queueInline = await inlineFileSmart(
|
|
926
|
+
queuePath,
|
|
927
|
+
relGsdRootFile("QUEUE"),
|
|
928
|
+
"Project Queue",
|
|
929
|
+
`${mid} ${midTitle}`,
|
|
930
|
+
);
|
|
931
|
+
inlined.push(queueInline);
|
|
932
|
+
}
|
|
918
933
|
const knowledgeInlinePM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
919
934
|
if (knowledgeInlinePM) inlined.push(knowledgeInlinePM);
|
|
920
935
|
inlined.push(inlineTemplate("roadmap", "Roadmap"));
|
|
@@ -9,14 +9,28 @@ import { registerJournalTools } from "./journal-tools.js";
|
|
|
9
9
|
import { registerHooks } from "./register-hooks.js";
|
|
10
10
|
import { registerShortcuts } from "./register-shortcuts.js";
|
|
11
11
|
|
|
12
|
+
export function handleRecoverableExtensionProcessError(err: Error): boolean {
|
|
13
|
+
if ((err as NodeJS.ErrnoException).code === "EPIPE") {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
17
|
+
const syscall = (err as NodeJS.ErrnoException).syscall;
|
|
18
|
+
if (syscall?.startsWith("spawn")) {
|
|
19
|
+
process.stderr.write(`[gsd] spawn ENOENT: ${(err as any).path ?? "unknown"} — command not found\n`);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (syscall === "uv_cwd") {
|
|
23
|
+
process.stderr.write(`[gsd] ENOENT (${syscall}): ${err.message}\n`);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
function installEpipeGuard(): void {
|
|
13
31
|
if (!process.listeners("uncaughtException").some((listener) => listener.name === "_gsdEpipeGuard")) {
|
|
14
32
|
const _gsdEpipeGuard = (err: Error): void => {
|
|
15
|
-
if ((err
|
|
16
|
-
process.exit(0);
|
|
17
|
-
}
|
|
18
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT" && (err as any).syscall?.startsWith("spawn")) {
|
|
19
|
-
process.stderr.write(`[gsd] spawn ENOENT: ${(err as any).path ?? "unknown"} — command not found\n`);
|
|
33
|
+
if (handleRecoverableExtensionProcessError(err)) {
|
|
20
34
|
return;
|
|
21
35
|
}
|
|
22
36
|
throw err;
|
|
@@ -45,4 +59,3 @@ export function registerGsdExtension(pi: ExtensionAPI): void {
|
|
|
45
59
|
registerShortcuts(pi);
|
|
46
60
|
registerHooks(pi);
|
|
47
61
|
}
|
|
48
|
-
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import { resolvePreferredModelConfig } from "../auto-model-selection.js";
|
|
8
|
+
|
|
9
|
+
function makeTempDir(prefix: string): string {
|
|
10
|
+
return mkdtempSync(join(tmpdir(), prefix));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("resolvePreferredModelConfig synthesizes heavy routing ceiling when models section is absent", () => {
|
|
14
|
+
const originalCwd = process.cwd();
|
|
15
|
+
const originalGsdHome = process.env.GSD_HOME;
|
|
16
|
+
const tempProject = makeTempDir("gsd-routing-project-");
|
|
17
|
+
const tempGsdHome = makeTempDir("gsd-routing-home-");
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(join(tempProject, ".gsd"), { recursive: true });
|
|
21
|
+
writeFileSync(
|
|
22
|
+
join(tempProject, ".gsd", "PREFERENCES.md"),
|
|
23
|
+
[
|
|
24
|
+
"---",
|
|
25
|
+
"dynamic_routing:",
|
|
26
|
+
" enabled: true",
|
|
27
|
+
" tier_models:",
|
|
28
|
+
" light: claude-haiku-4-5",
|
|
29
|
+
" standard: claude-sonnet-4-6",
|
|
30
|
+
" heavy: claude-opus-4-6",
|
|
31
|
+
"---",
|
|
32
|
+
].join("\n"),
|
|
33
|
+
"utf-8",
|
|
34
|
+
);
|
|
35
|
+
process.env.GSD_HOME = tempGsdHome;
|
|
36
|
+
process.chdir(tempProject);
|
|
37
|
+
|
|
38
|
+
const config = resolvePreferredModelConfig("plan-slice", {
|
|
39
|
+
provider: "anthropic",
|
|
40
|
+
id: "claude-sonnet-4-6",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
assert.deepEqual(config, {
|
|
44
|
+
primary: "claude-opus-4-6",
|
|
45
|
+
fallbacks: [],
|
|
46
|
+
});
|
|
47
|
+
} finally {
|
|
48
|
+
process.chdir(originalCwd);
|
|
49
|
+
if (originalGsdHome === undefined) delete process.env.GSD_HOME;
|
|
50
|
+
else process.env.GSD_HOME = originalGsdHome;
|
|
51
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
52
|
+
rmSync(tempGsdHome, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("resolvePreferredModelConfig falls back to auto start model when heavy tier is absent", () => {
|
|
57
|
+
const originalCwd = process.cwd();
|
|
58
|
+
const originalGsdHome = process.env.GSD_HOME;
|
|
59
|
+
const tempProject = makeTempDir("gsd-routing-project-");
|
|
60
|
+
const tempGsdHome = makeTempDir("gsd-routing-home-");
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
mkdirSync(join(tempProject, ".gsd"), { recursive: true });
|
|
64
|
+
writeFileSync(
|
|
65
|
+
join(tempProject, ".gsd", "PREFERENCES.md"),
|
|
66
|
+
[
|
|
67
|
+
"---",
|
|
68
|
+
"dynamic_routing:",
|
|
69
|
+
" enabled: true",
|
|
70
|
+
" tier_models:",
|
|
71
|
+
" light: claude-haiku-4-5",
|
|
72
|
+
" standard: claude-sonnet-4-6",
|
|
73
|
+
"---",
|
|
74
|
+
].join("\n"),
|
|
75
|
+
"utf-8",
|
|
76
|
+
);
|
|
77
|
+
process.env.GSD_HOME = tempGsdHome;
|
|
78
|
+
process.chdir(tempProject);
|
|
79
|
+
|
|
80
|
+
const config = resolvePreferredModelConfig("execute-task", {
|
|
81
|
+
provider: "openai",
|
|
82
|
+
id: "gpt-5.4",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
assert.deepEqual(config, {
|
|
86
|
+
primary: "openai/gpt-5.4",
|
|
87
|
+
fallbacks: [],
|
|
88
|
+
});
|
|
89
|
+
} finally {
|
|
90
|
+
process.chdir(originalCwd);
|
|
91
|
+
if (originalGsdHome === undefined) delete process.env.GSD_HOME;
|
|
92
|
+
else process.env.GSD_HOME = originalGsdHome;
|
|
93
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
94
|
+
rmSync(tempGsdHome, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("resolvePreferredModelConfig keeps explicit phase models as the ceiling", () => {
|
|
99
|
+
const originalCwd = process.cwd();
|
|
100
|
+
const originalGsdHome = process.env.GSD_HOME;
|
|
101
|
+
const tempProject = makeTempDir("gsd-routing-project-");
|
|
102
|
+
const tempGsdHome = makeTempDir("gsd-routing-home-");
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
mkdirSync(join(tempProject, ".gsd"), { recursive: true });
|
|
106
|
+
writeFileSync(
|
|
107
|
+
join(tempProject, ".gsd", "PREFERENCES.md"),
|
|
108
|
+
[
|
|
109
|
+
"---",
|
|
110
|
+
"models:",
|
|
111
|
+
" planning: claude-sonnet-4-6",
|
|
112
|
+
"dynamic_routing:",
|
|
113
|
+
" enabled: true",
|
|
114
|
+
" tier_models:",
|
|
115
|
+
" heavy: claude-opus-4-6",
|
|
116
|
+
"---",
|
|
117
|
+
].join("\n"),
|
|
118
|
+
"utf-8",
|
|
119
|
+
);
|
|
120
|
+
process.env.GSD_HOME = tempGsdHome;
|
|
121
|
+
process.chdir(tempProject);
|
|
122
|
+
|
|
123
|
+
const config = resolvePreferredModelConfig("plan-slice", {
|
|
124
|
+
provider: "anthropic",
|
|
125
|
+
id: "claude-opus-4-6",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
assert.deepEqual(config, {
|
|
129
|
+
primary: "claude-sonnet-4-6",
|
|
130
|
+
fallbacks: [],
|
|
131
|
+
});
|
|
132
|
+
} finally {
|
|
133
|
+
process.chdir(originalCwd);
|
|
134
|
+
if (originalGsdHome === undefined) delete process.env.GSD_HOME;
|
|
135
|
+
else process.env.GSD_HOME = originalGsdHome;
|
|
136
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
137
|
+
rmSync(tempGsdHome, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
@@ -260,6 +260,61 @@ test("runDispatch emits dispatch-stop when dispatch returns stop action", async
|
|
|
260
260
|
assert.equal(stopEvents[0].flowId, ic.flowId);
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
+
test("runDispatch checks prior-slice completion against the project root in worktree mode", async () => {
|
|
264
|
+
const capture = createEventCapture();
|
|
265
|
+
const guardCalls: Array<{ fn: string; args: unknown[] }> = [];
|
|
266
|
+
const deps = makeMockDeps(capture, {
|
|
267
|
+
getMainBranch: (basePath: string) => {
|
|
268
|
+
guardCalls.push({ fn: "getMainBranch", args: [basePath] });
|
|
269
|
+
return "main";
|
|
270
|
+
},
|
|
271
|
+
getPriorSliceCompletionBlocker: (
|
|
272
|
+
basePath: string,
|
|
273
|
+
mainBranch: string,
|
|
274
|
+
unitType: string,
|
|
275
|
+
unitId: string,
|
|
276
|
+
) => {
|
|
277
|
+
guardCalls.push({
|
|
278
|
+
fn: "getPriorSliceCompletionBlocker",
|
|
279
|
+
args: [basePath, mainBranch, unitType, unitId],
|
|
280
|
+
});
|
|
281
|
+
return null;
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
const ic = makeIC(deps, {
|
|
285
|
+
s: {
|
|
286
|
+
...makeSession(),
|
|
287
|
+
basePath: "/tmp/project/.gsd/worktrees/M029-xoklo9",
|
|
288
|
+
originalBasePath: "/tmp/project",
|
|
289
|
+
} as any,
|
|
290
|
+
});
|
|
291
|
+
const preData: PreDispatchData = {
|
|
292
|
+
state: {
|
|
293
|
+
phase: "executing",
|
|
294
|
+
activeMilestone: { id: "M029-xoklo9", title: "Test", status: "active" },
|
|
295
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
296
|
+
registry: [{ id: "M029-xoklo9", status: "active" }],
|
|
297
|
+
blockers: [],
|
|
298
|
+
} as any,
|
|
299
|
+
mid: "M029-xoklo9",
|
|
300
|
+
midTitle: "Test Milestone",
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const result = await runDispatch(ic, preData, {
|
|
304
|
+
recentUnits: [],
|
|
305
|
+
stuckRecoveryAttempts: 0,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
assert.equal(result.action, "next");
|
|
309
|
+
assert.deepEqual(guardCalls, [
|
|
310
|
+
{ fn: "getMainBranch", args: ["/tmp/project"] },
|
|
311
|
+
{
|
|
312
|
+
fn: "getPriorSliceCompletionBlocker",
|
|
313
|
+
args: ["/tmp/project", "main", "execute-task", "M001/S01/T01"],
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
});
|
|
317
|
+
|
|
263
318
|
test("runUnitPhase emits unit-start and unit-end with causedBy reference", async () => {
|
|
264
319
|
const capture = createEventCapture();
|
|
265
320
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import { buildPlanMilestonePrompt } from "../auto-prompts.ts";
|
|
8
|
+
|
|
9
|
+
function createBase(): string {
|
|
10
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-plan-queue-"));
|
|
11
|
+
mkdirSync(join(base, ".gsd", "milestones", "M010"), { recursive: true });
|
|
12
|
+
return base;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cleanup(base: string): void {
|
|
16
|
+
rmSync(base, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("plan-milestone queue context", () => {
|
|
20
|
+
test("includes queue brief when planning milestone without roadmap context", async () => {
|
|
21
|
+
const base = createBase();
|
|
22
|
+
try {
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(base, ".gsd", "QUEUE.md"),
|
|
25
|
+
[
|
|
26
|
+
"# Queue",
|
|
27
|
+
"",
|
|
28
|
+
"### M010: Analytics Dashboard — Interactivity, Intelligence & Demo Readiness",
|
|
29
|
+
"**Vision:** Ship a polished analytics dashboard with drilldowns and AI assistance.",
|
|
30
|
+
"",
|
|
31
|
+
"## Scope",
|
|
32
|
+
"- Interactivity",
|
|
33
|
+
"- Intelligence",
|
|
34
|
+
"- Demo readiness",
|
|
35
|
+
"",
|
|
36
|
+
].join("\n"),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const prompt = await buildPlanMilestonePrompt("M010", "M010", base);
|
|
40
|
+
|
|
41
|
+
assert.match(prompt, /Source: `\.gsd\/QUEUE\.md`/);
|
|
42
|
+
assert.match(prompt, /Analytics Dashboard — Interactivity, Intelligence & Demo Readiness/);
|
|
43
|
+
assert.match(prompt, /Ship a polished analytics dashboard/);
|
|
44
|
+
} finally {
|
|
45
|
+
cleanup(base);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { handleRecoverableExtensionProcessError } from "../bootstrap/register-extension.ts";
|
|
5
|
+
|
|
6
|
+
test("handleRecoverableExtensionProcessError swallows spawn ENOENT", () => {
|
|
7
|
+
let stderr = "";
|
|
8
|
+
const originalWrite = process.stderr.write.bind(process.stderr);
|
|
9
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
10
|
+
stderr += String(chunk);
|
|
11
|
+
return true;
|
|
12
|
+
}) as typeof process.stderr.write;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const handled = handleRecoverableExtensionProcessError(
|
|
16
|
+
Object.assign(new Error("missing binary"), {
|
|
17
|
+
code: "ENOENT",
|
|
18
|
+
syscall: "spawn npm",
|
|
19
|
+
path: "npm",
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
assert.equal(handled, true);
|
|
23
|
+
assert.match(stderr, /spawn ENOENT: npm/);
|
|
24
|
+
} finally {
|
|
25
|
+
process.stderr.write = originalWrite;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("handleRecoverableExtensionProcessError swallows uv_cwd ENOENT", () => {
|
|
30
|
+
let stderr = "";
|
|
31
|
+
const originalWrite = process.stderr.write.bind(process.stderr);
|
|
32
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
33
|
+
stderr += String(chunk);
|
|
34
|
+
return true;
|
|
35
|
+
}) as typeof process.stderr.write;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const handled = handleRecoverableExtensionProcessError(
|
|
39
|
+
Object.assign(new Error("process.cwd failed"), {
|
|
40
|
+
code: "ENOENT",
|
|
41
|
+
syscall: "uv_cwd",
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
assert.equal(handled, true);
|
|
45
|
+
assert.match(stderr, /ENOENT \(uv_cwd\): process\.cwd failed/);
|
|
46
|
+
} finally {
|
|
47
|
+
process.stderr.write = originalWrite;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("handleRecoverableExtensionProcessError leaves unrelated errors unhandled", () => {
|
|
52
|
+
const handled = handleRecoverableExtensionProcessError(
|
|
53
|
+
Object.assign(new Error("permission denied"), {
|
|
54
|
+
code: "EPERM",
|
|
55
|
+
syscall: "open",
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
assert.equal(handled, false);
|
|
59
|
+
});
|