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
|
@@ -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(
|
|
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(
|
|
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 } :
|
|
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(
|
|
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(
|
|
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(
|
|
104
|
+
windowTokens = resolveExecutorContextWindow(modelRegistry, prefs?.preferences, sessionContextWindow);
|
|
102
105
|
} catch (e) {
|
|
103
106
|
logWarning("prompt", `resolveExecutorContextWindow failed: ${(e as Error).message}`);
|
|
104
|
-
|
|
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 {
|
|
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(
|
|
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
|
-
{
|
|
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
|
-
"--",
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
409
|
-
* files the task will create
|
|
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,
|
|
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
|
|
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", () => {
|