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.
Files changed (128) hide show
  1. package/dist/resources/extensions/gsd/auto/session.js +6 -0
  2. package/dist/resources/extensions/gsd/auto-post-unit.js +79 -0
  3. package/dist/resources/extensions/gsd/auto-prompts.js +48 -7
  4. package/dist/resources/extensions/gsd/auto-start.js +62 -3
  5. package/dist/resources/extensions/gsd/auto.js +34 -0
  6. package/dist/resources/extensions/gsd/context-store.js +23 -7
  7. package/dist/resources/extensions/gsd/forensics.js +106 -0
  8. package/dist/resources/extensions/gsd/preferences-validation.js +23 -0
  9. package/dist/resources/extensions/gsd/prompt-cache-optimizer.js +4 -0
  10. package/dist/resources/extensions/gsd/slice-cadence.js +238 -0
  11. package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -2
  12. package/dist/resources/extensions/gsd/worktree-manager.js +51 -0
  13. package/dist/resources/extensions/gsd/worktree-resolver.js +86 -7
  14. package/dist/resources/extensions/gsd/worktree-telemetry.js +198 -0
  15. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  16. package/dist/web/standalone/.next/BUILD_ID +1 -1
  17. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  18. package/dist/web/standalone/.next/build-manifest.json +2 -2
  19. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  20. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.html +1 -1
  37. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  44. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  45. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  46. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  47. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  48. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  49. package/package.json +1 -1
  50. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -5
  51. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  52. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  53. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +30 -12
  54. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  55. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +49 -3
  56. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +48 -9
  57. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  58. package/src/resources/extensions/gsd/auto/session.ts +7 -0
  59. package/src/resources/extensions/gsd/auto-post-unit.ts +81 -0
  60. package/src/resources/extensions/gsd/auto-prompts.ts +59 -7
  61. package/src/resources/extensions/gsd/auto-start.ts +64 -2
  62. package/src/resources/extensions/gsd/auto.ts +37 -0
  63. package/src/resources/extensions/gsd/context-store.ts +25 -8
  64. package/src/resources/extensions/gsd/forensics.ts +118 -1
  65. package/src/resources/extensions/gsd/git-service.ts +16 -0
  66. package/src/resources/extensions/gsd/journal.ts +11 -1
  67. package/src/resources/extensions/gsd/preferences-validation.ts +21 -0
  68. package/src/resources/extensions/gsd/prompt-cache-optimizer.ts +4 -0
  69. package/src/resources/extensions/gsd/slice-cadence.ts +299 -0
  70. package/src/resources/extensions/gsd/tests/artifacts-table-preserved-on-cache-invalidate.test.ts +2 -1
  71. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +5 -8
  72. package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +4 -1
  73. package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +12 -9
  74. package/src/resources/extensions/gsd/tests/auto-start-clean-runtime-db-gated.test.ts +3 -2
  75. package/src/resources/extensions/gsd/tests/auto-start-cold-db-bootstrap.test.ts +2 -2
  76. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +4 -3
  77. package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +2 -2
  78. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +3 -2
  79. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +3 -2
  80. package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -1
  81. package/src/resources/extensions/gsd/tests/canonical-milestone-root.test.ts +108 -0
  82. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +2 -1
  83. package/src/resources/extensions/gsd/tests/context-store.test.ts +79 -0
  84. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +2 -1
  85. package/src/resources/extensions/gsd/tests/discuss-slice-structured-questions.test.ts +2 -1
  86. package/src/resources/extensions/gsd/tests/discuss-tool-scope-leak.test.ts +2 -1
  87. package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +4 -3
  88. package/src/resources/extensions/gsd/tests/empty-content-abort-loop.test.ts +4 -3
  89. package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +2 -2
  90. package/src/resources/extensions/gsd/tests/forensics-hook-key-parse.test.ts +2 -1
  91. package/src/resources/extensions/gsd/tests/forensics-worktree-telemetry.test.ts +145 -0
  92. package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +6 -1
  93. package/src/resources/extensions/gsd/tests/import-done-milestones.test.ts +2 -1
  94. package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +2 -1
  95. package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +9 -3
  96. package/src/resources/extensions/gsd/tests/knowledge.test.ts +93 -1
  97. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +10 -3
  98. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +2 -1
  99. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +59 -2
  100. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +4 -1
  101. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +2 -1
  102. package/src/resources/extensions/gsd/tests/prompt-cache-optimizer.test.ts +12 -0
  103. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +15 -4
  104. package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +3 -2
  105. package/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts +4 -5
  106. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +6 -3
  107. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +3 -2
  108. package/src/resources/extensions/gsd/tests/slice-cadence.test.ts +242 -0
  109. package/src/resources/extensions/gsd/tests/slice-context-injection.test.ts +3 -2
  110. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +2 -1
  111. package/src/resources/extensions/gsd/tests/stop-auto-race-null-unit.test.ts +3 -3
  112. package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +7 -6
  113. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +4 -3
  114. package/src/resources/extensions/gsd/tests/test-helpers.test.ts +147 -0
  115. package/src/resources/extensions/gsd/tests/test-helpers.ts +140 -0
  116. package/src/resources/extensions/gsd/tests/token-profile.test.ts +8 -1
  117. package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +6 -5
  118. package/src/resources/extensions/gsd/tests/worktree-health-monorepo.test.ts +2 -2
  119. package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +2 -2
  120. package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +2 -2
  121. package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +210 -0
  122. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -3
  123. package/src/resources/extensions/gsd/tools/validate-milestone.ts +8 -2
  124. package/src/resources/extensions/gsd/worktree-manager.ts +53 -0
  125. package/src/resources/extensions/gsd/worktree-resolver.ts +96 -9
  126. package/src/resources/extensions/gsd/worktree-telemetry.ts +322 -0
  127. /package/dist/web/standalone/.next/static/{vidAVJkURvTJ0_V2-64ro → Cev5xrAYA3ZGTRLyjR2fX}/_buildManifest.js +0 -0
  128. /package/dist/web/standalone/.next/static/{vidAVJkURvTJ0_V2-64ro → Cev5xrAYA3ZGTRLyjR2fX}/_ssgManifest.js +0 -0
@@ -15,7 +15,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, realpathSy
15
15
  import { join } from 'node:path';
16
16
  import { tmpdir } from 'node:os';
17
17
  import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
18
- import { inlineGsdRootFile } from '../auto-prompts.ts';
18
+ import { inlineGsdRootFile, inlineKnowledgeBudgeted } from '../auto-prompts.ts';
19
19
  import { appendKnowledge } from '../files.ts';
20
20
  import { loadKnowledgeBlock } from '../bootstrap/system-context.ts';
21
21
 
@@ -248,3 +248,95 @@ test('loadKnowledgeBlock: reports globalSizeKb above 4KB threshold', () => {
248
248
 
249
249
  rmSync(tmp, { recursive: true, force: true });
250
250
  });
251
+
252
+ // ─── inlineKnowledgeBudgeted — issue #4719 ─────────────────────────────────
253
+ // Milestone-phase prompts must not inject the full KNOWLEDGE.md. The budgeted
254
+ // helper scopes by milestone-level keywords and caps the injected size.
255
+
256
+ test('inlineKnowledgeBudgeted: returns scoped H3 entries for single-H2 file', async () => {
257
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
258
+ const gsdDir = join(tmp, '.gsd');
259
+ mkdirSync(gsdDir, { recursive: true });
260
+
261
+ const content = `# Project Knowledge
262
+
263
+ ## Patterns
264
+
265
+ ### Database: prepared statements
266
+ Always use prepared statements with SQLite.
267
+
268
+ ### API: versioned paths
269
+ Use /v1/resource style versioning.
270
+
271
+ ### Testing: node:test
272
+ Prefer node:test over external frameworks.
273
+ `;
274
+ writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), content);
275
+
276
+ const result = await inlineKnowledgeBudgeted(tmp, ['database']);
277
+ assert.ok(result !== null, 'should return content');
278
+ assert.ok(result!.includes('Database: prepared statements'), 'includes matching H3');
279
+ assert.ok(!result!.includes('API: versioned paths'), 'excludes non-matching H3');
280
+
281
+ rmSync(tmp, { recursive: true, force: true });
282
+ });
283
+
284
+ test('inlineKnowledgeBudgeted: caps payload below budget for large files', async () => {
285
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
286
+ const gsdDir = join(tmp, '.gsd');
287
+ mkdirSync(gsdDir, { recursive: true });
288
+
289
+ // Build a 200KB KNOWLEDGE with 500 H3 entries all matching 'shared'
290
+ const entries = Array.from({ length: 500 }, (_, i) =>
291
+ `### Entry ${i}: shared topic\n${'filler text '.repeat(30)}\n`,
292
+ ).join('\n');
293
+ const content = `# Project Knowledge\n\n## Patterns\n\n${entries}`;
294
+ writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), content);
295
+
296
+ const BUDGET_CHARS = 30_000;
297
+ const result = await inlineKnowledgeBudgeted(tmp, ['shared'], { maxChars: BUDGET_CHARS });
298
+ assert.ok(result !== null, 'should return content');
299
+ // Allow some overhead for header formatting, but must stay close to budget
300
+ assert.ok(
301
+ result!.length <= BUDGET_CHARS + 500,
302
+ `payload ${result!.length} chars should be <= budget ${BUDGET_CHARS} (+overhead)`,
303
+ );
304
+ // Far smaller than the raw file
305
+ assert.ok(
306
+ result!.length < content.length / 4,
307
+ `payload should be much smaller than full content (${content.length} chars)`,
308
+ );
309
+ assert.match(
310
+ result!,
311
+ /\[\.\.\.truncated \d+ chars; rerun with narrower scope if needed\]/,
312
+ 'should include truncation note when budget is exceeded',
313
+ );
314
+
315
+ rmSync(tmp, { recursive: true, force: true });
316
+ });
317
+
318
+ test('inlineKnowledgeBudgeted: returns null when no KNOWLEDGE.md exists', async () => {
319
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
320
+ const gsdDir = join(tmp, '.gsd');
321
+ mkdirSync(gsdDir, { recursive: true });
322
+
323
+ const result = await inlineKnowledgeBudgeted(tmp, ['database']);
324
+ assert.strictEqual(result, null);
325
+
326
+ rmSync(tmp, { recursive: true, force: true });
327
+ });
328
+
329
+ test('inlineKnowledgeBudgeted: returns null when no entries match', async () => {
330
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
331
+ const gsdDir = join(tmp, '.gsd');
332
+ mkdirSync(gsdDir, { recursive: true });
333
+ writeFileSync(
334
+ join(gsdDir, 'KNOWLEDGE.md'),
335
+ '# Project Knowledge\n\n## Patterns\n\n### Database\nuse it\n',
336
+ );
337
+
338
+ const result = await inlineKnowledgeBudgeted(tmp, ['nonexistent']);
339
+ assert.strictEqual(result, null);
340
+
341
+ rmSync(tmp, { recursive: true, force: true });
342
+ });
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
- import { createTestContext } from "./test-helpers.ts";
13
+ import {createTestContext, extractSourceRegion } from "./test-helpers.ts";
14
14
 
15
15
  const { assertTrue, report } = createTestContext();
16
16
 
@@ -27,7 +27,14 @@ console.log("\n=== #2330: Merge conflict stops auto loop ===");
27
27
  const methodStart = resolverSrc.indexOf("Worktree-mode merge:");
28
28
  assertTrue(methodStart > 0, "worktree-resolver has _mergeWorktreeMode method");
29
29
 
30
- const methodBody = resolverSrc.slice(methodStart, methodStart + 6000);
30
+ // Slice from the _mergeWorktreeMode docblock to the next method boundary
31
+ // (Branch-mode merge:) so that docblock/body growth doesn't silently drop
32
+ // the `throw err` out of the search window.
33
+ const methodEnd = resolverSrc.indexOf("Branch-mode merge:", methodStart);
34
+ const methodBody = resolverSrc.slice(
35
+ methodStart,
36
+ methodEnd > 0 ? methodEnd : methodStart + 8000,
37
+ );
31
38
  const rethrowsConflict =
32
39
  methodBody.includes("MergeConflictError") &&
33
40
  methodBody.includes("throw err");
@@ -51,7 +58,7 @@ const instanceofIdx = phasesSrc.indexOf("instanceof MergeConflictError");
51
58
  assertTrue(instanceofIdx > 0, "auto/phases.ts has instanceof MergeConflictError check");
52
59
 
53
60
  if (instanceofIdx > 0) {
54
- const afterHandler = phasesSrc.slice(instanceofIdx, instanceofIdx + 500);
61
+ const afterHandler = extractSourceRegion(phasesSrc, "instanceof MergeConflictError");
55
62
  const stopsLoop =
56
63
  afterHandler.includes("stopAuto") ||
57
64
  afterHandler.includes('action: "break"') ||
@@ -17,6 +17,7 @@ import { join } from "node:path";
17
17
  import { tmpdir } from "node:os";
18
18
  import { execFileSync } from "node:child_process";
19
19
  import { nativeIsRepo, nativeCommit, nativeResetHard } from "../native-git-bridge.js";
20
+ import { extractSourceRegion } from "./test-helpers.ts";
20
21
 
21
22
  // ─── Static analysis ──────────────────────────────────────────────────────
22
23
  // Verify the fallback paths of the three affected functions do not call the
@@ -28,7 +29,7 @@ const SRC_PATH = join(import.meta.dirname, "..", "native-git-bridge.ts");
28
29
  function extractFunctionBody(src: string, fnName: string): string {
29
30
  const idx = src.indexOf(`export function ${fnName}`);
30
31
  if (idx === -1) throw new Error(`${fnName} not found in source`);
31
- return src.slice(idx, idx + 1500);
32
+ return extractSourceRegion(src, `export function ${fnName}`);
32
33
  }
33
34
 
34
35
  function hasRawExecSync(body: string): boolean {
@@ -107,7 +107,8 @@ describe("auditOrphanedMilestoneBranches", () => {
107
107
  assert.ok(branches.includes("milestone/M001"), "unmerged branch must be preserved");
108
108
  });
109
109
 
110
- test("skips active (non-complete) milestone branches", () => {
110
+ test("skips active milestone branch with no commits ahead of main (nothing to recover)", () => {
111
+ // Branch created from main with zero divergence — no live work, nothing to warn about.
111
112
  run("git branch milestone/M001", dir);
112
113
  insertMilestone({ id: "M001", title: "Test", status: "active" });
113
114
 
@@ -116,11 +117,67 @@ describe("auditOrphanedMilestoneBranches", () => {
116
117
  assert.deepStrictEqual(result.recovered, []);
117
118
  assert.deepStrictEqual(result.warnings, []);
118
119
 
119
- // Branch should still exist
120
+ // Branch should still exist (data safety — user may intend to use it)
120
121
  const branches = run("git branch --list milestone/M001", dir);
121
122
  assert.ok(branches.includes("milestone/M001"), "active milestone branch should be preserved");
122
123
  });
123
124
 
125
+ test("#4762 — warns about in-progress milestone with unmerged commits ahead of main", () => {
126
+ // Simulates the primary #4761 scenario: auto-mode was interrupted mid-milestone.
127
+ // DB status = active/in_progress, branch has real work, main is behind.
128
+ run("git checkout -b milestone/M001", dir);
129
+ writeFileSync(join(dir, "feature.txt"), "in-progress work\n");
130
+ run("git add feature.txt", dir);
131
+ run("git commit -m \"in-progress work on M001\"", dir);
132
+ run("git checkout main", dir);
133
+
134
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
135
+
136
+ const result = auditOrphanedMilestoneBranches(dir, "worktree");
137
+
138
+ // Must NOT recover/delete (data safety — work is live)
139
+ assert.deepStrictEqual(result.recovered, [], "must not delete a branch with live in-progress work");
140
+
141
+ // Must surface a warning so the user knows the worktree holds uncollapsed work
142
+ assert.ok(result.warnings.length > 0, "should warn about in-progress orphan");
143
+ assert.ok(
144
+ result.warnings.some(w => w.includes("milestone/M001") && w.includes("in-progress")),
145
+ `warning should mention milestone/M001 and in-progress state; got: ${JSON.stringify(result.warnings)}`,
146
+ );
147
+
148
+ // Branch must still exist
149
+ const branches = run("git branch --list milestone/M001", dir);
150
+ assert.ok(branches.includes("milestone/M001"), "in-progress branch must be preserved");
151
+ });
152
+
153
+ test("#4762 — also surfaces worktree directory for in-progress orphan when present", () => {
154
+ // In-progress + unmerged + physical worktree directory — the full primary scenario.
155
+ run("git checkout -b milestone/M001", dir);
156
+ writeFileSync(join(dir, "feature.txt"), "in-progress work\n");
157
+ run("git add feature.txt", dir);
158
+ run("git commit -m \"in-progress work on M001\"", dir);
159
+ run("git checkout main", dir);
160
+
161
+ // Simulate a leftover worktree directory
162
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
163
+ mkdirSync(wtDir, { recursive: true });
164
+ writeFileSync(join(wtDir, ".git"), `gitdir: ${join(dir, ".git", "worktrees", "M001")}\n`);
165
+
166
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
167
+
168
+ const result = auditOrphanedMilestoneBranches(dir, "worktree");
169
+
170
+ // Must preserve everything for data safety
171
+ assert.deepStrictEqual(result.recovered, [], "must not touch worktree or branch with live work");
172
+ assert.ok(existsSync(wtDir), "worktree directory must be preserved");
173
+
174
+ // Warning should mention the worktree path so the user can find the work
175
+ assert.ok(
176
+ result.warnings.some(w => w.includes(".gsd/worktrees/M001") || w.includes("worktree")),
177
+ `warning should reference the worktree location; got: ${JSON.stringify(result.warnings)}`,
178
+ );
179
+ });
180
+
124
181
  test("cleans up orphaned worktree directory for merged milestone", () => {
125
182
  // Create milestone branch (merged — same as main)
126
183
  run("git branch milestone/M001", dir);
@@ -12,6 +12,7 @@ import { tmpdir } from "node:os";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
14
  import { resolveDispatch } from "../auto-dispatch.ts";
15
+ import { extractSourceRegion } from "./test-helpers.ts";
15
16
 
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
18
 
@@ -79,7 +80,9 @@ test("dispatch: parallel-research-slices requires 2+ slices", () => {
79
80
 
80
81
  test("dispatch: parallel-research-slices respects skip_research", () => {
81
82
  const ruleIdx = dispatchSrc.indexOf("parallel-research-slices");
82
- const ruleBlock = dispatchSrc.slice(ruleIdx, ruleIdx + 500);
83
+ // Pin to the located occurrence — if "parallel-research-slices" appears
84
+ // more than once in the source, fromIdx keeps the anchor deterministic.
85
+ const ruleBlock = extractSourceRegion(dispatchSrc, "parallel-research-slices", { fromIdx: ruleIdx });
83
86
  assert.ok(
84
87
  ruleBlock.includes("skip_research") || dispatchSrc.slice(ruleIdx - 300, ruleIdx).includes("skip_research"),
85
88
  "rule should check skip_research preference",
@@ -12,6 +12,7 @@ import assert from "node:assert/strict";
12
12
  import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, readdirSync, rmSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { tmpdir } from "node:os";
15
+ import { extractSourceRegion } from "./test-helpers.ts";
15
16
 
16
17
  test("#2684: preferences files are NOT in ROOT_STATE_FILES (forward-only sync)", () => {
17
18
  const srcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
@@ -49,7 +50,7 @@ test("copyPlanningArtifacts prefers canonical PREFERENCES.md with lowercase fall
49
50
  assert.ok(fnIdx !== -1, "copyPlanningArtifacts function exists");
50
51
 
51
52
  // Extract function body (up to the next top-level function)
52
- const fnBody = src.slice(fnIdx, fnIdx + 2200);
53
+ const fnBody = extractSourceRegion(src, "function copyPlanningArtifacts");
53
54
 
54
55
  assert.ok(
55
56
  fnBody.includes("PROJECT_PREFERENCES_FILE") && fnBody.includes("LEGACY_PROJECT_PREFERENCES_FILE"),
@@ -64,6 +64,18 @@ describe("prompt-cache-optimizer: classifySection", () => {
64
64
  assert.equal(classifySection("overrides"), "semi-static");
65
65
  });
66
66
 
67
+ // Regression: issue #4719 — KNOWLEDGE falls through to dynamic default.
68
+ // Knowledge content is reused across all tasks within a milestone, so it
69
+ // must be classified as semi-static to qualify for prefix caching when the
70
+ // cache optimizer is wired into the prompt path.
71
+ it("classifies knowledge as semi-static (issue #4719)", () => {
72
+ assert.equal(classifySection("knowledge"), "semi-static");
73
+ });
74
+
75
+ it("classifies project-knowledge as semi-static (issue #4719)", () => {
76
+ assert.equal(classifySection("project-knowledge"), "semi-static");
77
+ });
78
+
67
79
  it("classifies task-plan as dynamic", () => {
68
80
  assert.equal(classifySection("task-plan"), "dynamic");
69
81
  });
@@ -13,6 +13,7 @@ import assert from 'node:assert/strict';
13
13
  import { readFileSync } from 'node:fs';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { dirname, join } from 'node:path';
16
+ import { extractSourceRegion } from "./test-helpers.ts";
16
17
 
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = dirname(__filename);
@@ -69,11 +70,21 @@ describe('register-extension _gsdEpipeGuard (#3696)', () => {
69
70
 
70
71
  describe('register-hooks session_before_compact (#3696)', () => {
71
72
  test('session_before_compact only checks isAutoActive', () => {
72
- // Extract the session_before_compact handler
73
- const compactIdx = registerHooksSrc.indexOf('session_before_compact');
73
+ // Anchor on the full registration token rather than the bare event name —
74
+ // prevents matching unrelated substring occurrences.
75
+ const compactIdx = registerHooksSrc.indexOf('pi.on("session_before_compact"');
74
76
  assert.ok(compactIdx > -1, 'session_before_compact hook should exist');
75
- // The first check in the handler should be isAutoActive(), not isAutoPaused()
76
- const afterCompact = registerHooksSrc.slice(compactIdx, compactIdx + 300);
77
+ // The first check in the handler should be isAutoActive(), not isAutoPaused().
78
+ // Bound the region to this single handler — register-hooks.ts contains
79
+ // multiple pi.on("session_before_compact") handlers and a later handler
80
+ // legitimately references isAutoPaused.
81
+ const afterCompact = extractSourceRegion(
82
+ registerHooksSrc,
83
+ 'pi.on("session_before_compact"',
84
+ 'pi.on("',
85
+ // NB: endAnchor search starts AFTER the startAnchor, so the next
86
+ // pi.on("... matches the subsequent handler rather than this one.
87
+ );
77
88
  assert.match(afterCompact, /isAutoActive\(\)/,
78
89
  'session_before_compact should check isAutoActive()');
79
90
  // Should NOT block compaction when paused
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
6
6
 
7
7
  import { deriveState } from "../state.js";
8
8
  import { buildExistingMilestonesContext } from "../guided-flow.js";
9
+ import { extractSourceRegion } from "./test-helpers.ts";
9
10
 
10
11
  describe('queue-draft-detection', () => {
11
12
  test('draft and context milestone detection', async () => {
@@ -68,7 +69,7 @@ describe('queue-draft-detection', () => {
68
69
 
69
70
  // both files: CONTEXT.md wins, no draft label
70
71
  const m003Idx = context.indexOf("M003:");
71
- const m003Section = context.slice(m003Idx, m003Idx + 500);
72
+ const m003Section = extractSourceRegion(context, "M003:");
72
73
  assert.ok(
73
74
  m003Section.includes("**Context:**"),
74
75
  "M003 (both files) should use 'Context:' label (CONTEXT.md wins)",
@@ -84,7 +85,7 @@ describe('queue-draft-detection', () => {
84
85
 
85
86
  // neither file: no context section
86
87
  const m004Idx = context.indexOf("M004:");
87
- const m004Section = context.slice(m004Idx, m004Idx + 500);
88
+ const m004Section = extractSourceRegion(context, "M004:");
88
89
  assert.ok(
89
90
  !m004Section.includes("**Context:**"),
90
91
  "M004 (neither file) should not have Context: label",
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join, dirname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { extractSourceRegion } from "./test-helpers.ts";
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
 
@@ -28,7 +29,7 @@ describe("queued-discuss-fast-path", () => {
28
29
  const fnStart = source.indexOf("async function dispatchDiscussForMilestone(");
29
30
  assert.ok(fnStart > 0, "dispatchDiscussForMilestone must exist");
30
31
  const fnEnd = source.indexOf("\nasync function ", fnStart + 1);
31
- const fnBody = fnEnd > 0 ? source.slice(fnStart, fnEnd) : source.slice(fnStart, fnStart + 2000);
32
+ const fnBody = extractSourceRegion(source, "async function dispatchDiscussForMilestone(");
32
33
  assert.ok(
33
34
  fnBody.includes("fastPathInstruction"),
34
35
  "dispatchDiscussForMilestone must compute fastPathInstruction",
@@ -61,8 +62,7 @@ describe("queued-discuss-fast-path", () => {
61
62
  const source = guidedFlowSrc();
62
63
  const fnStart = source.indexOf("async function showDiscussQueuedMilestone(");
63
64
  assert.ok(fnStart > 0, "showDiscussQueuedMilestone must exist");
64
- const fnEnd = source.indexOf("\nasync function ", fnStart + 1);
65
- const fnBody = fnEnd > 0 ? source.slice(fnStart, fnEnd) : source.slice(fnStart, fnStart + 3000);
65
+ const fnBody = extractSourceRegion(source, "async function showDiscussQueuedMilestone(", "\nasync function ");
66
66
  assert.ok(
67
67
  fnBody.includes("hasDraft"),
68
68
  "showDiscussQueuedMilestone must check hasDraft",
@@ -81,8 +81,7 @@ describe("queued-discuss-fast-path", () => {
81
81
  const source = guidedFlowSrc();
82
82
  const fnStart = source.indexOf("async function showDiscussQueuedMilestone(");
83
83
  assert.ok(fnStart > 0, "showDiscussQueuedMilestone must exist");
84
- const fnEnd = source.indexOf("\nasync function ", fnStart + 1);
85
- const fnBody = fnEnd > 0 ? source.slice(fnStart, fnEnd) : source.slice(fnStart, fnStart + 3000);
84
+ const fnBody = extractSourceRegion(source, "async function showDiscussQueuedMilestone(", "\nasync function ");
86
85
  assert.ok(
87
86
  fnBody.includes("let fastPath = hasDraft"),
88
87
  "showDiscussQueuedMilestone must set fastPath = hasDraft so draft presence auto-enables fast path",
@@ -13,6 +13,7 @@ import { describe, it } from 'node:test'
13
13
  import assert from 'node:assert/strict'
14
14
  import { readFileSync } from 'node:fs'
15
15
  import { resolve } from 'node:path'
16
+ import { extractSourceRegion } from "./test-helpers.ts";
16
17
 
17
18
  const src = readFileSync(
18
19
  resolve(process.cwd(), 'src', 'resources', 'extensions', 'gsd', 'guided-flow.ts'),
@@ -37,7 +38,7 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
37
38
  assert.ok(discussCheck !== -1)
38
39
 
39
40
  // Look for savedTools assignment within the discuss block
40
- const blockAfter = src.slice(discussCheck, discussCheck + 500)
41
+ const blockAfter = extractSourceRegion(src, 'if (unitType?.startsWith("discuss-"))')
41
42
  assert.ok(
42
43
  blockAfter.includes('savedTools = currentTools'),
43
44
  'savedTools must be assigned from currentTools inside the discuss block',
@@ -55,8 +56,10 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
55
56
  const sendMsg = src.indexOf('triggerTurn: true', savedToolsAssign)
56
57
  assert.ok(sendMsg !== -1, 'discuss-flow sendMessage with triggerTurn must exist after savedTools capture')
57
58
 
58
- // After sendMessage, savedTools should be restored via setActiveTools
59
- const afterSend = src.slice(sendMsg, sendMsg + 500)
59
+ // After sendMessage, savedTools should be restored via setActiveTools.
60
+ // Use fromIdx to anchor at the discuss-flow sendMessage, not the first
61
+ // triggerTurn: true occurrence in the file.
62
+ const afterSend = extractSourceRegion(src, 'triggerTurn: true', { fromIdx: savedToolsAssign })
60
63
  assert.ok(
61
64
  afterSend.includes('if (savedTools)'),
62
65
  'savedTools restoration guard must exist after sendMessage',
@@ -11,6 +11,7 @@ import assert from "node:assert/strict";
11
11
  import { readFileSync } from "node:fs";
12
12
  import { join, dirname } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
+ import { extractSourceRegion } from "./test-helpers.ts";
14
15
 
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
17
  const SESSION_TS_PATH = join(__dirname, "..", "auto", "session.ts");
@@ -52,7 +53,7 @@ test("SidecarItem type is exported from session.ts", () => {
52
53
  test("SidecarItem has required kind field with hook/triage/quick-task union", () => {
53
54
  const source = getSessionTsSource();
54
55
  const ifaceIdx = source.indexOf("export interface SidecarItem");
55
- const ifaceBlock = source.slice(ifaceIdx, ifaceIdx + 500);
56
+ const ifaceBlock = extractSourceRegion(source, "export interface SidecarItem");
56
57
  assert.ok(
57
58
  ifaceBlock.includes('"hook"') && ifaceBlock.includes('"triage"') && ifaceBlock.includes('"quick-task"'),
58
59
  "SidecarItem.kind must be a union of 'hook' | 'triage' | 'quick-task'",
@@ -77,7 +78,7 @@ test("AutoSession resets sidecarQueue in reset()", () => {
77
78
  const source = getSessionTsSource();
78
79
  const resetIdx = source.indexOf("reset(): void");
79
80
  assert.ok(resetIdx > -1, "AutoSession must have a reset() method");
80
- const resetBlock = source.slice(resetIdx, resetIdx + 3000);
81
+ const resetBlock = extractSourceRegion(source, "reset(): void");
81
82
  assert.ok(
82
83
  resetBlock.includes("sidecarQueue"),
83
84
  "reset() must clear sidecarQueue",