gsd-pi 2.75.0-dev.a44b82572 → 2.75.0-dev.e41b70b10
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/extensions/gsd/auto/phases.js +2 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +22 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +8 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -11
- package/dist/resources/extensions/gsd/auto-model-selection.js +3 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +19 -9
- package/dist/resources/extensions/gsd/auto-worktree.js +16 -1
- package/dist/resources/extensions/gsd/doctor-git-checks.js +22 -2
- package/dist/resources/extensions/gsd/pre-execution-checks.js +12 -8
- package/dist/resources/extensions/gsd/prompts/add-tests.md +1 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +14 -0
- package/dist/resources/extensions/search-the-web/command-search-provider.js +4 -1
- package/dist/resources/extensions/search-the-web/native-search.js +13 -2
- 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 +15 -15
- 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 +15 -15
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- 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/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +102 -65
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +255 -0
- package/packages/mcp-server/src/workflow-tools.ts +108 -65
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/index.d.ts +1 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/providers/api-family.d.ts +27 -0
- package/packages/pi-ai/dist/providers/api-family.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/api-family.js +47 -0
- package/packages/pi-ai/dist/providers/api-family.js.map +1 -0
- package/packages/pi-ai/dist/providers/api-family.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/api-family.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/api-family.test.js +101 -0
- package/packages/pi-ai/dist/providers/api-family.test.js.map +1 -0
- package/packages/pi-ai/src/index.ts +1 -0
- package/packages/pi-ai/src/providers/api-family.test.ts +129 -0
- package/packages/pi-ai/src/providers/api-family.ts +57 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +4 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +4 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -2
- package/packages/pi-coding-agent/src/core/retry-handler.ts +4 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -10
- package/src/resources/extensions/gsd/auto/phases.ts +3 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +25 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +15 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +21 -7
- package/src/resources/extensions/gsd/auto-model-selection.ts +3 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +33 -9
- package/src/resources/extensions/gsd/auto-worktree.ts +16 -1
- package/src/resources/extensions/gsd/doctor-git-checks.ts +23 -2
- package/src/resources/extensions/gsd/pre-execution-checks.ts +12 -8
- package/src/resources/extensions/gsd/prompts/add-tests.md +1 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/integration/doctor-git-symlink-cwd.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +132 -8
- package/src/resources/extensions/gsd/tests/prompts-no-gitignored-test-refs.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +97 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +14 -0
- package/src/resources/extensions/search-the-web/command-search-provider.ts +4 -1
- package/src/resources/extensions/search-the-web/native-search.ts +13 -3
- /package/dist/web/standalone/.next/static/{iBwPQUj73sn8jxegTo320 → By_yegSJ-AA1OP0QjYbSl}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{iBwPQUj73sn8jxegTo320 → By_yegSJ-AA1OP0QjYbSl}/_ssgManifest.js +0 -0
|
@@ -607,6 +607,8 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
607
607
|
prefs,
|
|
608
608
|
session: s,
|
|
609
609
|
structuredQuestionsAvailable,
|
|
610
|
+
sessionContextWindow: ctx.model?.contextWindow,
|
|
611
|
+
modelRegistry: ctx.modelRegistry,
|
|
610
612
|
});
|
|
611
613
|
if (dispatchResult.action === "stop") {
|
|
612
614
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "dispatch-stop", rule: dispatchResult.matchedRule, data: { reason: dispatchResult.reason } });
|
|
@@ -9,6 +9,7 @@ import { getCurrentBranch } from "./worktree.js";
|
|
|
9
9
|
import { getActiveHook } from "./post-unit-hooks.js";
|
|
10
10
|
import { getLedger, getProjectTotals } from "./metrics.js";
|
|
11
11
|
import { getErrorMessage } from "./error-utils.js";
|
|
12
|
+
import { nativeIsRepo } from "./native-git-bridge.js";
|
|
12
13
|
import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
|
|
13
14
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
14
15
|
import { execFileSync } from "node:child_process";
|
|
@@ -256,6 +257,10 @@ let cachedLastCommit = null;
|
|
|
256
257
|
let lastCommitFetchedAt = 0;
|
|
257
258
|
function refreshLastCommit(basePath) {
|
|
258
259
|
try {
|
|
260
|
+
if (!nativeIsRepo(basePath)) {
|
|
261
|
+
cachedLastCommit = null;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
259
264
|
const raw = execFileSync("git", ["log", "-1", "--format=%cr|%s"], {
|
|
260
265
|
cwd: basePath,
|
|
261
266
|
encoding: "utf-8",
|
|
@@ -269,12 +274,15 @@ function refreshLastCommit(basePath) {
|
|
|
269
274
|
message: raw.slice(sep + 1),
|
|
270
275
|
};
|
|
271
276
|
}
|
|
272
|
-
lastCommitFetchedAt = Date.now();
|
|
273
277
|
}
|
|
274
278
|
catch (err) {
|
|
275
279
|
// Non-fatal — just skip last commit display
|
|
280
|
+
cachedLastCommit = null;
|
|
276
281
|
logWarning("dashboard", `operation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
277
282
|
}
|
|
283
|
+
finally {
|
|
284
|
+
lastCommitFetchedAt = Date.now();
|
|
285
|
+
}
|
|
278
286
|
}
|
|
279
287
|
function getLastCommit(basePath) {
|
|
280
288
|
// Refresh at most every 15 seconds
|
|
@@ -283,6 +291,19 @@ function getLastCommit(basePath) {
|
|
|
283
291
|
}
|
|
284
292
|
return cachedLastCommit;
|
|
285
293
|
}
|
|
294
|
+
export function _resetLastCommitCacheForTests() {
|
|
295
|
+
cachedLastCommit = null;
|
|
296
|
+
lastCommitFetchedAt = 0;
|
|
297
|
+
}
|
|
298
|
+
export function _refreshLastCommitForTests(basePath) {
|
|
299
|
+
refreshLastCommit(basePath);
|
|
300
|
+
}
|
|
301
|
+
export function _getLastCommitForTests(basePath) {
|
|
302
|
+
return getLastCommit(basePath);
|
|
303
|
+
}
|
|
304
|
+
export function _getLastCommitFetchedAtForTests() {
|
|
305
|
+
return lastCommitFetchedAt;
|
|
306
|
+
}
|
|
286
307
|
// ─── Footer Factory ───────────────────────────────────────────────────────────
|
|
287
308
|
/**
|
|
288
309
|
* Footer factory used by auto-mode.
|
|
@@ -68,7 +68,10 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
|
|
|
68
68
|
}
|
|
69
69
|
unitType = "plan-slice";
|
|
70
70
|
unitId = `${mid}/${sid}`;
|
|
71
|
-
prompt = await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, base
|
|
71
|
+
prompt = await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, base, undefined, {
|
|
72
|
+
sessionContextWindow: ctx.model?.contextWindow,
|
|
73
|
+
modelRegistry: ctx.modelRegistry,
|
|
74
|
+
});
|
|
72
75
|
}
|
|
73
76
|
else {
|
|
74
77
|
unitType = "plan-milestone";
|
|
@@ -93,7 +96,10 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
|
|
|
93
96
|
}
|
|
94
97
|
unitType = "execute-task";
|
|
95
98
|
unitId = `${mid}/${sid}/${tid}`;
|
|
96
|
-
prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
|
|
99
|
+
prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, {
|
|
100
|
+
sessionContextWindow: ctx.model?.contextWindow,
|
|
101
|
+
modelRegistry: ctx.modelRegistry,
|
|
102
|
+
});
|
|
97
103
|
break;
|
|
98
104
|
}
|
|
99
105
|
case "complete":
|
|
@@ -412,7 +412,7 @@ export const DISPATCH_RULES = [
|
|
|
412
412
|
// auto-heal without either adding an explicit `setSliceSketchFlag(..., false)`
|
|
413
413
|
// call here or doing so inside the plan-slice tool handler.
|
|
414
414
|
name: "refining → refine-slice",
|
|
415
|
-
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
415
|
+
match: async ({ state, mid, midTitle, basePath, prefs, sessionContextWindow, modelRegistry }) => {
|
|
416
416
|
if (state.phase !== "refining")
|
|
417
417
|
return null;
|
|
418
418
|
if (!state.activeSlice)
|
|
@@ -438,20 +438,20 @@ export const DISPATCH_RULES = [
|
|
|
438
438
|
action: "dispatch",
|
|
439
439
|
unitType: "plan-slice",
|
|
440
440
|
unitId: `${mid}/${sid}`,
|
|
441
|
-
prompt: await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, basePath, undefined, softScopeHint ? { softScopeHint } :
|
|
441
|
+
prompt: await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, basePath, undefined, { ...(softScopeHint ? { softScopeHint } : {}), sessionContextWindow, modelRegistry }),
|
|
442
442
|
};
|
|
443
443
|
}
|
|
444
444
|
return {
|
|
445
445
|
action: "dispatch",
|
|
446
446
|
unitType: "refine-slice",
|
|
447
447
|
unitId: `${mid}/${sid}`,
|
|
448
|
-
prompt: await buildRefineSlicePrompt(mid, midTitle, sid, sTitle, basePath),
|
|
448
|
+
prompt: await buildRefineSlicePrompt(mid, midTitle, sid, sTitle, basePath, undefined, { sessionContextWindow, modelRegistry }),
|
|
449
449
|
};
|
|
450
450
|
},
|
|
451
451
|
},
|
|
452
452
|
{
|
|
453
453
|
name: "planning → plan-slice",
|
|
454
|
-
match: async ({ state, mid, midTitle, basePath }) => {
|
|
454
|
+
match: async ({ state, mid, midTitle, basePath, sessionContextWindow, modelRegistry }) => {
|
|
455
455
|
if (state.phase !== "planning")
|
|
456
456
|
return null;
|
|
457
457
|
if (!state.activeSlice)
|
|
@@ -462,7 +462,7 @@ export const DISPATCH_RULES = [
|
|
|
462
462
|
action: "dispatch",
|
|
463
463
|
unitType: "plan-slice",
|
|
464
464
|
unitId: `${mid}/${sid}`,
|
|
465
|
-
prompt: await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, basePath),
|
|
465
|
+
prompt: await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, basePath, undefined, { sessionContextWindow, modelRegistry }),
|
|
466
466
|
};
|
|
467
467
|
},
|
|
468
468
|
},
|
|
@@ -511,7 +511,7 @@ export const DISPATCH_RULES = [
|
|
|
511
511
|
},
|
|
512
512
|
{
|
|
513
513
|
name: "executing → reactive-execute (parallel dispatch)",
|
|
514
|
-
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
514
|
+
match: async ({ state, mid, midTitle, basePath, prefs, sessionContextWindow, modelRegistry }) => {
|
|
515
515
|
if (state.phase !== "executing" || !state.activeTask)
|
|
516
516
|
return null;
|
|
517
517
|
if (!state.activeSlice)
|
|
@@ -574,7 +574,7 @@ export const DISPATCH_RULES = [
|
|
|
574
574
|
action: "dispatch",
|
|
575
575
|
unitType: "reactive-execute",
|
|
576
576
|
unitId: `${mid}/${sid}/reactive+${batchSuffix}`,
|
|
577
|
-
prompt: await buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, selected, basePath, subagentModel),
|
|
577
|
+
prompt: await buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, selected, basePath, subagentModel, { sessionContextWindow, modelRegistry }),
|
|
578
578
|
};
|
|
579
579
|
}
|
|
580
580
|
catch (err) {
|
|
@@ -586,7 +586,7 @@ export const DISPATCH_RULES = [
|
|
|
586
586
|
},
|
|
587
587
|
{
|
|
588
588
|
name: "executing → execute-task (recover missing task plan → plan-slice)",
|
|
589
|
-
match: async ({ state, mid, midTitle, basePath }) => {
|
|
589
|
+
match: async ({ state, mid, midTitle, basePath, sessionContextWindow, modelRegistry }) => {
|
|
590
590
|
if (state.phase !== "executing" || !state.activeTask)
|
|
591
591
|
return null;
|
|
592
592
|
if (!state.activeSlice)
|
|
@@ -605,7 +605,7 @@ export const DISPATCH_RULES = [
|
|
|
605
605
|
action: "dispatch",
|
|
606
606
|
unitType: "plan-slice",
|
|
607
607
|
unitId: `${mid}/${sid}`,
|
|
608
|
-
prompt: await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, basePath),
|
|
608
|
+
prompt: await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, basePath, undefined, { sessionContextWindow, modelRegistry }),
|
|
609
609
|
};
|
|
610
610
|
}
|
|
611
611
|
return null;
|
|
@@ -613,7 +613,7 @@ export const DISPATCH_RULES = [
|
|
|
613
613
|
},
|
|
614
614
|
{
|
|
615
615
|
name: "executing → execute-task",
|
|
616
|
-
match: async ({ state, mid, basePath }) => {
|
|
616
|
+
match: async ({ state, mid, basePath, sessionContextWindow, modelRegistry }) => {
|
|
617
617
|
if (state.phase !== "executing" || !state.activeTask)
|
|
618
618
|
return null;
|
|
619
619
|
if (!state.activeSlice)
|
|
@@ -626,7 +626,7 @@ export const DISPATCH_RULES = [
|
|
|
626
626
|
action: "dispatch",
|
|
627
627
|
unitType: "execute-task",
|
|
628
628
|
unitId: `${mid}/${sid}/${tid}`,
|
|
629
|
-
prompt: await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, basePath),
|
|
629
|
+
prompt: await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, basePath, { sessionContextWindow, modelRegistry }),
|
|
630
630
|
};
|
|
631
631
|
},
|
|
632
632
|
},
|
|
@@ -382,7 +382,9 @@ export function resolveModelId(modelId, availableModels, currentProvider) {
|
|
|
382
382
|
if (providerMatch)
|
|
383
383
|
return providerMatch;
|
|
384
384
|
}
|
|
385
|
-
// Prefer "anthropic" as the canonical provider for Anthropic models
|
|
385
|
+
// Prefer "anthropic" as the canonical provider for Anthropic models.
|
|
386
|
+
// Transport-specific tiebreaker (ADR-012): intentionally keys on provider,
|
|
387
|
+
// not api — we want the plain Anthropic transport when multiple are available.
|
|
386
388
|
const anthropicMatch = candidates.find(m => m.provider === "anthropic");
|
|
387
389
|
if (anthropicMatch)
|
|
388
390
|
return anthropicMatch;
|
|
@@ -75,15 +75,17 @@ function capPreamble(preamble) {
|
|
|
75
75
|
* Uses the budget engine to compute task count ranges and inline context budgets
|
|
76
76
|
* based on the configured executor model's context window.
|
|
77
77
|
*/
|
|
78
|
-
function formatExecutorConstraints() {
|
|
78
|
+
function formatExecutorConstraints(sessionContextWindow, modelRegistry) {
|
|
79
79
|
let windowTokens;
|
|
80
80
|
try {
|
|
81
81
|
const prefs = loadEffectiveGSDPreferences();
|
|
82
|
-
windowTokens = resolveExecutorContextWindow(
|
|
82
|
+
windowTokens = resolveExecutorContextWindow(modelRegistry, prefs?.preferences, sessionContextWindow);
|
|
83
83
|
}
|
|
84
84
|
catch (e) {
|
|
85
85
|
logWarning("prompt", `resolveExecutorContextWindow failed: ${e.message}`);
|
|
86
|
-
|
|
86
|
+
// Delegate to the budget engine without prefs (the path that just threw)
|
|
87
|
+
// so DEFAULT_CONTEXT_WINDOW stays the single source of truth.
|
|
88
|
+
windowTokens = resolveExecutorContextWindow(undefined, undefined, sessionContextWindow);
|
|
87
89
|
}
|
|
88
90
|
const budgets = computeBudgets(windowTokens);
|
|
89
91
|
const { min, max } = budgets.taskCountRange;
|
|
@@ -1136,7 +1138,7 @@ export async function buildResearchSlicePrompt(mid, _midTitle, sid, sTitle, base
|
|
|
1136
1138
|
* sketch-scope constraint).
|
|
1137
1139
|
*/
|
|
1138
1140
|
async function renderSlicePrompt(options) {
|
|
1139
|
-
const { mid, sid, sTitle, base, level, promptTemplate, prependBlocks = [], extraVars = {} } = options;
|
|
1141
|
+
const { mid, sid, sTitle, base, level, promptTemplate, prependBlocks = [], extraVars = {}, sessionContextWindow, modelRegistry, } = options;
|
|
1140
1142
|
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
1141
1143
|
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
1142
1144
|
const researchPath = resolveSliceFile(base, mid, sid, "RESEARCH");
|
|
@@ -1186,7 +1188,7 @@ async function renderSlicePrompt(options) {
|
|
|
1186
1188
|
if (overridesInline)
|
|
1187
1189
|
inlined.unshift(overridesInline);
|
|
1188
1190
|
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
1189
|
-
const executorContextConstraints = formatExecutorConstraints();
|
|
1191
|
+
const executorContextConstraints = formatExecutorConstraints(sessionContextWindow, modelRegistry);
|
|
1190
1192
|
const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
|
|
1191
1193
|
const commitInstruction = "Do not commit — .gsd/ planning docs are managed externally and not tracked in git.";
|
|
1192
1194
|
return loadPrompt(promptTemplate, {
|
|
@@ -1226,6 +1228,8 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
|
|
|
1226
1228
|
level: level ?? resolveInlineLevel(),
|
|
1227
1229
|
promptTemplate: "plan-slice",
|
|
1228
1230
|
prependBlocks,
|
|
1231
|
+
sessionContextWindow: options?.sessionContextWindow,
|
|
1232
|
+
modelRegistry: options?.modelRegistry,
|
|
1229
1233
|
});
|
|
1230
1234
|
}
|
|
1231
1235
|
/**
|
|
@@ -1235,7 +1239,7 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
|
|
|
1235
1239
|
* blank-sheet planning pass. Reuses inlineDependencySummaries for prior
|
|
1236
1240
|
* slice SUMMARY and inlines the stored sketch_scope as a hard constraint.
|
|
1237
1241
|
*/
|
|
1238
|
-
export async function buildRefineSlicePrompt(mid, _midTitle, sid, sTitle, base, level) {
|
|
1242
|
+
export async function buildRefineSlicePrompt(mid, _midTitle, sid, sTitle, base, level, options) {
|
|
1239
1243
|
// Pull the stored sketch scope from the DB — the hard constraint we plan within.
|
|
1240
1244
|
let sketchScope = "";
|
|
1241
1245
|
try {
|
|
@@ -1258,6 +1262,8 @@ export async function buildRefineSlicePrompt(mid, _midTitle, sid, sTitle, base,
|
|
|
1258
1262
|
promptTemplate: "refine-slice",
|
|
1259
1263
|
prependBlocks,
|
|
1260
1264
|
extraVars: { sketchScope },
|
|
1265
|
+
sessionContextWindow: options?.sessionContextWindow,
|
|
1266
|
+
modelRegistry: options?.modelRegistry,
|
|
1261
1267
|
});
|
|
1262
1268
|
}
|
|
1263
1269
|
export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, level) {
|
|
@@ -1323,7 +1329,7 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
|
|
|
1323
1329
|
const overridesSection = formatOverridesSection(activeOverrides);
|
|
1324
1330
|
// Compute verification budget for the executor's context window (issue #707)
|
|
1325
1331
|
const prefs = loadEffectiveGSDPreferences();
|
|
1326
|
-
const contextWindow = resolveExecutorContextWindow(
|
|
1332
|
+
const contextWindow = resolveExecutorContextWindow(opts.modelRegistry, prefs?.preferences, opts.sessionContextWindow);
|
|
1327
1333
|
const budgets = computeBudgets(contextWindow);
|
|
1328
1334
|
const verificationBudget = `~${Math.round(budgets.verificationBudgetChars / 1000)}K chars`;
|
|
1329
1335
|
// Truncate carry-forward section when it exceeds 40% of inline context budget.
|
|
@@ -1858,7 +1864,7 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
|
|
|
1858
1864
|
});
|
|
1859
1865
|
}
|
|
1860
1866
|
// ─── Reactive Execute Prompt ──────────────────────────────────────────────
|
|
1861
|
-
export async function buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, readyTaskIds, base, subagentModel) {
|
|
1867
|
+
export async function buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, readyTaskIds, base, subagentModel, opts) {
|
|
1862
1868
|
const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
|
|
1863
1869
|
// Build graph for context
|
|
1864
1870
|
const taskIO = await loadSliceTaskIO(base, mid, sid);
|
|
@@ -1889,7 +1895,11 @@ export async function buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, rea
|
|
|
1889
1895
|
// Build dependency-scoped carry-forward paths for this task
|
|
1890
1896
|
const depPaths = await getDependencyTaskSummaryPaths(mid, sid, tid, node?.dependsOn ?? [], base);
|
|
1891
1897
|
// Build a full execute-task prompt with dependency-based carry-forward
|
|
1892
|
-
const taskPrompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, {
|
|
1898
|
+
const taskPrompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, {
|
|
1899
|
+
carryForwardPaths: depPaths,
|
|
1900
|
+
sessionContextWindow: opts?.sessionContextWindow,
|
|
1901
|
+
modelRegistry: opts?.modelRegistry,
|
|
1902
|
+
});
|
|
1893
1903
|
const modelSuffix = subagentModel ? ` with model: "${subagentModel}"` : "";
|
|
1894
1904
|
subagentSections.push([
|
|
1895
1905
|
`### ${tid}: ${tTitle}`,
|
|
@@ -180,6 +180,14 @@ function clearProjectRootStateFiles(basePath, milestoneId) {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
+
function isProjectGsdSymlink(basePath) {
|
|
184
|
+
try {
|
|
185
|
+
return lstatSyncFn(join(basePath, ".gsd")).isSymbolicLink();
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
183
191
|
// ─── Build Artifact Auto-Resolve ─────────────────────────────────────────────
|
|
184
192
|
/** Patterns for machine-generated build artifacts that can be safely
|
|
185
193
|
* auto-resolved by accepting --theirs during merge. These files are
|
|
@@ -1440,10 +1448,17 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
|
|
|
1440
1448
|
// CONTEXT files into the stash. If stash pop later fails, those files
|
|
1441
1449
|
// are permanently trapped in the stash entry and lost on the next
|
|
1442
1450
|
// stash push or drop.
|
|
1451
|
+
//
|
|
1452
|
+
// When `.gsd` itself is a symlink, Git rejects pathspecs below it
|
|
1453
|
+
// ("beyond a symbolic link"). In that layout, exclude the whole symlink
|
|
1454
|
+
// and keep stashing real project files that could block the merge.
|
|
1455
|
+
const stashPathspecs = isProjectGsdSymlink(originalBasePath_)
|
|
1456
|
+
? [".", ":(exclude).gsd"]
|
|
1457
|
+
: [":(exclude).gsd/milestones"];
|
|
1443
1458
|
execFileSync("git", [
|
|
1444
1459
|
"stash", "push", "--include-untracked",
|
|
1445
1460
|
"-m", `gsd: pre-merge stash for ${milestoneId}`,
|
|
1446
|
-
"--",
|
|
1461
|
+
"--", ...stashPathspecs,
|
|
1447
1462
|
], { cwd: originalBasePath_, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" });
|
|
1448
1463
|
stashed = true;
|
|
1449
1464
|
}
|
|
@@ -34,6 +34,20 @@ function isDoctorArtifactOnly(dirPath) {
|
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
function normalizePathForComparison(path) {
|
|
38
|
+
const resolved = existsSync(path) ? realpathSync(path) : path;
|
|
39
|
+
const normalized = resolved
|
|
40
|
+
.replaceAll("\\", "/")
|
|
41
|
+
.replace(/^\/\/\?\//, "")
|
|
42
|
+
.replace(/\/+$/, "");
|
|
43
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
44
|
+
}
|
|
45
|
+
function isSameOrNestedPath(candidate, container) {
|
|
46
|
+
const normalizedCandidate = normalizePathForComparison(candidate);
|
|
47
|
+
const normalizedContainer = normalizePathForComparison(container);
|
|
48
|
+
return normalizedCandidate === normalizedContainer ||
|
|
49
|
+
normalizedCandidate.startsWith(`${normalizedContainer}/`);
|
|
50
|
+
}
|
|
37
51
|
export async function checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode = "none") {
|
|
38
52
|
// Degrade gracefully if not a git repo
|
|
39
53
|
if (!nativeIsRepo(basePath)) {
|
|
@@ -84,8 +98,14 @@ export async function checkGitHealth(basePath, issues, fixesApplied, shouldFix,
|
|
|
84
98
|
// pattern in removeWorktree() (#1946). Without this, git cannot
|
|
85
99
|
// remove the worktree and the doctor enters a deadlock where it
|
|
86
100
|
// detects the orphan every run but never cleans it up.
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
let cwd = basePath;
|
|
102
|
+
try {
|
|
103
|
+
cwd = process.cwd();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
cwd = basePath;
|
|
107
|
+
}
|
|
108
|
+
if (isSameOrNestedPath(cwd, wt.path)) {
|
|
89
109
|
try {
|
|
90
110
|
process.chdir(basePath);
|
|
91
111
|
}
|
|
@@ -347,10 +347,13 @@ function getExpectedOutputsUpTo(tasks, taskIndex) {
|
|
|
347
347
|
/**
|
|
348
348
|
* Check that all files referenced in task.inputs either:
|
|
349
349
|
* 1. Exist on disk, OR
|
|
350
|
-
* 2. Are in a prior task's expected_output
|
|
350
|
+
* 2. Are in a prior task's expected_output, OR
|
|
351
|
+
* 3. Are in the current task's own expected_output — the task produces them,
|
|
352
|
+
* so they don't need to pre-exist (#4459, mirroring the exemption #3626
|
|
353
|
+
* introduced for task.files).
|
|
351
354
|
*
|
|
352
|
-
* task.files ("files likely touched") is excluded
|
|
353
|
-
* files the task will create
|
|
355
|
+
* task.files ("files likely touched") is excluded entirely from this check —
|
|
356
|
+
* it intentionally includes files the task will create (#3626).
|
|
354
357
|
*
|
|
355
358
|
* All paths are normalized before comparison to ensure ./src/a.ts matches src/a.ts.
|
|
356
359
|
*/
|
|
@@ -359,6 +362,7 @@ export function checkFilePathConsistency(tasks, basePath) {
|
|
|
359
362
|
for (let i = 0; i < tasks.length; i++) {
|
|
360
363
|
const task = tasks[i];
|
|
361
364
|
const priorOutputs = getExpectedOutputsUpTo(tasks, i);
|
|
365
|
+
const ownOutputs = new Set(task.expected_output.map(normalizeFilePath));
|
|
362
366
|
const filesToCheck = [...task.inputs];
|
|
363
367
|
for (const file of filesToCheck) {
|
|
364
368
|
// Skip empty strings
|
|
@@ -375,21 +379,21 @@ export function checkFilePathConsistency(tasks, basePath) {
|
|
|
375
379
|
const existsOnDisk = existsSync(absolutePath);
|
|
376
380
|
// Check if file is in prior expected outputs (priorOutputs already normalized)
|
|
377
381
|
const inPriorOutputs = priorOutputs.has(normalizedFile);
|
|
382
|
+
const inOwnOutputs = ownOutputs.has(normalizedFile);
|
|
378
383
|
// Directory inputs are satisfied when something produces a file beneath
|
|
379
384
|
// them — either a prior task or the current task itself.
|
|
380
385
|
let directorySatisfied = false;
|
|
381
|
-
if (!existsOnDisk && !inPriorOutputs && isDirectoryReference(file)) {
|
|
382
|
-
const sameTaskOutputs = task.expected_output.map(normalizeFilePath);
|
|
386
|
+
if (!existsOnDisk && !inPriorOutputs && !inOwnOutputs && isDirectoryReference(file)) {
|
|
383
387
|
directorySatisfied =
|
|
384
388
|
anyOutputUnderDirectory(normalizedFile, priorOutputs) ||
|
|
385
|
-
anyOutputUnderDirectory(normalizedFile,
|
|
389
|
+
anyOutputUnderDirectory(normalizedFile, ownOutputs);
|
|
386
390
|
}
|
|
387
|
-
if (!existsOnDisk && !inPriorOutputs && !directorySatisfied) {
|
|
391
|
+
if (!existsOnDisk && !inPriorOutputs && !inOwnOutputs && !directorySatisfied) {
|
|
388
392
|
results.push({
|
|
389
393
|
category: "file",
|
|
390
394
|
target: file,
|
|
391
395
|
passed: false,
|
|
392
|
-
message: `Task ${task.id} references '${file}' which doesn't exist and isn't created by prior
|
|
396
|
+
message: `Task ${task.id} references '${file}' which doesn't exist and isn't created by prior or same-task outputs`,
|
|
393
397
|
blocking: true,
|
|
394
398
|
});
|
|
395
399
|
}
|
|
@@ -31,5 +31,6 @@ You are generating tests for recently completed GSD work.
|
|
|
31
31
|
- Do NOT modify implementation files — only create or update test files
|
|
32
32
|
- Name test files consistently with the project's conventions
|
|
33
33
|
- Keep tests focused and readable
|
|
34
|
+
- Tests must only reference files that are tracked in git. Do NOT import, read, or depend on paths listed in `.gitignore` — in particular GSD-local state such as `.gsd/`, `.planning/`, and `.audits/`. If a test seems to need one of those files, replace it with an inline fixture or a tracked sample; otherwise the test will fail for everyone but the author.
|
|
34
35
|
|
|
35
36
|
{{skillActivation}}
|
|
@@ -36,7 +36,7 @@ Then:
|
|
|
36
36
|
2. Execute the steps in the inlined task plan, adapting minor local mismatches when the surrounding code differs from the planner's snapshot
|
|
37
37
|
3. Before any `Write` that creates an artifact or output file, check whether that path already exists. If it does, read it first and decide whether the work is already done, should be extended, or truly needs replacement. "Create" in the plan does **not** mean the file is missing — a prior session may already have started it.
|
|
38
38
|
4. Build the real thing. If the task plan says "create login endpoint", build an endpoint that actually authenticates against a real store, not one that returns a hardcoded success response. If the task plan says "create dashboard page", build a page that renders real data from the API, not a component with hardcoded props. Stubs and mocks are for tests, not for the shipped feature.
|
|
39
|
-
5. Write or update tests as part of execution — tests are verification, not an afterthought. If the slice plan defines test files in its Verification section and this is the first task, create them (they should initially fail).
|
|
39
|
+
5. Write or update tests as part of execution — tests are verification, not an afterthought. If the slice plan defines test files in its Verification section and this is the first task, create them (they should initially fail). Tests must only reference files tracked in git; never import, read, or assert on paths listed in `.gitignore` (e.g. `.gsd/`, `.planning/`, `.audits/`) — those files are local-only and the test will fail for anyone else. Use inline fixtures or tracked samples instead.
|
|
40
40
|
6. When implementing non-trivial runtime behavior (async flows, API boundaries, background processes, error paths), add or preserve agent-usable observability. Skip this for simple changes where it doesn't apply.
|
|
41
41
|
|
|
42
42
|
**Background process rule:** Never use bare `command &` to run background processes. The shell's `&` operator leaves stdout/stderr attached to the parent, which causes the Bash tool to hang indefinitely waiting for those streams to close. Always redirect output before backgrounding:
|
|
@@ -53,6 +53,7 @@ Then:
|
|
|
53
53
|
- For simple slices: executable commands or script assertions are fine.
|
|
54
54
|
- If the project is non-trivial and has no test framework, the first task should set one up.
|
|
55
55
|
- If this slice establishes a boundary contract, verification must exercise that contract.
|
|
56
|
+
- Planned test files must only read from or import paths that are tracked in git. Do NOT plan tests whose inputs or fixtures are paths listed in `.gitignore` (e.g. `.gsd/`, `.planning/`, `.audits/`). If the scenario seems to require such a file, plan an inline fixture or a tracked sample instead.
|
|
56
57
|
4. **For non-trivial slices only** — plan observability, proof level, and integration closure:
|
|
57
58
|
- Include `Observability / Diagnostics` for backend, integration, async, stateful, or UI slices where failure diagnosis matters.
|
|
58
59
|
- Fill `Proof Level` and `Integration Closure` when the slice crosses runtime boundaries or has meaningful integration concerns.
|
|
@@ -4,6 +4,8 @@ import { loadWriteGateSnapshot, shouldBlockContextArtifactSaveInSnapshot } from
|
|
|
4
4
|
import { getMilestone, getSliceStatusSummary, getSliceTaskCounts, readTransaction, saveGateResult, } from "../gsd-db.js";
|
|
5
5
|
import { GATE_REGISTRY } from "../gate-registry.js";
|
|
6
6
|
import { saveArtifactToDb } from "../db-writer.js";
|
|
7
|
+
import { resolveMilestoneFile, resolveSliceFile } from "../paths.js";
|
|
8
|
+
import { unlinkSync } from "node:fs";
|
|
7
9
|
import { handleCompleteMilestone } from "./complete-milestone.js";
|
|
8
10
|
import { handleCompleteTask } from "./complete-task.js";
|
|
9
11
|
import { handleCompleteSlice } from "./complete-slice.js";
|
|
@@ -61,6 +63,18 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
|
|
|
61
63
|
slice_id: params.slice_id,
|
|
62
64
|
task_id: params.task_id,
|
|
63
65
|
}, basePath);
|
|
66
|
+
if (params.artifact_type === "CONTEXT" && !params.task_id) {
|
|
67
|
+
try {
|
|
68
|
+
const draftFile = params.slice_id
|
|
69
|
+
? resolveSliceFile(basePath, params.milestone_id, params.slice_id, "CONTEXT-DRAFT")
|
|
70
|
+
: resolveMilestoneFile(basePath, params.milestone_id, "CONTEXT-DRAFT");
|
|
71
|
+
if (draftFile)
|
|
72
|
+
unlinkSync(draftFile);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
logWarning("tool", `CONTEXT-DRAFT.md unlink failed: ${e.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
64
78
|
return {
|
|
65
79
|
content: [{ type: "text", text: `Saved ${params.artifact_type} artifact to ${relativePath}` }],
|
|
66
80
|
details: { operation: "save_summary", path: relativePath, artifact_type: params.artifact_type },
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* All provider logic lives in provider.ts (S01) — this is pure UI wiring.
|
|
9
9
|
*/
|
|
10
|
+
import { isAnthropicApi } from '@gsd/pi-ai';
|
|
10
11
|
import { getTavilyApiKey, getBraveApiKey, getOllamaApiKey, getSearchProviderPreference, setSearchProviderPreference, resolveSearchProvider, } from './provider.js';
|
|
11
12
|
const VALID_PREFERENCES = ['tavily', 'brave', 'ollama', 'auto'];
|
|
12
13
|
function keyStatus(provider) {
|
|
@@ -71,7 +72,9 @@ export function registerSearchProviderCommand(pi) {
|
|
|
71
72
|
}
|
|
72
73
|
setSearchProviderPreference(chosen);
|
|
73
74
|
const effective = resolveSearchProvider();
|
|
74
|
-
|
|
75
|
+
// Gate on api (#4478 / ADR-012): covers claude-code, anthropic-vertex, and
|
|
76
|
+
// other Anthropic-fronting transports — not just the plain `anthropic` provider.
|
|
77
|
+
const isAnthropic = isAnthropicApi(ctx.model);
|
|
75
78
|
const nativeNote = isAnthropic ? '\nNote: Native Anthropic web search is also active (automatic, no API key needed).' : '';
|
|
76
79
|
ctx.ui.notify(`Search provider set to ${chosen}. Effective provider: ${effective ?? 'none (no API keys)'}${nativeNote}`, 'info');
|
|
77
80
|
},
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Extracted from index.ts so it can be unit-tested without importing
|
|
5
5
|
* the heavy tool-registration modules.
|
|
6
6
|
*/
|
|
7
|
+
import { isAnthropicApi } from "@gsd/pi-ai";
|
|
7
8
|
import { resolveSearchProviderFromPreferences } from "../gsd/preferences.js";
|
|
8
9
|
/** Tool names for the Brave-backed custom search tools */
|
|
9
10
|
export const BRAVE_TOOL_NAMES = ["search-the-web", "search_and_read"];
|
|
@@ -75,7 +76,10 @@ export function registerNativeSearchHooks(pi) {
|
|
|
75
76
|
pi.on("model_select", async (event, ctx) => {
|
|
76
77
|
modelSelectFired = true;
|
|
77
78
|
const wasAnthropic = isAnthropicProvider;
|
|
78
|
-
|
|
79
|
+
// Gate on `api` not `provider` (#4478 / ADR-012): covers claude-code OAuth,
|
|
80
|
+
// anthropic-vertex, and Vercel-gateway-hosted Anthropic — all serve the
|
|
81
|
+
// Messages API and accept the native web_search tool.
|
|
82
|
+
isAnthropicProvider = isAnthropicApi(event.model);
|
|
79
83
|
const hasBrave = !!process.env.BRAVE_API_KEY;
|
|
80
84
|
// When Anthropic (and not preferring Brave): disable custom search tools —
|
|
81
85
|
// native web_search is server-side and more reliable.
|
|
@@ -116,7 +120,14 @@ export function registerNativeSearchHooks(pi) {
|
|
|
116
120
|
// modelsAreEqual suppresses model_select AND the SDK doesn't pass model.
|
|
117
121
|
const eventModel = event.model;
|
|
118
122
|
let isAnthropic;
|
|
119
|
-
if (eventModel?.
|
|
123
|
+
if (eventModel?.api) {
|
|
124
|
+
// Preferred path: gate on wire protocol (#4478 / ADR-012).
|
|
125
|
+
isAnthropic = isAnthropicApi(eventModel);
|
|
126
|
+
}
|
|
127
|
+
else if (eventModel?.provider) {
|
|
128
|
+
// Fallback for event shapes that carry provider but not api — only plain
|
|
129
|
+
// `anthropic` maps unambiguously without the api field. Other Anthropic
|
|
130
|
+
// transports will arrive via the modelSelectFired or model-name branch.
|
|
120
131
|
isAnthropic = eventModel.provider === "anthropic";
|
|
121
132
|
}
|
|
122
133
|
else if (modelSelectFired) {
|