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
@@ -16,7 +16,7 @@ import type {
16
16
  VerificationContext,
17
17
  VerificationResult,
18
18
  } from "../auto-verification.js";
19
- import type { DispatchAction } from "../auto-dispatch.js";
19
+ import type { DispatchAction, DispatchContext } from "../auto-dispatch.js";
20
20
  import type { WorktreeResolver } from "../worktree-resolver.js";
21
21
  import type { CmuxLogLevel } from "../../cmux/index.js";
22
22
  import type { JournalEntry } from "../journal.js";
@@ -145,15 +145,7 @@ export interface LoopDeps {
145
145
  } | null>;
146
146
 
147
147
  // Dispatch
148
- resolveDispatch: (dctx: {
149
- basePath: string;
150
- mid: string;
151
- midTitle: string;
152
- state: GSDState;
153
- prefs: GSDPreferences | undefined;
154
- session?: AutoSession;
155
- structuredQuestionsAvailable?: "true" | "false";
156
- }) => Promise<DispatchAction>;
148
+ resolveDispatch: (dctx: DispatchContext) => Promise<DispatchAction>;
157
149
  runPreDispatchHooks: (
158
150
  unitType: string,
159
151
  unitId: string,
@@ -50,6 +50,7 @@ import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "
50
50
  import { getEligibleSlices } from "../slice-parallel-eligibility.js";
51
51
  import { startSliceParallel } from "../slice-parallel-orchestrator.js";
52
52
  import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
53
+ import type { MinimalModelRegistry } from "../context-budget.js";
53
54
  import { ensurePlanV2Graph } from "../uok/plan-v2.js";
54
55
  import { resolveUokFlags } from "../uok/flags.js";
55
56
  import { UokGateRunner } from "../uok/gate-runner.js";
@@ -857,6 +858,8 @@ export async function runDispatch(
857
858
  prefs,
858
859
  session: s,
859
860
  structuredQuestionsAvailable,
861
+ sessionContextWindow: ctx.model?.contextWindow,
862
+ modelRegistry: ctx.modelRegistry as MinimalModelRegistry | undefined,
860
863
  });
861
864
 
862
865
  if (dispatchResult.action === "stop") {
@@ -18,6 +18,7 @@ import { getCurrentBranch } from "./worktree.js";
18
18
  import { getActiveHook } from "./post-unit-hooks.js";
19
19
  import { getLedger, getProjectTotals } from "./metrics.js";
20
20
  import { getErrorMessage } from "./error-utils.js";
21
+ import { nativeIsRepo } from "./native-git-bridge.js";
21
22
  import {
22
23
  resolveMilestoneFile,
23
24
  resolveSliceFile,
@@ -336,6 +337,10 @@ let lastCommitFetchedAt = 0;
336
337
 
337
338
  function refreshLastCommit(basePath: string): void {
338
339
  try {
340
+ if (!nativeIsRepo(basePath)) {
341
+ cachedLastCommit = null;
342
+ return;
343
+ }
339
344
  const raw = execFileSync("git", ["log", "-1", "--format=%cr|%s"], {
340
345
  cwd: basePath,
341
346
  encoding: "utf-8",
@@ -349,10 +354,12 @@ function refreshLastCommit(basePath: string): void {
349
354
  message: raw.slice(sep + 1),
350
355
  };
351
356
  }
352
- lastCommitFetchedAt = Date.now();
353
357
  } catch (err) {
354
358
  // Non-fatal — just skip last commit display
359
+ cachedLastCommit = null;
355
360
  logWarning("dashboard", `operation failed: ${err instanceof Error ? err.message : String(err)}`);
361
+ } finally {
362
+ lastCommitFetchedAt = Date.now();
356
363
  }
357
364
  }
358
365
 
@@ -364,6 +371,23 @@ function getLastCommit(basePath: string): { timeAgo: string; message: string } |
364
371
  return cachedLastCommit;
365
372
  }
366
373
 
374
+ export function _resetLastCommitCacheForTests(): void {
375
+ cachedLastCommit = null;
376
+ lastCommitFetchedAt = 0;
377
+ }
378
+
379
+ export function _refreshLastCommitForTests(basePath: string): void {
380
+ refreshLastCommit(basePath);
381
+ }
382
+
383
+ export function _getLastCommitForTests(basePath: string): { timeAgo: string; message: string } | null {
384
+ return getLastCommit(basePath);
385
+ }
386
+
387
+ export function _getLastCommitFetchedAtForTests(): number {
388
+ return lastCommitFetchedAt;
389
+ }
390
+
367
391
  // ─── Footer Factory ───────────────────────────────────────────────────────────
368
392
 
369
393
  /**
@@ -28,6 +28,7 @@ import {
28
28
  buildReplanSlicePrompt,
29
29
  } from "./auto-prompts.js";
30
30
  import { loadEffectiveGSDPreferences } from "./preferences.js";
31
+ import type { MinimalModelRegistry } from "./context-budget.js";
31
32
  import { pauseAuto } from "./auto.js";
32
33
  import {
33
34
  getWorkflowTransportSupportError,
@@ -104,7 +105,13 @@ export async function dispatchDirectPhase(
104
105
  }
105
106
  unitType = "plan-slice";
106
107
  unitId = `${mid}/${sid}`;
107
- prompt = await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, base);
108
+ prompt = await buildPlanSlicePrompt(
109
+ mid, midTitle, sid, sTitle, base, undefined,
110
+ {
111
+ sessionContextWindow: ctx.model?.contextWindow,
112
+ modelRegistry: ctx.modelRegistry as MinimalModelRegistry | undefined,
113
+ },
114
+ );
108
115
  } else {
109
116
  unitType = "plan-milestone";
110
117
  unitId = mid;
@@ -129,7 +136,13 @@ export async function dispatchDirectPhase(
129
136
  }
130
137
  unitType = "execute-task";
131
138
  unitId = `${mid}/${sid}/${tid}`;
132
- prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base);
139
+ prompt = await buildExecuteTaskPrompt(
140
+ mid, sid, sTitle, tid, tTitle, base,
141
+ {
142
+ sessionContextWindow: ctx.model?.contextWindow,
143
+ modelRegistry: ctx.modelRegistry as MinimalModelRegistry | undefined,
144
+ },
145
+ );
133
146
  break;
134
147
  }
135
148
 
@@ -12,6 +12,7 @@
12
12
  import type { GSDState } from "./types.js";
13
13
  import type { GSDPreferences } from "./preferences.js";
14
14
  import type { UatType } from "./files.js";
15
+ import type { MinimalModelRegistry } from "./context-budget.js";
15
16
  import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
16
17
  import { isDbAvailable, getMilestoneSlices, getPendingGates, markAllGatesOmitted, getMilestone, updateMilestoneStatus } from "./gsd-db.js";
17
18
  import { isClosedStatus } from "./status-guards.js";
@@ -81,6 +82,10 @@ export interface DispatchContext {
81
82
  prefs: GSDPreferences | undefined;
82
83
  session?: import("./auto/session.js").AutoSession;
83
84
  structuredQuestionsAvailable?: "true" | "false";
85
+ /** Session model context window in tokens, forwarded to the budget engine's prompt builders. */
86
+ sessionContextWindow?: number;
87
+ /** Model registry forwarded to the budget engine so it can look up the configured executor model. */
88
+ modelRegistry?: MinimalModelRegistry;
84
89
  }
85
90
 
86
91
  export interface DispatchRule {
@@ -521,7 +526,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
521
526
  // auto-heal without either adding an explicit `setSliceSketchFlag(..., false)`
522
527
  // call here or doing so inside the plan-slice tool handler.
523
528
  name: "refining → refine-slice",
524
- match: async ({ state, mid, midTitle, basePath, prefs }) => {
529
+ match: async ({ state, mid, midTitle, basePath, prefs, sessionContextWindow, modelRegistry }) => {
525
530
  if (state.phase !== "refining") return null;
526
531
  if (!state.activeSlice) return missingSliceStop(mid, state.phase);
527
532
  const sid = state.activeSlice.id;
@@ -546,7 +551,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
546
551
  unitId: `${mid}/${sid}`,
547
552
  prompt: await buildPlanSlicePrompt(
548
553
  mid, midTitle, sid, sTitle, basePath, undefined,
549
- softScopeHint ? { softScopeHint } : undefined,
554
+ { ...(softScopeHint ? { softScopeHint } : {}), sessionContextWindow, modelRegistry },
550
555
  ),
551
556
  };
552
557
  }
@@ -554,13 +559,16 @@ export const DISPATCH_RULES: DispatchRule[] = [
554
559
  action: "dispatch",
555
560
  unitType: "refine-slice",
556
561
  unitId: `${mid}/${sid}`,
557
- prompt: await buildRefineSlicePrompt(mid, midTitle, sid, sTitle, basePath),
562
+ prompt: await buildRefineSlicePrompt(
563
+ mid, midTitle, sid, sTitle, basePath, undefined,
564
+ { sessionContextWindow, modelRegistry },
565
+ ),
558
566
  };
559
567
  },
560
568
  },
561
569
  {
562
570
  name: "planning → plan-slice",
563
- match: async ({ state, mid, midTitle, basePath }) => {
571
+ match: async ({ state, mid, midTitle, basePath, sessionContextWindow, modelRegistry }) => {
564
572
  if (state.phase !== "planning") return null;
565
573
  if (!state.activeSlice) return missingSliceStop(mid, state.phase);
566
574
  const sid = state.activeSlice!.id;
@@ -575,6 +583,8 @@ export const DISPATCH_RULES: DispatchRule[] = [
575
583
  sid,
576
584
  sTitle,
577
585
  basePath,
586
+ undefined,
587
+ { sessionContextWindow, modelRegistry },
578
588
  ),
579
589
  };
580
590
  },
@@ -635,7 +645,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
635
645
  },
636
646
  {
637
647
  name: "executing → reactive-execute (parallel dispatch)",
638
- match: async ({ state, mid, midTitle, basePath, prefs }) => {
648
+ match: async ({ state, mid, midTitle, basePath, prefs, sessionContextWindow, modelRegistry }) => {
639
649
  if (state.phase !== "executing" || !state.activeTask) return null;
640
650
  if (!state.activeSlice) return null; // fall through
641
651
 
@@ -726,6 +736,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
726
736
  selected,
727
737
  basePath,
728
738
  subagentModel,
739
+ { sessionContextWindow, modelRegistry },
729
740
  ),
730
741
  };
731
742
  } catch (err) {
@@ -737,7 +748,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
737
748
  },
738
749
  {
739
750
  name: "executing → execute-task (recover missing task plan → plan-slice)",
740
- match: async ({ state, mid, midTitle, basePath }) => {
751
+ match: async ({ state, mid, midTitle, basePath, sessionContextWindow, modelRegistry }) => {
741
752
  if (state.phase !== "executing" || !state.activeTask) return null;
742
753
  if (!state.activeSlice) return missingSliceStop(mid, state.phase);
743
754
  const sid = state.activeSlice!.id;
@@ -761,6 +772,8 @@ export const DISPATCH_RULES: DispatchRule[] = [
761
772
  sid,
762
773
  sTitle,
763
774
  basePath,
775
+ undefined,
776
+ { sessionContextWindow, modelRegistry },
764
777
  ),
765
778
  };
766
779
  }
@@ -770,7 +783,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
770
783
  },
771
784
  {
772
785
  name: "executing → execute-task",
773
- match: async ({ state, mid, basePath }) => {
786
+ match: async ({ state, mid, basePath, sessionContextWindow, modelRegistry }) => {
774
787
  if (state.phase !== "executing" || !state.activeTask) return null;
775
788
  if (!state.activeSlice) return missingSliceStop(mid, state.phase);
776
789
  const sid = state.activeSlice!.id;
@@ -789,6 +802,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
789
802
  tid,
790
803
  tTitle,
791
804
  basePath,
805
+ { sessionContextWindow, modelRegistry },
792
806
  ),
793
807
  };
794
808
  },
@@ -488,7 +488,9 @@ export function resolveModelId<T extends { id: string; provider: string }>(
488
488
  if (providerMatch) return providerMatch;
489
489
  }
490
490
 
491
- // Prefer "anthropic" as the canonical provider for Anthropic models
491
+ // Prefer "anthropic" as the canonical provider for Anthropic models.
492
+ // Transport-specific tiebreaker (ADR-012): intentionally keys on provider,
493
+ // not api — we want the plain Anthropic transport when multiple are available.
492
494
  const anthropicMatch = candidates.find(m => m.provider === "anthropic");
493
495
  if (anthropicMatch) return anthropicMatch;
494
496
 
@@ -23,7 +23,7 @@ import type { GSDPreferences } from "./preferences.js";
23
23
  import { getLoadedSkills, type Skill } from "@gsd/pi-coding-agent";
24
24
  import { join, basename } from "node:path";
25
25
  import { existsSync } from "node:fs";
26
- import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
26
+ import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary, type MinimalModelRegistry } from "./context-budget.js";
27
27
  import { getPendingGates, getPendingGatesForTurn } from "./gsd-db.js";
28
28
  import {
29
29
  GATE_REGISTRY,
@@ -94,14 +94,19 @@ function capPreamble(preamble: string): string {
94
94
  * Uses the budget engine to compute task count ranges and inline context budgets
95
95
  * based on the configured executor model's context window.
96
96
  */
97
- function formatExecutorConstraints(): string {
97
+ function formatExecutorConstraints(
98
+ sessionContextWindow?: number,
99
+ modelRegistry?: MinimalModelRegistry,
100
+ ): string {
98
101
  let windowTokens: number;
99
102
  try {
100
103
  const prefs = loadEffectiveGSDPreferences();
101
- windowTokens = resolveExecutorContextWindow(undefined, prefs?.preferences);
104
+ windowTokens = resolveExecutorContextWindow(modelRegistry, prefs?.preferences, sessionContextWindow);
102
105
  } catch (e) {
103
106
  logWarning("prompt", `resolveExecutorContextWindow failed: ${(e as Error).message}`);
104
- windowTokens = 200_000; // safe default
107
+ // Delegate to the budget engine without prefs (the path that just threw)
108
+ // so DEFAULT_CONTEXT_WINDOW stays the single source of truth.
109
+ windowTokens = resolveExecutorContextWindow(undefined, undefined, sessionContextWindow);
105
110
  }
106
111
  const budgets = computeBudgets(windowTokens);
107
112
  const { min, max } = budgets.taskCountRange;
@@ -1288,8 +1293,13 @@ async function renderSlicePrompt(options: {
1288
1293
  promptTemplate: "plan-slice" | "refine-slice";
1289
1294
  prependBlocks?: string[];
1290
1295
  extraVars?: Record<string, string>;
1296
+ sessionContextWindow?: number;
1297
+ modelRegistry?: MinimalModelRegistry;
1291
1298
  }): Promise<string> {
1292
- const { mid, sid, sTitle, base, level, promptTemplate, prependBlocks = [], extraVars = {} } = options;
1299
+ const {
1300
+ mid, sid, sTitle, base, level, promptTemplate, prependBlocks = [], extraVars = {},
1301
+ sessionContextWindow, modelRegistry,
1302
+ } = options;
1293
1303
 
1294
1304
  const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
1295
1305
  const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
@@ -1341,7 +1351,7 @@ async function renderSlicePrompt(options: {
1341
1351
  if (overridesInline) inlined.unshift(overridesInline);
1342
1352
 
1343
1353
  const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1344
- const executorContextConstraints = formatExecutorConstraints();
1354
+ const executorContextConstraints = formatExecutorConstraints(sessionContextWindow, modelRegistry);
1345
1355
  const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
1346
1356
  const commitInstruction = "Do not commit — .gsd/ planning docs are managed externally and not tracked in git.";
1347
1357
 
@@ -1370,7 +1380,7 @@ async function renderSlicePrompt(options: {
1370
1380
 
1371
1381
  export async function buildPlanSlicePrompt(
1372
1382
  mid: string, _midTitle: string, sid: string, sTitle: string, base: string, level?: InlineLevel,
1373
- options?: { softScopeHint?: string },
1383
+ options?: { softScopeHint?: string; sessionContextWindow?: number; modelRegistry?: MinimalModelRegistry },
1374
1384
  ): Promise<string> {
1375
1385
  const prependBlocks: string[] = [];
1376
1386
  // ADR-011: when the refining-phase dispatch rule gracefully downgrades to
@@ -1388,6 +1398,8 @@ export async function buildPlanSlicePrompt(
1388
1398
  level: level ?? resolveInlineLevel(),
1389
1399
  promptTemplate: "plan-slice",
1390
1400
  prependBlocks,
1401
+ sessionContextWindow: options?.sessionContextWindow,
1402
+ modelRegistry: options?.modelRegistry,
1391
1403
  });
1392
1404
  }
1393
1405
 
@@ -1400,6 +1412,7 @@ export async function buildPlanSlicePrompt(
1400
1412
  */
1401
1413
  export async function buildRefineSlicePrompt(
1402
1414
  mid: string, _midTitle: string, sid: string, sTitle: string, base: string, level?: InlineLevel,
1415
+ options?: { sessionContextWindow?: number; modelRegistry?: MinimalModelRegistry },
1403
1416
  ): Promise<string> {
1404
1417
  // Pull the stored sketch scope from the DB — the hard constraint we plan within.
1405
1418
  let sketchScope = "";
@@ -1426,6 +1439,8 @@ export async function buildRefineSlicePrompt(
1426
1439
  promptTemplate: "refine-slice",
1427
1440
  prependBlocks,
1428
1441
  extraVars: { sketchScope },
1442
+ sessionContextWindow: options?.sessionContextWindow,
1443
+ modelRegistry: options?.modelRegistry,
1429
1444
  });
1430
1445
  }
1431
1446
 
@@ -1434,6 +1449,10 @@ export interface ExecuteTaskPromptOptions {
1434
1449
  level?: InlineLevel;
1435
1450
  /** Override carry-forward paths (dependency-based instead of order-based). */
1436
1451
  carryForwardPaths?: string[];
1452
+ /** Session model context window in tokens, forwarded to the budget engine. */
1453
+ sessionContextWindow?: number;
1454
+ /** Model registry forwarded to the budget engine for executor-model lookup. */
1455
+ modelRegistry?: MinimalModelRegistry;
1437
1456
  }
1438
1457
 
1439
1458
  export async function buildExecuteTaskPrompt(
@@ -1525,7 +1544,7 @@ export async function buildExecuteTaskPrompt(
1525
1544
 
1526
1545
  // Compute verification budget for the executor's context window (issue #707)
1527
1546
  const prefs = loadEffectiveGSDPreferences();
1528
- const contextWindow = resolveExecutorContextWindow(undefined, prefs?.preferences);
1547
+ const contextWindow = resolveExecutorContextWindow(opts.modelRegistry, prefs?.preferences, opts.sessionContextWindow);
1529
1548
  const budgets = computeBudgets(contextWindow);
1530
1549
  const verificationBudget = `~${Math.round(budgets.verificationBudgetChars / 1000)}K chars`;
1531
1550
 
@@ -2101,6 +2120,7 @@ export async function buildReactiveExecutePrompt(
2101
2120
  mid: string, midTitle: string, sid: string, sTitle: string,
2102
2121
  readyTaskIds: string[], base: string,
2103
2122
  subagentModel?: string,
2123
+ opts?: { sessionContextWindow?: number; modelRegistry?: MinimalModelRegistry },
2104
2124
  ): Promise<string> {
2105
2125
  const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
2106
2126
 
@@ -2142,7 +2162,11 @@ export async function buildReactiveExecutePrompt(
2142
2162
  // Build a full execute-task prompt with dependency-based carry-forward
2143
2163
  const taskPrompt = await buildExecuteTaskPrompt(
2144
2164
  mid, sid, sTitle, tid, tTitle, base,
2145
- { carryForwardPaths: depPaths },
2165
+ {
2166
+ carryForwardPaths: depPaths,
2167
+ sessionContextWindow: opts?.sessionContextWindow,
2168
+ modelRegistry: opts?.modelRegistry,
2169
+ },
2146
2170
  );
2147
2171
 
2148
2172
  const modelSuffix = subagentModel ? ` with model: "${subagentModel}"` : "";
@@ -238,6 +238,14 @@ function clearProjectRootStateFiles(basePath: string, milestoneId: string): void
238
238
  }
239
239
  }
240
240
 
241
+ function isProjectGsdSymlink(basePath: string): boolean {
242
+ try {
243
+ return lstatSyncFn(join(basePath, ".gsd")).isSymbolicLink();
244
+ } catch {
245
+ return false;
246
+ }
247
+ }
248
+
241
249
  // ─── Build Artifact Auto-Resolve ─────────────────────────────────────────────
242
250
 
243
251
  /** Patterns for machine-generated build artifacts that can be safely
@@ -1675,12 +1683,19 @@ export function mergeMilestoneToMain(
1675
1683
  // CONTEXT files into the stash. If stash pop later fails, those files
1676
1684
  // are permanently trapped in the stash entry and lost on the next
1677
1685
  // stash push or drop.
1686
+ //
1687
+ // When `.gsd` itself is a symlink, Git rejects pathspecs below it
1688
+ // ("beyond a symbolic link"). In that layout, exclude the whole symlink
1689
+ // and keep stashing real project files that could block the merge.
1690
+ const stashPathspecs = isProjectGsdSymlink(originalBasePath_)
1691
+ ? [".", ":(exclude).gsd"]
1692
+ : [":(exclude).gsd/milestones"];
1678
1693
  execFileSync(
1679
1694
  "git",
1680
1695
  [
1681
1696
  "stash", "push", "--include-untracked",
1682
1697
  "-m", `gsd: pre-merge stash for ${milestoneId}`,
1683
- "--", ":(exclude).gsd/milestones",
1698
+ "--", ...stashPathspecs,
1684
1699
  ],
1685
1700
  { cwd: originalBasePath_, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
1686
1701
  );
@@ -36,6 +36,22 @@ function isDoctorArtifactOnly(dirPath: string): boolean {
36
36
  }
37
37
  }
38
38
 
39
+ function normalizePathForComparison(path: string): string {
40
+ const resolved = existsSync(path) ? realpathSync(path) : path;
41
+ const normalized = resolved
42
+ .replaceAll("\\", "/")
43
+ .replace(/^\/\/\?\//, "")
44
+ .replace(/\/+$/, "");
45
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
46
+ }
47
+
48
+ function isSameOrNestedPath(candidate: string, container: string): boolean {
49
+ const normalizedCandidate = normalizePathForComparison(candidate);
50
+ const normalizedContainer = normalizePathForComparison(container);
51
+ return normalizedCandidate === normalizedContainer ||
52
+ normalizedCandidate.startsWith(`${normalizedContainer}/`);
53
+ }
54
+
39
55
  export async function checkGitHealth(
40
56
  basePath: string,
41
57
  issues: DoctorIssue[],
@@ -98,8 +114,13 @@ export async function checkGitHealth(
98
114
  // pattern in removeWorktree() (#1946). Without this, git cannot
99
115
  // remove the worktree and the doctor enters a deadlock where it
100
116
  // detects the orphan every run but never cleans it up.
101
- const cwd = process.cwd();
102
- if (wt.path === cwd || cwd.startsWith(wt.path + sep)) {
117
+ let cwd = basePath;
118
+ try {
119
+ cwd = process.cwd();
120
+ } catch {
121
+ cwd = basePath;
122
+ }
123
+ if (isSameOrNestedPath(cwd, wt.path)) {
103
124
  try {
104
125
  process.chdir(basePath);
105
126
  } catch {
@@ -403,10 +403,13 @@ function getExpectedOutputsUpTo(tasks: TaskRow[], taskIndex: number): Set<string
403
403
  /**
404
404
  * Check that all files referenced in task.inputs either:
405
405
  * 1. Exist on disk, OR
406
- * 2. Are in a prior task's expected_output
406
+ * 2. Are in a prior task's expected_output, OR
407
+ * 3. Are in the current task's own expected_output — the task produces them,
408
+ * so they don't need to pre-exist (#4459, mirroring the exemption #3626
409
+ * introduced for task.files).
407
410
  *
408
- * task.files ("files likely touched") is excluded it intentionally includes
409
- * files the task will create, so they don't need to pre-exist (#3626).
411
+ * task.files ("files likely touched") is excluded entirely from this check —
412
+ * it intentionally includes files the task will create (#3626).
410
413
  *
411
414
  * All paths are normalized before comparison to ensure ./src/a.ts matches src/a.ts.
412
415
  */
@@ -419,6 +422,7 @@ export function checkFilePathConsistency(
419
422
  for (let i = 0; i < tasks.length; i++) {
420
423
  const task = tasks[i];
421
424
  const priorOutputs = getExpectedOutputsUpTo(tasks, i);
425
+ const ownOutputs = new Set<string>(task.expected_output.map(normalizeFilePath));
422
426
  const filesToCheck = [...task.inputs];
423
427
 
424
428
  for (const file of filesToCheck) {
@@ -436,23 +440,23 @@ export function checkFilePathConsistency(
436
440
 
437
441
  // Check if file is in prior expected outputs (priorOutputs already normalized)
438
442
  const inPriorOutputs = priorOutputs.has(normalizedFile);
443
+ const inOwnOutputs = ownOutputs.has(normalizedFile);
439
444
 
440
445
  // Directory inputs are satisfied when something produces a file beneath
441
446
  // them — either a prior task or the current task itself.
442
447
  let directorySatisfied = false;
443
- if (!existsOnDisk && !inPriorOutputs && isDirectoryReference(file)) {
444
- const sameTaskOutputs = task.expected_output.map(normalizeFilePath);
448
+ if (!existsOnDisk && !inPriorOutputs && !inOwnOutputs && isDirectoryReference(file)) {
445
449
  directorySatisfied =
446
450
  anyOutputUnderDirectory(normalizedFile, priorOutputs) ||
447
- anyOutputUnderDirectory(normalizedFile, sameTaskOutputs);
451
+ anyOutputUnderDirectory(normalizedFile, ownOutputs);
448
452
  }
449
453
 
450
- if (!existsOnDisk && !inPriorOutputs && !directorySatisfied) {
454
+ if (!existsOnDisk && !inPriorOutputs && !inOwnOutputs && !directorySatisfied) {
451
455
  results.push({
452
456
  category: "file",
453
457
  target: file,
454
458
  passed: false,
455
- message: `Task ${task.id} references '${file}' which doesn't exist and isn't created by prior tasks`,
459
+ message: `Task ${task.id} references '${file}' which doesn't exist and isn't created by prior or same-task outputs`,
456
460
  blocking: true,
457
461
  });
458
462
  }
@@ -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.
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
+ import { execFileSync } from "node:child_process";
6
7
 
7
8
  import {
8
9
  unitVerb,
@@ -15,6 +16,10 @@ import {
15
16
  getWidgetMode,
16
17
  cycleWidgetMode,
17
18
  _resetWidgetModeForTests,
19
+ _resetLastCommitCacheForTests,
20
+ _refreshLastCommitForTests,
21
+ _getLastCommitForTests,
22
+ _getLastCommitFetchedAtForTests,
18
23
  } from "../auto-dashboard.ts";
19
24
 
20
25
  const autoSource = readFileSync(join(process.cwd(), "src", "resources", "extensions", "gsd", "auto.ts"), "utf-8");
@@ -215,6 +220,50 @@ test("auto progress widget renders RTK savings under the footer stats line", ()
215
220
  assert.match(dashboardSource, /lines\.push\(rightAlign\("", theme\.fg\("dim", cachedRtkLabel\), width\)\);/);
216
221
  });
217
222
 
223
+ test("last commit refresh backs off cleanly when base path is not a git repo", (t) => {
224
+ const dir = makeTempDir("non-git");
225
+ mkdirSync(dir, { recursive: true });
226
+
227
+ t.after(() => {
228
+ cleanup(dir);
229
+ _resetLastCommitCacheForTests();
230
+ });
231
+
232
+ _resetLastCommitCacheForTests();
233
+ _refreshLastCommitForTests(dir);
234
+
235
+ assert.equal(_getLastCommitForTests(dir), null);
236
+ assert.ok(
237
+ _getLastCommitFetchedAtForTests() > 0,
238
+ "non-git refresh should still advance fetchedAt to avoid render-loop retries",
239
+ );
240
+ });
241
+
242
+ test("last commit refresh still returns commit info for a valid git repo", (t) => {
243
+ const dir = makeTempDir("git");
244
+ mkdirSync(dir, { recursive: true });
245
+
246
+ execFileSync("git", ["init", "-b", "main"], { cwd: dir, stdio: "pipe" });
247
+ execFileSync("git", ["config", "user.name", "GSD Test"], { cwd: dir, stdio: "pipe" });
248
+ execFileSync("git", ["config", "user.email", "gsd@example.com"], { cwd: dir, stdio: "pipe" });
249
+ writeFileSync(join(dir, "README.md"), "hello\n", "utf-8");
250
+ execFileSync("git", ["add", "README.md"], { cwd: dir, stdio: "pipe" });
251
+ execFileSync("git", ["commit", "-m", "test: seed dashboard repo"], { cwd: dir, stdio: "pipe" });
252
+
253
+ t.after(() => {
254
+ cleanup(dir);
255
+ _resetLastCommitCacheForTests();
256
+ });
257
+
258
+ _resetLastCommitCacheForTests();
259
+ _refreshLastCommitForTests(dir);
260
+
261
+ const lastCommit = _getLastCommitForTests(dir);
262
+ assert.ok(lastCommit, "git repo should produce last commit metadata");
263
+ assert.match(lastCommit!.message, /test: seed dashboard repo/);
264
+ assert.ok(lastCommit!.timeAgo.length > 0, "relative time should be populated");
265
+ });
266
+
218
267
  // ─── extractUatSliceId ───────────────────────────────────────────────────
219
268
 
220
269
  test("extractUatSliceId extracts slice ID from M001/S01 format", () => {