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.
Files changed (111) hide show
  1. package/dist/resources/extensions/gsd/auto/phases.js +2 -0
  2. package/dist/resources/extensions/gsd/auto-dashboard.js +22 -1
  3. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +8 -2
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -11
  5. package/dist/resources/extensions/gsd/auto-model-selection.js +3 -1
  6. package/dist/resources/extensions/gsd/auto-prompts.js +19 -9
  7. package/dist/resources/extensions/gsd/auto-worktree.js +16 -1
  8. package/dist/resources/extensions/gsd/doctor-git-checks.js +22 -2
  9. package/dist/resources/extensions/gsd/pre-execution-checks.js +12 -8
  10. package/dist/resources/extensions/gsd/prompts/add-tests.md +1 -0
  11. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  12. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -0
  13. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +14 -0
  14. package/dist/resources/extensions/search-the-web/command-search-provider.js +4 -1
  15. package/dist/resources/extensions/search-the-web/native-search.js +13 -2
  16. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  17. package/dist/web/standalone/.next/BUILD_ID +1 -1
  18. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  19. package/dist/web/standalone/.next/build-manifest.json +2 -2
  20. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  21. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.html +1 -1
  38. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  45. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  47. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  48. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  49. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  50. package/package.json +1 -1
  51. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  52. package/packages/mcp-server/dist/workflow-tools.js +102 -65
  53. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  54. package/packages/mcp-server/src/workflow-tools.test.ts +255 -0
  55. package/packages/mcp-server/src/workflow-tools.ts +108 -65
  56. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  57. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  58. package/packages/pi-ai/dist/index.d.ts +1 -0
  59. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  60. package/packages/pi-ai/dist/index.js +1 -0
  61. package/packages/pi-ai/dist/index.js.map +1 -1
  62. package/packages/pi-ai/dist/providers/api-family.d.ts +27 -0
  63. package/packages/pi-ai/dist/providers/api-family.d.ts.map +1 -0
  64. package/packages/pi-ai/dist/providers/api-family.js +47 -0
  65. package/packages/pi-ai/dist/providers/api-family.js.map +1 -0
  66. package/packages/pi-ai/dist/providers/api-family.test.d.ts +2 -0
  67. package/packages/pi-ai/dist/providers/api-family.test.d.ts.map +1 -0
  68. package/packages/pi-ai/dist/providers/api-family.test.js +101 -0
  69. package/packages/pi-ai/dist/providers/api-family.test.js.map +1 -0
  70. package/packages/pi-ai/src/index.ts +1 -0
  71. package/packages/pi-ai/src/providers/api-family.test.ts +129 -0
  72. package/packages/pi-ai/src/providers/api-family.ts +57 -0
  73. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  74. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  75. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -1
  78. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/retry-handler.js +4 -1
  82. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  83. package/packages/pi-coding-agent/src/core/extensions/runner.ts +4 -1
  84. package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -2
  85. package/packages/pi-coding-agent/src/core/retry-handler.ts +4 -1
  86. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  87. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -10
  88. package/src/resources/extensions/gsd/auto/phases.ts +3 -0
  89. package/src/resources/extensions/gsd/auto-dashboard.ts +25 -1
  90. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +15 -2
  91. package/src/resources/extensions/gsd/auto-dispatch.ts +21 -7
  92. package/src/resources/extensions/gsd/auto-model-selection.ts +3 -1
  93. package/src/resources/extensions/gsd/auto-prompts.ts +33 -9
  94. package/src/resources/extensions/gsd/auto-worktree.ts +16 -1
  95. package/src/resources/extensions/gsd/doctor-git-checks.ts +23 -2
  96. package/src/resources/extensions/gsd/pre-execution-checks.ts +12 -8
  97. package/src/resources/extensions/gsd/prompts/add-tests.md +1 -0
  98. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  99. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -0
  100. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +49 -0
  101. package/src/resources/extensions/gsd/tests/integration/doctor-git-symlink-cwd.test.ts +79 -0
  102. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +66 -0
  103. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +132 -8
  104. package/src/resources/extensions/gsd/tests/prompts-no-gitignored-test-refs.test.ts +56 -0
  105. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +54 -0
  106. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +97 -0
  107. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +14 -0
  108. package/src/resources/extensions/search-the-web/command-search-provider.ts +4 -1
  109. package/src/resources/extensions/search-the-web/native-search.ts +13 -3
  110. /package/dist/web/standalone/.next/static/{iBwPQUj73sn8jxegTo320 → By_yegSJ-AA1OP0QjYbSl}/_buildManifest.js +0 -0
  111. /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 } : undefined),
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(undefined, prefs?.preferences);
82
+ windowTokens = resolveExecutorContextWindow(modelRegistry, prefs?.preferences, sessionContextWindow);
83
83
  }
84
84
  catch (e) {
85
85
  logWarning("prompt", `resolveExecutorContextWindow failed: ${e.message}`);
86
- windowTokens = 200_000; // safe default
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(undefined, prefs?.preferences);
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, { carryForwardPaths: depPaths });
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
- "--", ":(exclude).gsd/milestones",
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
- const cwd = process.cwd();
88
- if (wt.path === cwd || cwd.startsWith(wt.path + sep)) {
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 it intentionally includes
353
- * files the task will create, so they don't need to pre-exist (#3626).
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, sameTaskOutputs);
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 tasks`,
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
- const isAnthropic = ctx.model?.provider === 'anthropic';
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
- isAnthropicProvider = event.model.provider === "anthropic";
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?.provider) {
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) {