gsd-pi 2.77.0-dev.1d17f366c → 2.77.0-dev.58d3d4d6c
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/session.js +6 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +79 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +48 -7
- package/dist/resources/extensions/gsd/auto-start.js +62 -3
- package/dist/resources/extensions/gsd/auto.js +34 -0
- package/dist/resources/extensions/gsd/context-store.js +23 -7
- package/dist/resources/extensions/gsd/forensics.js +106 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +23 -0
- package/dist/resources/extensions/gsd/prompt-cache-optimizer.js +4 -0
- package/dist/resources/extensions/gsd/slice-cadence.js +238 -0
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -2
- package/dist/resources/extensions/gsd/worktree-manager.js +51 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +86 -7
- package/dist/resources/extensions/gsd/worktree-telemetry.js +198 -0
- 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 +9 -9
- 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 +9 -9
- 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/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -5
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +30 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +49 -3
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +48 -9
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +7 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +81 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +59 -7
- package/src/resources/extensions/gsd/auto-start.ts +64 -2
- package/src/resources/extensions/gsd/auto.ts +37 -0
- package/src/resources/extensions/gsd/context-store.ts +25 -8
- package/src/resources/extensions/gsd/forensics.ts +118 -1
- package/src/resources/extensions/gsd/git-service.ts +16 -0
- package/src/resources/extensions/gsd/journal.ts +11 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +21 -0
- package/src/resources/extensions/gsd/prompt-cache-optimizer.ts +4 -0
- package/src/resources/extensions/gsd/slice-cadence.ts +299 -0
- package/src/resources/extensions/gsd/tests/artifacts-table-preserved-on-cache-invalidate.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +5 -8
- package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +12 -9
- package/src/resources/extensions/gsd/tests/auto-start-clean-runtime-db-gated.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-start-cold-db-bootstrap.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/canonical-milestone-root.test.ts +108 -0
- package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/context-store.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/discuss-slice-structured-questions.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/discuss-tool-scope-leak.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/empty-content-abort-loop.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/forensics-hook-key-parse.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/forensics-worktree-telemetry.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/import-done-milestones.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +9 -3
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +93 -1
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +10 -3
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +59 -2
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/prompt-cache-optimizer.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +15 -4
- package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts +4 -5
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/slice-cadence.test.ts +242 -0
- package/src/resources/extensions/gsd/tests/slice-context-injection.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/stop-auto-race-null-unit.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +7 -6
- package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/test-helpers.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/test-helpers.ts +140 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +6 -5
- package/src/resources/extensions/gsd/tests/worktree-health-monorepo.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -3
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +8 -2
- package/src/resources/extensions/gsd/worktree-manager.ts +53 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +96 -9
- package/src/resources/extensions/gsd/worktree-telemetry.ts +322 -0
- /package/dist/web/standalone/.next/static/{vidAVJkURvTJ0_V2-64ro → Cev5xrAYA3ZGTRLyjR2fX}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{vidAVJkURvTJ0_V2-64ro → Cev5xrAYA3ZGTRLyjR2fX}/_ssgManifest.js +0 -0
|
@@ -987,6 +987,27 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
987
987
|
if (g.merge_to_main !== undefined) {
|
|
988
988
|
warnings.push("git.merge_to_main is deprecated — milestone-level merge is now always used. Remove this setting.");
|
|
989
989
|
}
|
|
990
|
+
// #4765 — collapse cadence + milestone resquash
|
|
991
|
+
if (g.collapse_cadence !== undefined) {
|
|
992
|
+
const validCadence = new Set(["milestone", "slice"]);
|
|
993
|
+
if (typeof g.collapse_cadence === "string" && validCadence.has(g.collapse_cadence)) {
|
|
994
|
+
git.collapse_cadence = g.collapse_cadence as "milestone" | "slice";
|
|
995
|
+
} else {
|
|
996
|
+
errors.push("git.collapse_cadence must be one of: milestone, slice");
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (g.milestone_resquash !== undefined) {
|
|
1000
|
+
if (typeof g.milestone_resquash === "boolean") {
|
|
1001
|
+
git.milestone_resquash = g.milestone_resquash;
|
|
1002
|
+
const cadence = (git.collapse_cadence as string | undefined)
|
|
1003
|
+
?? (typeof g.collapse_cadence === "string" ? g.collapse_cadence : undefined);
|
|
1004
|
+
if (cadence !== "slice") {
|
|
1005
|
+
warnings.push('git.milestone_resquash is ignored unless git.collapse_cadence is "slice"');
|
|
1006
|
+
}
|
|
1007
|
+
} else {
|
|
1008
|
+
errors.push("git.milestone_resquash must be a boolean");
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
990
1011
|
|
|
991
1012
|
if (Object.keys(git).length > 0) {
|
|
992
1013
|
validated.git = git as GitPreferences;
|
|
@@ -55,6 +55,10 @@ const SEMI_STATIC_LABELS = new Set([
|
|
|
55
55
|
"prior-summaries",
|
|
56
56
|
"project-context",
|
|
57
57
|
"overrides",
|
|
58
|
+
// KNOWLEDGE is milestone-scoped (stable within a session), so it belongs
|
|
59
|
+
// in the cacheable prefix. See issue #4719.
|
|
60
|
+
"knowledge",
|
|
61
|
+
"project-knowledge",
|
|
58
62
|
]);
|
|
59
63
|
|
|
60
64
|
/** Labels that change per-task */
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice-cadence collapse — #4765.
|
|
3
|
+
*
|
|
4
|
+
* When `git.collapse_cadence: "slice"` is set, each slice's commits are
|
|
5
|
+
* squash-merged from the milestone branch to main as soon as the slice
|
|
6
|
+
* passes validation. Shrinks the orphan window (#4761) from milestone-size
|
|
7
|
+
* to slice-size and surfaces merge conflicts per-slice rather than all at
|
|
8
|
+
* once at milestone end.
|
|
9
|
+
*
|
|
10
|
+
* This module is deliberately focused and narrower than mergeMilestoneToMain:
|
|
11
|
+
* - No worktree teardown (worktree is reused for the next slice)
|
|
12
|
+
* - No DB reconciliation (modern worktrees share the main DB via path resolver)
|
|
13
|
+
* - No roadmap/summary/gate handling (that's still the milestone's job)
|
|
14
|
+
* - Fails loudly on dirty main — caller is responsible for cleanliness
|
|
15
|
+
*
|
|
16
|
+
* Kernighan: the v1 surface handles the happy path + conflict. Edge cases
|
|
17
|
+
* that mergeMilestoneToMain covers (concurrent merges, shared DB paths,
|
|
18
|
+
* submodules) are explicit non-goals; users opt in via preference and early-
|
|
19
|
+
* adopter scenarios are scoped narrow.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { execFileSync } from "node:child_process";
|
|
25
|
+
|
|
26
|
+
import { GSDError, GSD_GIT_ERROR } from "./errors.js";
|
|
27
|
+
import { MergeConflictError } from "./git-service.js";
|
|
28
|
+
import {
|
|
29
|
+
nativeBranchForceReset,
|
|
30
|
+
nativeCheckoutBranch,
|
|
31
|
+
nativeCommit,
|
|
32
|
+
nativeCommitCountBetween,
|
|
33
|
+
nativeConflictFiles,
|
|
34
|
+
nativeDetectMainBranch,
|
|
35
|
+
nativeMergeSquash,
|
|
36
|
+
} from "./native-git-bridge.js";
|
|
37
|
+
import { resolveGitDir } from "./worktree-manager.js";
|
|
38
|
+
import { logWarning } from "./workflow-logger.js";
|
|
39
|
+
import { emitSliceMerged, emitMilestoneResquash } from "./worktree-telemetry.js";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Auto-worktree milestone branch name. Must match autoWorktreeBranch() in
|
|
43
|
+
* auto-worktree.ts; duplicated here to avoid a cyclic import.
|
|
44
|
+
*/
|
|
45
|
+
function milestoneBranchName(milestoneId: string): string {
|
|
46
|
+
return `milestone/${milestoneId}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cleanupMergeArtifacts(projectRoot: string): void {
|
|
50
|
+
try {
|
|
51
|
+
const gitDir = resolveGitDir(projectRoot);
|
|
52
|
+
for (const f of ["SQUASH_MSG", "MERGE_MSG", "MERGE_HEAD"]) {
|
|
53
|
+
const p = join(gitDir, f);
|
|
54
|
+
if (existsSync(p)) unlinkSync(p);
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logWarning("worktree", `merge artifact cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SliceMergeResult {
|
|
62
|
+
commitSha: string | null;
|
|
63
|
+
mainBranch: string;
|
|
64
|
+
milestoneBranch: string;
|
|
65
|
+
durationMs: number;
|
|
66
|
+
skipped: boolean;
|
|
67
|
+
skippedReason?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Squash-merge one slice's commits from the milestone branch to main.
|
|
72
|
+
*
|
|
73
|
+
* Preconditions:
|
|
74
|
+
* - Caller is on the milestone branch inside the worktree
|
|
75
|
+
* - `projectRoot` points at the real project root (not the worktree)
|
|
76
|
+
*
|
|
77
|
+
* Post-conditions on success:
|
|
78
|
+
* - Slice's commits are a single squash commit on main
|
|
79
|
+
* - `milestone/<MID>` is fast-forwarded to main (so next slice's work
|
|
80
|
+
* starts from a clean base)
|
|
81
|
+
* - caller's process.cwd is restored
|
|
82
|
+
*
|
|
83
|
+
* Throws MergeConflictError on conflicts; caller should surface and stop.
|
|
84
|
+
* Throws GSDError on dirty main / detection failures.
|
|
85
|
+
*/
|
|
86
|
+
export function mergeSliceToMain(
|
|
87
|
+
projectRoot: string,
|
|
88
|
+
milestoneId: string,
|
|
89
|
+
sliceId: string,
|
|
90
|
+
): SliceMergeResult {
|
|
91
|
+
const started = Date.now();
|
|
92
|
+
const worktreeCwd = process.cwd();
|
|
93
|
+
const milestoneBranch = milestoneBranchName(milestoneId);
|
|
94
|
+
const mainBranch = nativeDetectMainBranch(projectRoot);
|
|
95
|
+
|
|
96
|
+
// Fast path: if the milestone branch has no commits ahead of main, there
|
|
97
|
+
// is nothing to merge. Return a skip result instead of no-op'ing silently
|
|
98
|
+
// so the caller's telemetry shows the decision.
|
|
99
|
+
let commitsAhead = 0;
|
|
100
|
+
try {
|
|
101
|
+
commitsAhead = nativeCommitCountBetween(projectRoot, mainBranch, milestoneBranch);
|
|
102
|
+
} catch {
|
|
103
|
+
// If we can't count, assume there's work and let the merge proceed —
|
|
104
|
+
// a failing merge is more informative than a silent skip.
|
|
105
|
+
commitsAhead = 1;
|
|
106
|
+
}
|
|
107
|
+
if (commitsAhead === 0) {
|
|
108
|
+
// Do NOT emit slice-merged here — this is a no-op, not a merge. Emitting
|
|
109
|
+
// would inflate slicesMerged in telemetry/forensics and distort the
|
|
110
|
+
// conflict rate denominator.
|
|
111
|
+
return {
|
|
112
|
+
commitSha: null,
|
|
113
|
+
mainBranch,
|
|
114
|
+
milestoneBranch,
|
|
115
|
+
durationMs: Date.now() - started,
|
|
116
|
+
skipped: true,
|
|
117
|
+
skippedReason: "no-commits-ahead",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
process.chdir(projectRoot);
|
|
122
|
+
try {
|
|
123
|
+
// Dirty-main check — v1 fails loudly rather than auto-stashing. Users
|
|
124
|
+
// running slice-cadence opt in knowing main stays clean between merges.
|
|
125
|
+
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
126
|
+
cwd: projectRoot,
|
|
127
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
128
|
+
encoding: "utf-8",
|
|
129
|
+
}).trim();
|
|
130
|
+
if (status) {
|
|
131
|
+
throw new GSDError(
|
|
132
|
+
GSD_GIT_ERROR,
|
|
133
|
+
`slice-cadence merge requires a clean project root; uncommitted changes detected. ` +
|
|
134
|
+
`Commit or stash at ${projectRoot} before retrying. Status:\n${status}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
nativeCheckoutBranch(projectRoot, mainBranch);
|
|
139
|
+
|
|
140
|
+
// Clean any stale merge artifacts before attempting the squash (#2912 pattern)
|
|
141
|
+
cleanupMergeArtifacts(projectRoot);
|
|
142
|
+
|
|
143
|
+
const mergeResult = nativeMergeSquash(projectRoot, milestoneBranch);
|
|
144
|
+
if (!mergeResult.success) {
|
|
145
|
+
const conflictedFiles = mergeResult.conflicts.length > 0
|
|
146
|
+
? mergeResult.conflicts
|
|
147
|
+
: nativeConflictFiles(projectRoot);
|
|
148
|
+
cleanupMergeArtifacts(projectRoot);
|
|
149
|
+
try {
|
|
150
|
+
emitSliceMerged(projectRoot, milestoneId, sliceId, {
|
|
151
|
+
durationMs: Date.now() - started,
|
|
152
|
+
conflict: true,
|
|
153
|
+
});
|
|
154
|
+
} catch { /* silent */ }
|
|
155
|
+
throw new MergeConflictError(
|
|
156
|
+
conflictedFiles,
|
|
157
|
+
"squash",
|
|
158
|
+
milestoneBranch,
|
|
159
|
+
mainBranch,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Commit the squash with a slice-scoped message
|
|
164
|
+
const commitSha = nativeCommit(
|
|
165
|
+
projectRoot,
|
|
166
|
+
`gsd: merge ${sliceId} of ${milestoneId} (slice-cadence)`,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Advance the milestone branch to main so the next slice's commits start
|
|
170
|
+
// from a clean base. Force-reset is safe because we just merged this
|
|
171
|
+
// branch's entire delta.
|
|
172
|
+
nativeBranchForceReset(projectRoot, milestoneBranch, mainBranch);
|
|
173
|
+
|
|
174
|
+
const durationMs = Date.now() - started;
|
|
175
|
+
try {
|
|
176
|
+
emitSliceMerged(projectRoot, milestoneId, sliceId, {
|
|
177
|
+
durationMs,
|
|
178
|
+
conflict: false,
|
|
179
|
+
commitSha: commitSha ?? undefined,
|
|
180
|
+
});
|
|
181
|
+
} catch { /* silent */ }
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
commitSha,
|
|
185
|
+
mainBranch,
|
|
186
|
+
milestoneBranch,
|
|
187
|
+
durationMs,
|
|
188
|
+
skipped: false,
|
|
189
|
+
};
|
|
190
|
+
} finally {
|
|
191
|
+
// Always restore cwd even if anything above threw.
|
|
192
|
+
try { process.chdir(worktreeCwd); } catch { /* best-effort */ }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Re-squash per-slice commits on main into a single milestone commit.
|
|
198
|
+
*
|
|
199
|
+
* Runs at milestone completion when `collapse_cadence: "slice"` AND
|
|
200
|
+
* `milestone_resquash: true`. The `startSha` is the SHA of main immediately
|
|
201
|
+
* before the milestone's first slice merge — the caller is responsible for
|
|
202
|
+
* recording this (AutoSession field, git ref, or DB row).
|
|
203
|
+
*
|
|
204
|
+
* Strategy: soft-reset main to startSha, then commit the net diff. The
|
|
205
|
+
* N slice commits between startSha and HEAD are collapsed into one.
|
|
206
|
+
*
|
|
207
|
+
* No-op (returns false) if startSha equals HEAD (nothing to re-squash).
|
|
208
|
+
*/
|
|
209
|
+
export function resquashMilestoneOnMain(
|
|
210
|
+
projectRoot: string,
|
|
211
|
+
milestoneId: string,
|
|
212
|
+
startSha: string,
|
|
213
|
+
): { resquashed: boolean; newSha: string | null } {
|
|
214
|
+
const mainBranch = nativeDetectMainBranch(projectRoot);
|
|
215
|
+
const worktreeCwd = process.cwd();
|
|
216
|
+
|
|
217
|
+
process.chdir(projectRoot);
|
|
218
|
+
try {
|
|
219
|
+
nativeCheckoutBranch(projectRoot, mainBranch);
|
|
220
|
+
|
|
221
|
+
// Verify the startSha..HEAD range contains ONLY this milestone's slice-
|
|
222
|
+
// cadence commits. If any unrelated commits landed on main since the
|
|
223
|
+
// milestone started (e.g. concurrent work, cherry-picks, hotfixes), a
|
|
224
|
+
// blind `git reset --soft` would fold them into the re-squash and rewrite
|
|
225
|
+
// their attribution. Fail closed — the user can resolve manually.
|
|
226
|
+
const expectedSuffix = `(slice-cadence)`;
|
|
227
|
+
const expectedMilestoneToken = ` of ${milestoneId} `;
|
|
228
|
+
let subjectsRaw = "";
|
|
229
|
+
try {
|
|
230
|
+
subjectsRaw = execFileSync(
|
|
231
|
+
"git",
|
|
232
|
+
["log", "--format=%s", `${startSha}..HEAD`],
|
|
233
|
+
{ cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
|
|
234
|
+
);
|
|
235
|
+
} catch {
|
|
236
|
+
return { resquashed: false, newSha: null };
|
|
237
|
+
}
|
|
238
|
+
const subjects = subjectsRaw.split("\n").filter((s) => s.length > 0);
|
|
239
|
+
const sliceCount = subjects.length;
|
|
240
|
+
if (sliceCount === 0) {
|
|
241
|
+
return { resquashed: false, newSha: null };
|
|
242
|
+
}
|
|
243
|
+
const foreign = subjects.filter(
|
|
244
|
+
(s) => !(s.endsWith(expectedSuffix) && s.includes(expectedMilestoneToken)),
|
|
245
|
+
);
|
|
246
|
+
if (foreign.length > 0) {
|
|
247
|
+
logWarning(
|
|
248
|
+
"worktree",
|
|
249
|
+
`slice-cadence: skipping milestone resquash for ${milestoneId} — ` +
|
|
250
|
+
`${foreign.length} non-slice-cadence commit(s) in ${startSha}..HEAD ` +
|
|
251
|
+
`would be folded in. First: "${foreign[0]}". Resolve history manually.`,
|
|
252
|
+
);
|
|
253
|
+
return { resquashed: false, newSha: null };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Safe to collapse: all commits in the range are this milestone's slices.
|
|
257
|
+
execFileSync("git", ["reset", "--soft", startSha], {
|
|
258
|
+
cwd: projectRoot,
|
|
259
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
260
|
+
encoding: "utf-8",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const newSha = nativeCommit(
|
|
264
|
+
projectRoot,
|
|
265
|
+
`gsd: complete milestone ${milestoneId} (${sliceCount} slices re-squashed)`,
|
|
266
|
+
{ allowEmpty: true },
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
emitMilestoneResquash(projectRoot, milestoneId, {
|
|
271
|
+
sliceCount,
|
|
272
|
+
startSha,
|
|
273
|
+
endSha: newSha ?? undefined,
|
|
274
|
+
});
|
|
275
|
+
} catch { /* silent */ }
|
|
276
|
+
|
|
277
|
+
return { resquashed: true, newSha };
|
|
278
|
+
} finally {
|
|
279
|
+
try { process.chdir(worktreeCwd); } catch { /* best-effort */ }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Read the effective collapse cadence from validated preferences. Accepts
|
|
285
|
+
* a raw preferences object (the shape loadEffectiveGSDPreferences returns).
|
|
286
|
+
*/
|
|
287
|
+
export function getCollapseCadence(
|
|
288
|
+
prefs: { git?: { collapse_cadence?: "milestone" | "slice" } } | undefined | null,
|
|
289
|
+
): "milestone" | "slice" {
|
|
290
|
+
return prefs?.git?.collapse_cadence ?? "milestone";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function getMilestoneResquash(
|
|
294
|
+
prefs: { git?: { milestone_resquash?: boolean } } | undefined | null,
|
|
295
|
+
): boolean {
|
|
296
|
+
// Default true when cadence is slice — resquash preserves the milestone-
|
|
297
|
+
// level history shape users expect.
|
|
298
|
+
return prefs?.git?.milestone_resquash !== false;
|
|
299
|
+
}
|
package/src/resources/extensions/gsd/tests/artifacts-table-preserved-on-cache-invalidate.test.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { readFileSync } from "node:fs";
|
|
|
22
22
|
import { resolve } from "node:path";
|
|
23
23
|
|
|
24
24
|
import { invalidateAllCaches } from "../cache.ts";
|
|
25
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
25
26
|
import {
|
|
26
27
|
openDatabase,
|
|
27
28
|
closeDatabase,
|
|
@@ -160,7 +161,7 @@ describe("cache.ts must not re-import clearArtifacts into invalidateAllCaches",
|
|
|
160
161
|
test("invalidateAllCaches does not call clearArtifacts", () => {
|
|
161
162
|
const fnIdx = src.indexOf("function invalidateAllCaches");
|
|
162
163
|
assert.ok(fnIdx !== -1);
|
|
163
|
-
const body = src
|
|
164
|
+
const body = extractSourceRegion(src, "function invalidateAllCaches", { fromIdx: fnIdx });
|
|
164
165
|
assert.ok(
|
|
165
166
|
!/\bclearArtifacts\s*\(/.test(body),
|
|
166
167
|
"invalidateAllCaches must not call clearArtifacts() — it wipes the write-through store",
|
|
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
|
|
6
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
6
7
|
import {
|
|
7
8
|
resolveAgentEnd,
|
|
8
9
|
resolveAgentEndCancelled,
|
|
@@ -595,7 +596,7 @@ test("auto/phases.ts: selectAndApplyModel called exactly once and before updateP
|
|
|
595
596
|
// Extract the runUnitPhase function body
|
|
596
597
|
const fnStart = src.indexOf("export async function runUnitPhase");
|
|
597
598
|
assert.ok(fnStart > 0, "runUnitPhase should exist in phases.ts");
|
|
598
|
-
const fnBody = src
|
|
599
|
+
const fnBody = extractSourceRegion(src, "export async function runUnitPhase");
|
|
599
600
|
|
|
600
601
|
// selectAndApplyModel must appear exactly once
|
|
601
602
|
const allOccurrences = [...fnBody.matchAll(/selectAndApplyModel\(/g)];
|
|
@@ -1538,9 +1539,7 @@ test("auto.ts startAuto dispatches through the UOK kernel wrapper with explicit
|
|
|
1538
1539
|
// Find the startAuto function body
|
|
1539
1540
|
const fnIdx = src.indexOf("export async function startAuto");
|
|
1540
1541
|
assert.ok(fnIdx > -1, "startAuto must exist in auto.ts");
|
|
1541
|
-
const
|
|
1542
|
-
const fnBlock =
|
|
1543
|
-
fnEnd > -1 ? src.slice(fnIdx, fnEnd) : src.slice(fnIdx, fnIdx + 5000);
|
|
1542
|
+
const fnBlock = extractSourceRegion(src, "export async function startAuto", "\n// ─── ");
|
|
1544
1543
|
assert.ok(
|
|
1545
1544
|
fnBlock.includes("runAutoLoopWithUok("),
|
|
1546
1545
|
"startAuto must dispatch through runAutoLoopWithUok()",
|
|
@@ -1562,9 +1561,7 @@ test("startAuto calls selfHealRuntimeRecords before autoLoop (#1727)", { skip: "
|
|
|
1562
1561
|
);
|
|
1563
1562
|
const fnIdx = src.indexOf("export async function startAuto");
|
|
1564
1563
|
assert.ok(fnIdx > -1, "startAuto must exist in auto.ts");
|
|
1565
|
-
const
|
|
1566
|
-
const fnBlock =
|
|
1567
|
-
fnEnd > -1 ? src.slice(fnIdx, fnEnd) : src.slice(fnIdx, fnIdx + 5000);
|
|
1564
|
+
const fnBlock = extractSourceRegion(src, "export async function startAuto", "\n// ─── ");
|
|
1568
1565
|
|
|
1569
1566
|
// Both autoLoop call sites must be preceded by selfHealRuntimeRecords
|
|
1570
1567
|
const healIdx = fnBlock.indexOf("selfHealRuntimeRecords");
|
|
@@ -1589,7 +1586,7 @@ test("startAuto guards against concurrent invocation (#2923)", () => {
|
|
|
1589
1586
|
const fnIdx = src.indexOf("export async function startAuto");
|
|
1590
1587
|
assert.ok(fnIdx > -1, "startAuto must exist in auto.ts");
|
|
1591
1588
|
// The guard must appear before any other logic in the function body
|
|
1592
|
-
const fnBody = src
|
|
1589
|
+
const fnBody = extractSourceRegion(src, "export async function startAuto");
|
|
1593
1590
|
const activeGuard = fnBody.indexOf("if (s.active)");
|
|
1594
1591
|
assert.ok(activeGuard > -1, "startAuto must check s.active to prevent concurrent auto-loops");
|
|
1595
1592
|
const returnIdx = fnBody.indexOf("return;", activeGuard);
|
|
@@ -15,6 +15,7 @@ import assert from 'node:assert/strict';
|
|
|
15
15
|
import { readFileSync } from 'node:fs';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { dirname, join } from 'node:path';
|
|
18
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
18
19
|
|
|
19
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
21
|
const __dirname = dirname(__filename);
|
|
@@ -43,7 +44,9 @@ describe('auto-remediate stale slice status (#3673)', () => {
|
|
|
43
44
|
assert.match(before, /try\s*\{/,
|
|
44
45
|
'updateSliceStatus should be inside a try block');
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
// Bound the region to stop before the rogue fallback so /catch/ only
|
|
48
|
+
// matches this try block's catch, not an unrelated later one.
|
|
49
|
+
const after = extractSourceRegion(source, 'updateSliceStatus(mid, sid', 'rogues.push({');
|
|
47
50
|
assert.match(after, /catch/,
|
|
48
51
|
'try block should have a catch for fallback');
|
|
49
52
|
});
|
|
@@ -16,6 +16,7 @@ import assert from "node:assert/strict";
|
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
17
|
import { resolve } from "node:path";
|
|
18
18
|
|
|
19
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
19
20
|
import {
|
|
20
21
|
resetEvidence,
|
|
21
22
|
getEvidence,
|
|
@@ -107,7 +108,9 @@ describe("register-hooks: skip prepareWorkflowMcpForProject inside auto-worktree
|
|
|
107
108
|
it("session_start hook is gated on isInAutoWorktree", () => {
|
|
108
109
|
const idx = src.indexOf('pi.on("session_start"');
|
|
109
110
|
assert.ok(idx !== -1, "session_start handler must exist");
|
|
110
|
-
|
|
111
|
+
// Bound the extraction at the next pi.on(...) so adjacent handlers
|
|
112
|
+
// can't bleed into this handler's assertions.
|
|
113
|
+
const block = extractSourceRegion(src, 'pi.on("session_start"', 'pi.on("session_switch"');
|
|
111
114
|
assert.ok(
|
|
112
115
|
block.includes("isInAutoWorktree"),
|
|
113
116
|
"session_start must consult isInAutoWorktree before preparing MCP",
|
|
@@ -121,7 +124,7 @@ describe("register-hooks: skip prepareWorkflowMcpForProject inside auto-worktree
|
|
|
121
124
|
it("session_switch hook is gated on isInAutoWorktree", () => {
|
|
122
125
|
const idx = src.indexOf('pi.on("session_switch"');
|
|
123
126
|
assert.ok(idx !== -1, "session_switch handler must exist");
|
|
124
|
-
const block = src.
|
|
127
|
+
const block = extractSourceRegion(src, 'pi.on("session_switch"', 'pi.on("tool_call"');
|
|
125
128
|
assert.ok(
|
|
126
129
|
block.includes("isInAutoWorktree"),
|
|
127
130
|
"session_switch must consult isInAutoWorktree before preparing MCP",
|
|
@@ -192,7 +195,7 @@ describe("mcp-server workflow-tools: projectDir routing (Phase B root cause)", (
|
|
|
192
195
|
it("projectDirParam is optional and documents the default", () => {
|
|
193
196
|
const idx = src.indexOf("const projectDirParam");
|
|
194
197
|
assert.ok(idx !== -1, "projectDirParam definition must exist");
|
|
195
|
-
const block = src
|
|
198
|
+
const block = extractSourceRegion(src, "const projectDirParam");
|
|
196
199
|
assert.ok(
|
|
197
200
|
block.includes(".optional()"),
|
|
198
201
|
"projectDirParam must be optional so the agent stops deliberating",
|
|
@@ -206,7 +209,7 @@ describe("mcp-server workflow-tools: projectDir routing (Phase B root cause)", (
|
|
|
206
209
|
it("parseWorkflowArgs defaults projectDir to process.cwd() when omitted", () => {
|
|
207
210
|
const idx = src.indexOf("function parseWorkflowArgs");
|
|
208
211
|
assert.ok(idx !== -1, "parseWorkflowArgs must exist");
|
|
209
|
-
const block = src
|
|
212
|
+
const block = extractSourceRegion(src, "function parseWorkflowArgs");
|
|
210
213
|
assert.ok(
|
|
211
214
|
block.includes("parsed.projectDir ?? process.cwd()"),
|
|
212
215
|
"parseWorkflowArgs must fall back to process.cwd() when projectDir is omitted",
|
|
@@ -216,7 +219,7 @@ describe("mcp-server workflow-tools: projectDir routing (Phase B root cause)", (
|
|
|
216
219
|
it("validateProjectDir accepts external-state worktree paths via .gsd symlink target", () => {
|
|
217
220
|
const idx = src.indexOf("function validateProjectDir");
|
|
218
221
|
assert.ok(idx !== -1, "validateProjectDir must exist");
|
|
219
|
-
const block = src
|
|
222
|
+
const block = extractSourceRegion(src, "function validateProjectDir");
|
|
220
223
|
assert.ok(
|
|
221
224
|
block.includes("resolveExternalStateRoot"),
|
|
222
225
|
"validateProjectDir must consult resolveExternalStateRoot for external-state layouts",
|
|
@@ -224,7 +227,7 @@ describe("mcp-server workflow-tools: projectDir routing (Phase B root cause)", (
|
|
|
224
227
|
|
|
225
228
|
const helperIdx = src.indexOf("function resolveExternalStateRoot");
|
|
226
229
|
assert.ok(helperIdx !== -1, "resolveExternalStateRoot helper must exist");
|
|
227
|
-
const helperBlock = src
|
|
230
|
+
const helperBlock = extractSourceRegion(src, "function resolveExternalStateRoot");
|
|
228
231
|
assert.ok(
|
|
229
232
|
helperBlock.includes("realpathSync"),
|
|
230
233
|
"resolveExternalStateRoot must use realpathSync to follow the symlink",
|
|
@@ -240,7 +243,7 @@ describe("mcp-server workflow-tools: projectDir routing (Phase B root cause)", (
|
|
|
240
243
|
// milestone that has an auto-worktree at <projectRoot>/.gsd/worktrees/<MID>/,
|
|
241
244
|
// tool writes must go to the worktree .gsd rather than the shared project .gsd.
|
|
242
245
|
const parseIdx = src.indexOf("function parseWorkflowArgs");
|
|
243
|
-
const parseBlock = src
|
|
246
|
+
const parseBlock = extractSourceRegion(src, "function parseWorkflowArgs");
|
|
244
247
|
assert.ok(
|
|
245
248
|
parseBlock.includes("resolveActiveWorktreeBasePath"),
|
|
246
249
|
"parseWorkflowArgs must consult resolveActiveWorktreeBasePath",
|
|
@@ -254,7 +257,7 @@ describe("mcp-server workflow-tools: projectDir routing (Phase B root cause)", (
|
|
|
254
257
|
it("resolveActiveWorktreeBasePath checks .git presence to avoid hijacking stray directories", () => {
|
|
255
258
|
const idx = src.indexOf("function resolveActiveWorktreeBasePath");
|
|
256
259
|
assert.ok(idx !== -1, "resolveActiveWorktreeBasePath helper must exist");
|
|
257
|
-
const block = src
|
|
260
|
+
const block = extractSourceRegion(src, "function resolveActiveWorktreeBasePath");
|
|
258
261
|
assert.ok(
|
|
259
262
|
block.includes('existsSync(join(wtPath, ".git"))'),
|
|
260
263
|
"resolveActiveWorktreeBasePath must verify a .git file exists in the worktree",
|
|
@@ -264,7 +267,7 @@ describe("mcp-server workflow-tools: projectDir routing (Phase B root cause)", (
|
|
|
264
267
|
it("extractMilestoneId handles camelCase, snake_case, and short aliases", () => {
|
|
265
268
|
const idx = src.indexOf("function extractMilestoneId");
|
|
266
269
|
assert.ok(idx !== -1, "extractMilestoneId helper must exist");
|
|
267
|
-
const block = src
|
|
270
|
+
const block = extractSourceRegion(src, "function extractMilestoneId");
|
|
268
271
|
assert.ok(block.includes("milestoneId"), "must check milestoneId");
|
|
269
272
|
assert.ok(block.includes("milestone_id"), "must check milestone_id");
|
|
270
273
|
assert.ok(block.includes("mid"), "must check mid");
|
|
@@ -10,6 +10,7 @@ import assert from "node:assert/strict";
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { join, dirname } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
13
14
|
|
|
14
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
const sourceFile = join(__dirname, "..", "auto-start.ts");
|
|
@@ -37,7 +38,7 @@ describe("auto-start cleanStaleRuntimeUnits DB gating (#4663)", () => {
|
|
|
37
38
|
test("cleanStaleRuntimeUnits predicate consults DB status when available", () => {
|
|
38
39
|
const cleanIdx = source.indexOf("cleanStaleRuntimeUnits(");
|
|
39
40
|
assert.ok(cleanIdx > -1);
|
|
40
|
-
const snippet = source
|
|
41
|
+
const snippet = extractSourceRegion(source, "cleanStaleRuntimeUnits(");
|
|
41
42
|
assert.match(
|
|
42
43
|
snippet,
|
|
43
44
|
/isDbAvailable\(\)/,
|
|
@@ -53,7 +54,7 @@ describe("auto-start cleanStaleRuntimeUnits DB gating (#4663)", () => {
|
|
|
53
54
|
test("cleanStaleRuntimeUnits predicate still falls back to SUMMARY-file when DB unavailable", () => {
|
|
54
55
|
const cleanIdx = source.indexOf("cleanStaleRuntimeUnits(");
|
|
55
56
|
assert.ok(cleanIdx > -1);
|
|
56
|
-
const snippet = source
|
|
57
|
+
const snippet = extractSourceRegion(source, "cleanStaleRuntimeUnits(");
|
|
57
58
|
assert.match(
|
|
58
59
|
snippet,
|
|
59
60
|
/resolveMilestoneFile\(base,\s*mid,\s*["']SUMMARY["']\)/,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {createTestContext, extractSourceRegion } from "./test-helpers.ts";
|
|
5
5
|
|
|
6
6
|
const { assertTrue, report } = createTestContext();
|
|
7
7
|
|
|
@@ -13,7 +13,7 @@ console.log("\n=== #2841: cold DB opened before initial deriveState ===");
|
|
|
13
13
|
const helperIdx = src.indexOf("async function openProjectDbIfPresent");
|
|
14
14
|
assertTrue(helperIdx >= 0, "auto-start.ts defines a helper for pre-derive DB open (#2841)");
|
|
15
15
|
|
|
16
|
-
const helperRegion = helperIdx >= 0 ? src
|
|
16
|
+
const helperRegion = helperIdx >= 0 ? extractSourceRegion(src, "async function openProjectDbIfPresent") : "";
|
|
17
17
|
assertTrue(
|
|
18
18
|
helperRegion.includes("resolveProjectRootDbPath(basePath)"),
|
|
19
19
|
"pre-derive DB helper resolves the project-root DB path (#2841)",
|
|
@@ -2,6 +2,7 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
5
6
|
|
|
6
7
|
const sourcePath = join(import.meta.dirname, "..", "auto-start.ts");
|
|
7
8
|
const source = readFileSync(sourcePath, "utf-8");
|
|
@@ -53,7 +54,7 @@ test("bootstrapAutoSession checks manual session override before preferences", (
|
|
|
53
54
|
);
|
|
54
55
|
|
|
55
56
|
// Preferred model should still be part of fallback resolution.
|
|
56
|
-
const snapshotBlock = source
|
|
57
|
+
const snapshotBlock = extractSourceRegion(source, "const startModelSnapshot = manualSessionOverride");
|
|
57
58
|
assert.ok(
|
|
58
59
|
snapshotBlock.includes("validatedPreferredModel") || snapshotBlock.includes("preferredModel"),
|
|
59
60
|
"startModelSnapshot must still consider preferredModel for built-in providers",
|
|
@@ -64,7 +65,7 @@ test("bootstrapAutoSession prioritizes current session model over PREFERENCES.md
|
|
|
64
65
|
const snapshotIdx = source.indexOf("const startModelSnapshot = manualSessionOverride");
|
|
65
66
|
assert.ok(snapshotIdx > -1, "auto-start.ts should build startModelSnapshot");
|
|
66
67
|
|
|
67
|
-
const snapshotBlock = source
|
|
68
|
+
const snapshotBlock = extractSourceRegion(source, "const startModelSnapshot = manualSessionOverride");
|
|
68
69
|
const currentIdx = snapshotBlock.indexOf("currentSessionModel");
|
|
69
70
|
const preferredIdx = snapshotBlock.indexOf("validatedPreferredModel");
|
|
70
71
|
|
|
@@ -97,7 +98,7 @@ test("bootstrapAutoSession prefers session model over PREFERENCES.md when provid
|
|
|
97
98
|
const preferredIdx = source.indexOf("const preferredModel = ");
|
|
98
99
|
assert.ok(preferredIdx > -1, "auto-start.ts should build preferredModel");
|
|
99
100
|
|
|
100
|
-
const preferredBlock = source
|
|
101
|
+
const preferredBlock = extractSourceRegion(source, "const preferredModel = ");
|
|
101
102
|
assert.ok(
|
|
102
103
|
preferredBlock.includes("sessionProviderIsCustom"),
|
|
103
104
|
"preferredModel must be gated on sessionProviderIsCustom so PREFERENCES.md is skipped for custom providers",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {createTestContext, extractSourceRegion } from "./test-helpers.ts";
|
|
5
5
|
|
|
6
6
|
const { assertTrue, report } = createTestContext();
|
|
7
7
|
|
|
@@ -13,7 +13,7 @@ console.log("\n=== #3822: worktree bootstrap uses project DB path ===");
|
|
|
13
13
|
const dbLifecycleIdx = src.indexOf("// ── DB lifecycle ──");
|
|
14
14
|
assertTrue(dbLifecycleIdx > 0, "auto-start.ts has a DB lifecycle section");
|
|
15
15
|
|
|
16
|
-
const dbLifecycleRegion = dbLifecycleIdx > 0 ? src
|
|
16
|
+
const dbLifecycleRegion = dbLifecycleIdx > 0 ? extractSourceRegion(src, "// ── DB lifecycle ──") : "";
|
|
17
17
|
|
|
18
18
|
assertTrue(
|
|
19
19
|
dbLifecycleRegion.includes("const gsdDbPath = resolveProjectRootDbPath(s.basePath);"),
|
|
@@ -2,6 +2,7 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
5
6
|
|
|
6
7
|
const autoSrc = readFileSync(join(import.meta.dirname, "..", "auto.ts"), "utf-8");
|
|
7
8
|
const phasesSrc = readFileSync(join(import.meta.dirname, "..", "auto", "phases.ts"), "utf-8");
|
|
@@ -20,7 +21,7 @@ test("stopAuto restores original thinking level", () => {
|
|
|
20
21
|
test("runUnitPhase threads captured thinking level into selectAndApplyModel", () => {
|
|
21
22
|
const callIdx = phasesSrc.indexOf("deps.selectAndApplyModel(");
|
|
22
23
|
assert.ok(callIdx > -1, "phases.ts should call selectAndApplyModel");
|
|
23
|
-
const callBlock = phasesSrc.
|
|
24
|
+
const callBlock = extractSourceRegion(phasesSrc, "deps.selectAndApplyModel(");
|
|
24
25
|
assert.ok(
|
|
25
26
|
callBlock.includes("s.autoModeStartThinkingLevel"),
|
|
26
27
|
"runUnitPhase should pass autoModeStartThinkingLevel to selectAndApplyModel",
|
|
@@ -30,7 +31,7 @@ test("runUnitPhase threads captured thinking level into selectAndApplyModel", ()
|
|
|
30
31
|
test("hook model override preserves captured thinking level", () => {
|
|
31
32
|
const hookIdx = phasesSrc.indexOf("const hookModelOverride = sidecarItem?.model ?? iterData.hookModelOverride;");
|
|
32
33
|
assert.ok(hookIdx > -1, "phases.ts should include hook model override handling");
|
|
33
|
-
const hookBlock = phasesSrc
|
|
34
|
+
const hookBlock = extractSourceRegion(phasesSrc, "const hookModelOverride = sidecarItem?.model ?? iterData.hookModelOverride;");
|
|
34
35
|
assert.ok(
|
|
35
36
|
hookBlock.includes("pi.setThinkingLevel(s.autoModeStartThinkingLevel)"),
|
|
36
37
|
"hook model override should re-apply captured thinking level after setModel",
|