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
@@ -105,6 +105,11 @@ export class AutoSession {
105
105
  /** Set to true after phases.ts successfully calls mergeAndExit, so that
106
106
  * stopAuto does not attempt the same merge a second time (#2645). */
107
107
  milestoneMergedInPhases = false;
108
+ // #4765 — slice-cadence collapse: main-branch SHAs at the moment each
109
+ // milestone's first slice merge began. Used by resquashMilestoneOnMain at
110
+ // milestone completion to collapse N slice commits into one. Cleared when
111
+ // the milestone finishes (or resquash runs).
112
+ milestoneStartShas = new Map();
108
113
  // ── Dispatch circuit breakers ──────────────────────────────────────
109
114
  rewriteAttemptCount = 0;
110
115
  /** Tracks consecutive bootstrap attempts that found phase === "complete".
@@ -221,6 +226,7 @@ export class AutoSession {
221
226
  this.lastGitActionStatus = null;
222
227
  this.isolationDegraded = false;
223
228
  this.milestoneMergedInPhases = false;
229
+ this.milestoneStartShas = new Map();
224
230
  this.checkpointSha = null;
225
231
  // Signal handler
226
232
  this.sigtermHandler = null;
@@ -521,6 +521,85 @@ export async function postUnitPreVerification(pctx, opts) {
521
521
  clearReactiveState(s.basePath, mid, sid);
522
522
  }
523
523
  });
524
+ // #4765 — slice-cadence collapse. When `git.collapse_cadence: "slice"`
525
+ // is set, squash-merge the slice's commits from the milestone branch
526
+ // onto main right here, so orphan risk shrinks from milestone-size to
527
+ // slice-size. Only runs in worktree isolation mode — the feature needs
528
+ // a milestone branch to squash from.
529
+ let sliceMergeStopped = false;
530
+ await runSafely("postUnit", "slice-cadence-merge", async () => {
531
+ const prefsResult = loadEffectiveGSDPreferences(s.basePath);
532
+ const prefs = prefsResult?.preferences;
533
+ const { getCollapseCadence, mergeSliceToMain } = await import("./slice-cadence.js");
534
+ if (getCollapseCadence(prefs) !== "slice")
535
+ return;
536
+ if (prefs?.git?.isolation !== "worktree")
537
+ return;
538
+ if (s.isolationDegraded)
539
+ return;
540
+ const projectRoot = s.originalBasePath || s.basePath;
541
+ const { milestone: mid, slice: sid } = parseUnitId(unit.id);
542
+ if (!mid || !sid)
543
+ return;
544
+ // Record the milestone start SHA before the first slice merge, so
545
+ // resquashMilestoneOnMain has a target at milestone completion.
546
+ // Resolve main branch dynamically — hard-coding "main" breaks repos
547
+ // that use "master" or a custom default branch.
548
+ if (!s.milestoneStartShas.has(mid)) {
549
+ try {
550
+ const { nativeDetectMainBranch } = await import("./native-git-bridge.js");
551
+ const mainBranch = nativeDetectMainBranch(projectRoot);
552
+ const { execFileSync } = await import("node:child_process");
553
+ const sha = execFileSync("git", ["rev-parse", mainBranch], {
554
+ cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8",
555
+ }).trim();
556
+ if (sha)
557
+ s.milestoneStartShas.set(mid, sha);
558
+ }
559
+ catch (err) {
560
+ logWarning("engine", `slice-cadence: failed to record milestone start SHA: ${err instanceof Error ? err.message : String(err)}`);
561
+ }
562
+ }
563
+ try {
564
+ const result = mergeSliceToMain(projectRoot, mid, sid);
565
+ if (result.skipped) {
566
+ logWarning("engine", `slice-cadence: merge skipped for ${sid} — ${result.skippedReason}`);
567
+ return;
568
+ }
569
+ ctx.ui.notify(`slice-cadence: ${sid} merged to main (${result.durationMs}ms).`, "info");
570
+ }
571
+ catch (err) {
572
+ const { MergeConflictError } = await import("./git-service.js");
573
+ if (err instanceof MergeConflictError) {
574
+ ctx.ui.notify(`slice-cadence merge conflict in ${sid}: ${err.conflictedFiles.join(", ")}. ` +
575
+ `Resolve manually on main and run \`/gsd auto\` to resume.`, "error");
576
+ // Stop auto AND signal the outer postUnit flow to exit early.
577
+ // Without the flag, subsequent hooks (triage, rogue detection,
578
+ // DB writes) would keep running against a conflicted main
579
+ // checkout after the loop was already told to stop.
580
+ const { stopAuto } = await import("./auto.js");
581
+ await stopAuto(ctx, undefined, `slice-merge-conflict on ${sid}`);
582
+ sliceMergeStopped = true;
583
+ return;
584
+ }
585
+ logError("engine", `slice-cadence merge failed for ${sid}`, {
586
+ error: err instanceof Error ? err.message : String(err),
587
+ });
588
+ // Non-conflict failures (dirty main, rev-walk error, etc.) can
589
+ // leave the checkout in an unexpected state. Stop auto-mode so
590
+ // the next slice doesn't dispatch on top of it.
591
+ const { stopAuto } = await import("./auto.js");
592
+ await stopAuto(ctx, undefined, `slice-merge-error on ${sid}`);
593
+ sliceMergeStopped = true;
594
+ }
595
+ });
596
+ // Exit early after stopAuto so the rest of post-unit processing
597
+ // (triage, rogue detection, hook dispatch, DB writes) doesn't run
598
+ // against a conflicted main checkout. Return "dispatched" to match
599
+ // the convention used by other stop/pauseAuto paths in this function
600
+ // (see signal handling earlier: stop/pause also return "dispatched").
601
+ if (sliceMergeStopped)
602
+ return "dispatched";
524
603
  }
525
604
  // Post-triage: execute actionable resolutions
526
605
  if (s.currentUnit.type === "triage-captures") {
@@ -446,6 +446,40 @@ export async function inlineKnowledgeScoped(base, keywords) {
446
446
  return null;
447
447
  return `### Project Knowledge (scoped)\nSource: \`${relGsdRootFile("KNOWLEDGE")}\`\n\n${scoped.trim()}`;
448
448
  }
449
+ /**
450
+ * Budget-capped knowledge inline for milestone-level prompt assembly.
451
+ *
452
+ * Addresses issue #4719: the six milestone-phase prompts (research-milestone,
453
+ * plan-milestone, complete-slice, complete-milestone, validate-milestone,
454
+ * reassess-roadmap) previously injected the full KNOWLEDGE.md (~226KB for a
455
+ * real project) on every invocation. This helper scopes by caller-supplied
456
+ * keywords and caps the payload at `maxChars` (default 30,000 chars).
457
+ *
458
+ * Returns null when no KNOWLEDGE.md exists or no entries match any keyword.
459
+ */
460
+ export async function inlineKnowledgeBudgeted(base, keywords, options) {
461
+ const DEFAULT_MAX_CHARS = 30_000;
462
+ const HARD_MAX_CHARS = 100_000;
463
+ const raw = Number(options?.maxChars ?? DEFAULT_MAX_CHARS);
464
+ const maxChars = Number.isFinite(raw)
465
+ ? Math.max(0, Math.min(Math.floor(raw), HARD_MAX_CHARS))
466
+ : DEFAULT_MAX_CHARS;
467
+ const knowledgePath = resolveGsdRootFile(base, "KNOWLEDGE");
468
+ if (!existsSync(knowledgePath))
469
+ return null;
470
+ const content = await loadFile(knowledgePath);
471
+ if (!content)
472
+ return null;
473
+ const { queryKnowledge } = await import("./context-store.js");
474
+ const scoped = await queryKnowledge(content, keywords);
475
+ if (!scoped)
476
+ return null;
477
+ const trimmed = scoped.trim();
478
+ const truncated = trimmed.length > maxChars
479
+ ? `${trimmed.slice(0, maxChars)}\n\n[...truncated ${trimmed.length - maxChars} chars; rerun with narrower scope if needed]`
480
+ : trimmed;
481
+ return `### Project Knowledge (scoped)\nSource: \`${relGsdRootFile("KNOWLEDGE")}\`\n\n${truncated}`;
482
+ }
449
483
  /**
450
484
  * Inline a roadmap excerpt for a specific slice.
451
485
  * Reads full roadmap, extracts minimal excerpt with header + predecessor + target row.
@@ -958,7 +992,8 @@ export async function buildResearchMilestonePrompt(mid, midTitle, base) {
958
992
  const decisionsInline = await inlineDecisionsFromDb(base, mid);
959
993
  if (decisionsInline)
960
994
  inlined.push(decisionsInline);
961
- const knowledgeInlineRM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
995
+ // Scoped + budgeted see issue #4719
996
+ const knowledgeInlineRM = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
962
997
  if (knowledgeInlineRM)
963
998
  inlined.push(knowledgeInlineRM);
964
999
  inlined.push(inlineTemplate("research", "Research"));
@@ -1015,7 +1050,8 @@ export async function buildPlanMilestonePrompt(mid, midTitle, base, level) {
1015
1050
  const queueInline = await inlineFileSmart(queuePath, relGsdRootFile("QUEUE"), "Project Queue", `${mid} ${midTitle}`);
1016
1051
  inlined.push(queueInline);
1017
1052
  }
1018
- const knowledgeInlinePM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1053
+ // Scoped + budgeted see issue #4719
1054
+ const knowledgeInlinePM = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
1019
1055
  if (knowledgeInlinePM)
1020
1056
  inlined.push(knowledgeInlinePM);
1021
1057
  inlined.push(inlineTemplate("roadmap", "Roadmap"));
@@ -1417,7 +1453,7 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
1417
1453
  }),
1418
1454
  });
1419
1455
  }
1420
- export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base, level) {
1456
+ export async function buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, base, level) {
1421
1457
  const inlineLevel = level ?? resolveInlineLevel();
1422
1458
  const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
1423
1459
  const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
@@ -1436,7 +1472,9 @@ export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base
1436
1472
  if (requirementsInline)
1437
1473
  inlined.push(requirementsInline);
1438
1474
  }
1439
- const knowledgeInlineCS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1475
+ // Scoped + budgeted see issue #4719. Slice context is richer than
1476
+ // milestone context at complete-slice time, so combine both title sources.
1477
+ const knowledgeInlineCS = await inlineKnowledgeBudgeted(base, [...extractKeywords(midTitle), ...extractKeywords(sTitle)]);
1440
1478
  if (knowledgeInlineCS)
1441
1479
  inlined.push(knowledgeInlineCS);
1442
1480
  // Inline all task summaries for this slice
@@ -1532,7 +1570,8 @@ export async function buildCompleteMilestonePrompt(mid, midTitle, base, level) {
1532
1570
  if (projectInline)
1533
1571
  inlined.push(projectInline);
1534
1572
  }
1535
- const knowledgeInlineCM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1573
+ // Scoped + budgeted see issue #4719
1574
+ const knowledgeInlineCM = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
1536
1575
  if (knowledgeInlineCM)
1537
1576
  inlined.push(knowledgeInlineCM);
1538
1577
  // Inline milestone context file (milestone-level, not GSD root)
@@ -1671,7 +1710,8 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
1671
1710
  if (projectInline)
1672
1711
  inlined.push(projectInline);
1673
1712
  }
1674
- const knowledgeInline = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1713
+ // Scoped + budgeted see issue #4719
1714
+ const knowledgeInline = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
1675
1715
  if (knowledgeInline)
1676
1716
  inlined.push(knowledgeInline);
1677
1717
  // Inline milestone context file
@@ -1841,7 +1881,8 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
1841
1881
  if (decisionsInline)
1842
1882
  inlined.push(decisionsInline);
1843
1883
  }
1844
- const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1884
+ // Scoped + budgeted see issue #4719
1885
+ const knowledgeInlineRA = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
1845
1886
  if (knowledgeInlineRA)
1846
1887
  inlined.push(knowledgeInlineRA);
1847
1888
  const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
@@ -19,12 +19,13 @@ import { invalidateAllCaches } from "./cache.js";
19
19
  import { writeLock, clearLock } from "./crash-recovery.js";
20
20
  import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
21
21
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
22
- import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeCheckoutBranch, nativeBranchList, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, } from "./native-git-bridge.js";
22
+ import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeCheckoutBranch, nativeBranchList, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, nativeCommitCountBetween, } from "./native-git-bridge.js";
23
23
  import { GitServiceImpl } from "./git-service.js";
24
24
  import { captureIntegrationBranch, detectWorktreeName, setActiveMilestoneId, } from "./worktree.js";
25
25
  import { getAutoWorktreePath } from "./auto-worktree.js";
26
26
  import { readResourceVersion, cleanStaleRuntimeUnits } from "./auto-worktree.js";
27
27
  import { worktreePath as getWorktreeDir, isInsideWorktreesDir } from "./worktree-manager.js";
28
+ import { emitWorktreeOrphaned } from "./worktree-telemetry.js";
28
29
  import { initMetrics } from "./metrics.js";
29
30
  import { initRoutingHistory } from "./routing-history.js";
30
31
  import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
@@ -120,10 +121,58 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode) {
120
121
  for (const branch of milestoneBranches) {
121
122
  const milestoneId = branch.replace(/^milestone\//, "");
122
123
  const milestone = getMilestone(milestoneId);
123
- // Only audit completed milestones
124
- if (!milestone || milestone.status !== "complete")
124
+ if (!milestone)
125
125
  continue;
126
126
  const isMerged = mergedBranches.has(branch);
127
+ // #4762 — in-progress milestone branch with unmerged commits ahead of
128
+ // main. This is the pre-completion orphan case: auto-mode exited without
129
+ // completing the milestone (pause, stop, crash, merge error, blocker) and
130
+ // work is stranded on the branch or in the worktree. Data safety first:
131
+ // we never delete or touch; we just surface a warning so the user knows
132
+ // where to look.
133
+ //
134
+ // Gate on isClosedStatus so we only warn about genuinely open milestones.
135
+ // Parked/other closed statuses go through the legacy complete/unmerged
136
+ // path below where appropriate.
137
+ if (!isClosedStatus(milestone.status)) {
138
+ if (isMerged)
139
+ continue; // nothing to recover
140
+ let commitsAhead = 0;
141
+ try {
142
+ commitsAhead = nativeCommitCountBetween(basePath, mainBranch, branch);
143
+ }
144
+ catch {
145
+ // Rev-walk failure — skip rather than noise
146
+ continue;
147
+ }
148
+ if (commitsAhead === 0)
149
+ continue;
150
+ const wtDir = getWorktreeDir(basePath, milestoneId);
151
+ const wtDirExists = existsSync(wtDir);
152
+ const wtSuffix = wtDirExists
153
+ ? ` Worktree directory at .gsd/worktrees/${milestoneId}/ holds the live work.`
154
+ : "";
155
+ warnings.push(`Branch ${branch} has ${commitsAhead} commit(s) ahead of ${mainBranch} for in-progress milestone ${milestoneId}.` +
156
+ wtSuffix +
157
+ ` Run \`/gsd auto\` to resume, or merge manually if abandoning.`);
158
+ // #4764 telemetry
159
+ try {
160
+ emitWorktreeOrphaned(basePath, milestoneId, {
161
+ reason: "in-progress-unmerged",
162
+ commitsAhead,
163
+ worktreeDirExists: wtDirExists,
164
+ });
165
+ }
166
+ catch (err) {
167
+ logWarning("engine", `worktree-orphaned telemetry failed for ${milestoneId}: ${err instanceof Error ? err.message : String(err)}`);
168
+ }
169
+ continue;
170
+ }
171
+ // Only the "complete" status participates in the merged/unmerged cleanup
172
+ // paths below — other closed statuses (parked, etc.) are intentionally
173
+ // left alone.
174
+ if (milestone.status !== "complete")
175
+ continue;
127
176
  if (isMerged) {
128
177
  // Branch is merged — safe to delete branch and clean up worktree dir
129
178
  try {
@@ -170,6 +219,16 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode) {
170
219
  // Branch is NOT merged — preserve for safety, warn the user
171
220
  warnings.push(`Branch ${branch} exists for completed milestone ${milestoneId} but is NOT merged into ${mainBranch}. ` +
172
221
  `This may contain unmerged work. Merge manually or run \`/gsd health --fix\` to resolve.`);
222
+ // #4764 telemetry
223
+ try {
224
+ emitWorktreeOrphaned(basePath, milestoneId, {
225
+ reason: "complete-unmerged",
226
+ worktreeDirExists: existsSync(getWorktreeDir(basePath, milestoneId)),
227
+ });
228
+ }
229
+ catch (err) {
230
+ logWarning("engine", `worktree-orphaned telemetry failed for ${milestoneId}: ${err instanceof Error ? err.message : String(err)}`);
231
+ }
173
232
  }
174
233
  }
175
234
  return { recovered, warnings };
@@ -528,6 +528,40 @@ export async function stopAuto(ctx, pi, reason) {
528
528
  return;
529
529
  const loadedPreferences = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences;
530
530
  const reasonSuffix = reason ? ` — ${reason}` : "";
531
+ // #4764 — telemetry: record the exit reason and whether the current milestone
532
+ // was merged before we entered stopAuto. This is the producer-side signal for
533
+ // the #4761 orphan class: milestoneMerged=false + currentMilestoneId present
534
+ // is exactly the pattern that strands work.
535
+ try {
536
+ const { emitAutoExit } = await import("./worktree-telemetry.js");
537
+ // Normalize the free-form reason to a closed set so the telemetry
538
+ // aggregator buckets stably. Raw detail is preserved in the phases.ts
539
+ // notification and the notify'd error string.
540
+ const rawReason = reason ?? "stop";
541
+ const normalizedReason = rawReason.startsWith("Blocked:")
542
+ ? "blocked"
543
+ : rawReason.startsWith("Merge conflict")
544
+ ? "merge-conflict"
545
+ : rawReason.startsWith("Merge error") || rawReason.startsWith("Merge failed")
546
+ ? "merge-failed"
547
+ : rawReason.startsWith("slice-merge-conflict")
548
+ ? "slice-merge-conflict"
549
+ : rawReason === "All milestones complete"
550
+ ? "all-complete"
551
+ : rawReason === "No active milestone"
552
+ ? "no-active-milestone"
553
+ : rawReason === "stop" || rawReason === "pause"
554
+ ? rawReason
555
+ : "other";
556
+ emitAutoExit(s.originalBasePath || s.basePath, {
557
+ reason: normalizedReason,
558
+ milestoneId: s.currentMilestoneId ?? undefined,
559
+ milestoneMerged: s.milestoneMergedInPhases === true,
560
+ });
561
+ }
562
+ catch (err) {
563
+ logWarning("engine", `auto-exit telemetry failed: ${err instanceof Error ? err.message : String(err)}`);
564
+ }
531
565
  try {
532
566
  // ── Step 1: Timers and locks ──
533
567
  try {
@@ -180,7 +180,13 @@ export function queryProject() {
180
180
  // ─── Knowledge Query ───────────────────────────────────────────────────────
181
181
  /**
182
182
  * Filter KNOWLEDGE.md sections by keyword matching.
183
- * Uses H2 sections, matches keywords case-insensitively against:
183
+ *
184
+ * Structure-adaptive (issue #4719): files that organise entries as H3 items
185
+ * under one or more H2 topics are filtered at H3 granularity. Files with only
186
+ * H2 topic headers (no H3) fall back to H2-level filtering for backwards
187
+ * compatibility.
188
+ *
189
+ * Matches keywords case-insensitively against:
184
190
  * 1. Section header text
185
191
  * 2. First paragraph of section content (up to first blank line or next heading)
186
192
  *
@@ -189,28 +195,38 @@ export function queryProject() {
189
195
  *
190
196
  * @param content - Full KNOWLEDGE.md content
191
197
  * @param keywords - Keywords to match (case-insensitive)
192
- * @returns Concatenated matching sections with H2 headers, or empty string
198
+ * @returns Concatenated matching sections with their original heading prefix, or empty string
193
199
  */
194
200
  export async function queryKnowledge(content, keywords) {
195
201
  if (!content || keywords.length === 0)
196
202
  return '';
197
203
  // Lazy import to avoid circular dependency
198
204
  const { extractAllSections } = await import('./files.js');
199
- const sections = extractAllSections(content, 2);
205
+ // Prefer H3 granularity when available; fall back to H2 for H2-only files.
206
+ // This prevents single-H2-with-many-H3 layouts from returning the entire
207
+ // file on a keyword match against the H2 header or its first paragraph.
208
+ const h3Sections = extractAllSections(content, 3);
209
+ const useH3 = h3Sections.size > 0;
210
+ const sections = useH3 ? h3Sections : extractAllSections(content, 2);
200
211
  if (sections.size === 0)
201
212
  return '';
202
- // Normalize keywords for case-insensitive matching
203
- const normalizedKeywords = keywords.map(k => k.toLowerCase());
213
+ const prefix = useH3 ? '###' : '##';
214
+ // Trim, lowercase, drop empties, and de-dupe so callers can pass raw
215
+ // user-provided strings without risking empty-string / whitespace matches.
216
+ const normalizedKeywords = [...new Set(keywords
217
+ .map(k => k.trim().toLowerCase())
218
+ .filter(k => k.length > 0))];
219
+ if (normalizedKeywords.length === 0)
220
+ return '';
204
221
  const matchingSections = [];
205
222
  for (const [header, body] of sections) {
206
223
  // Extract first paragraph: everything up to first blank line or next heading
207
224
  const firstParagraph = body.split(/\n\s*\n|\n#/)[0] || '';
208
- // Check if any keyword matches header or first paragraph
209
225
  const headerLower = header.toLowerCase();
210
226
  const paragraphLower = firstParagraph.toLowerCase();
211
227
  const matches = normalizedKeywords.some(kw => headerLower.includes(kw) || paragraphLower.includes(kw));
212
228
  if (matches) {
213
- matchingSections.push(`## ${header}\n\n${body}`);
229
+ matchingSections.push(`${prefix} ${header}\n\n${body}`);
214
230
  }
215
231
  }
216
232
  return matchingSections.join('\n\n');
@@ -29,6 +29,7 @@ import { getAutoWorktreePath } from "./auto-worktree.js";
29
29
  import { loadEffectiveGSDPreferences, loadGlobalGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
30
30
  import { showNextAction } from "../shared/tui.js";
31
31
  import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
32
+ import { summarizeWorktreeTelemetry, percentile } from "./worktree-telemetry.js";
32
33
  // ─── Duplicate Detection ──────────────────────────────────────────────────────
33
34
  const DEDUP_PROMPT_SECTION = `
34
35
  ## Pre-Investigation: Duplicate Check (REQUIRED)
@@ -214,6 +215,16 @@ export async function buildForensicReport(basePath) {
214
215
  detectCrash(crashLock, anomalies);
215
216
  detectDoctorIssues(doctorIssues, anomalies);
216
217
  detectErrorTraces(unitTraces, anomalies);
218
+ // 11b. #4764 — worktree lifecycle telemetry
219
+ let worktreeTelemetry = null;
220
+ try {
221
+ worktreeTelemetry = summarizeWorktreeTelemetry(basePath);
222
+ detectWorktreeOrphans(worktreeTelemetry, anomalies);
223
+ }
224
+ catch {
225
+ // Telemetry is best-effort — do not let an aggregator failure block the
226
+ // rest of the forensic report.
227
+ }
217
228
  detectJournalAnomalies(journalSummary, anomalies);
218
229
  return {
219
230
  gsdVersion,
@@ -232,6 +243,7 @@ export async function buildForensicReport(basePath) {
232
243
  recentUnits,
233
244
  journalSummary,
234
245
  activityLogMeta,
246
+ worktreeTelemetry,
235
247
  };
236
248
  }
237
249
  // ─── Activity Log Scanner ─────────────────────────────────────────────────────
@@ -630,6 +642,45 @@ function detectMissingArtifacts(completedKeys, basePath, activeMilestone, anomal
630
642
  }
631
643
  }
632
644
  }
645
+ /**
646
+ * #4764 — surface worktree lifecycle and orphan signals in the forensic report.
647
+ *
648
+ * Consumes only the aggregated summary (not raw journal events) to respect
649
+ * the forensics memory-bloat guard in forensics-journal.test.ts — per-event
650
+ * detail stays in the journal itself where the LLM can query it on demand.
651
+ */
652
+ function detectWorktreeOrphans(summary, anomalies) {
653
+ // 1. Orphan aggregate — severity depends on reason. In-progress orphans are
654
+ // the #4761 consumer-side signal (live work sitting on an unmerged branch).
655
+ for (const [reason, count] of Object.entries(summary.orphansByReason)) {
656
+ if (count <= 0)
657
+ continue;
658
+ const severity = reason === "in-progress-unmerged" ? "warning" : "info";
659
+ anomalies.push({
660
+ type: "worktree-orphan",
661
+ severity,
662
+ summary: `${count} worktree orphan(s) detected (${reason})`,
663
+ details: reason === "in-progress-unmerged"
664
+ ? "Auto-mode exited without completing a milestone; live work sits on an unmerged milestone branch. Run `/gsd auto` to resume, or merge manually."
665
+ : reason === "complete-unmerged"
666
+ ? "A completed milestone's branch was never merged back to main. Run `/gsd health --fix` to resolve."
667
+ : `Reason: ${reason}.`,
668
+ });
669
+ }
670
+ // 2. Auto-exit producer signal — #4761's upstream cause.
671
+ if (summary.exitsWithUnmergedWork > 0) {
672
+ const reasonBreakdown = Object.entries(summary.exitsByReason)
673
+ .filter(([, n]) => n > 0)
674
+ .map(([r, n]) => `${r}=${n}`)
675
+ .join(", ");
676
+ anomalies.push({
677
+ type: "worktree-unmerged-exit",
678
+ severity: "warning",
679
+ summary: `${summary.exitsWithUnmergedWork} auto-exit(s) left milestone work unmerged`,
680
+ details: `Exit reasons: ${reasonBreakdown || "(none)"} · Producer-side signal for #4761-class orphans. Inspect .gsd/journal/*.jsonl with eventType:"auto-exit" for per-exit detail.`,
681
+ });
682
+ }
683
+ }
633
684
  function detectCrash(crashLock, anomalies) {
634
685
  if (!crashLock)
635
686
  return;
@@ -808,6 +859,39 @@ function saveForensicReport(basePath, report, problemDescription) {
808
859
  sections.push(`- Newest: ${meta.newestFile}`);
809
860
  sections.push(``);
810
861
  }
862
+ // #4764 — Worktree telemetry summary
863
+ if (report.worktreeTelemetry) {
864
+ const t = report.worktreeTelemetry;
865
+ const p50 = percentile(t.mergeDurationsMs, 0.5);
866
+ const p95 = percentile(t.mergeDurationsMs, 0.95);
867
+ sections.push(`## Worktree Telemetry`, ``);
868
+ sections.push(`- Worktrees created: ${t.worktreesCreated}`);
869
+ sections.push(`- Worktrees merged: ${t.worktreesMerged}`);
870
+ sections.push(`- Orphans detected: ${t.orphansDetected}`);
871
+ if (t.orphansDetected > 0) {
872
+ const breakdown = Object.entries(t.orphansByReason)
873
+ .map(([r, n]) => `${r}=${n}`).join(", ");
874
+ sections.push(` - By reason: ${breakdown}`);
875
+ }
876
+ sections.push(`- Merge conflicts: ${t.mergeConflicts}`);
877
+ if (t.mergeDurationsMs.length > 0) {
878
+ sections.push(`- Merge duration p50 / p95: ${p50 ?? "-"} / ${p95 ?? "-"} ms (n=${t.mergeDurationsMs.length})`);
879
+ }
880
+ sections.push(`- Auto-exits leaving unmerged work: ${t.exitsWithUnmergedWork}`);
881
+ if (Object.keys(t.exitsByReason).length > 0) {
882
+ const breakdown = Object.entries(t.exitsByReason)
883
+ .sort((a, b) => b[1] - a[1])
884
+ .map(([r, n]) => `${r}=${n}`).join(", ");
885
+ sections.push(` - Exit reasons: ${breakdown}`);
886
+ }
887
+ sections.push(`- Canonical-root redirects (#4761 fix fired): ${t.canonicalRedirects}`);
888
+ // #4765 slice-cadence counters
889
+ if (t.slicesMerged + t.sliceMergeConflicts + t.milestoneResquashes > 0) {
890
+ sections.push(`- Slices merged: ${t.slicesMerged} · Slice merge conflicts: ${t.sliceMergeConflicts}`);
891
+ sections.push(`- Milestone re-squashes: ${t.milestoneResquashes}`);
892
+ }
893
+ sections.push(``);
894
+ }
811
895
  // Journal summary
812
896
  if (report.journalSummary) {
813
897
  const js = report.journalSummary;
@@ -940,6 +1024,28 @@ function formatReportForPrompt(report) {
940
1024
  sections.push(`- Total duration: ${formatDuration(totals.duration)}`);
941
1025
  sections.push("");
942
1026
  }
1027
+ // #4764 — worktree telemetry (compact prompt form)
1028
+ if (report.worktreeTelemetry) {
1029
+ const t = report.worktreeTelemetry;
1030
+ const hasSignal = t.worktreesCreated + t.worktreesMerged + t.orphansDetected +
1031
+ t.exitsWithUnmergedWork + t.canonicalRedirects +
1032
+ t.slicesMerged + t.milestoneResquashes > 0;
1033
+ if (hasSignal) {
1034
+ sections.push("### Worktree Telemetry");
1035
+ sections.push(`- Created: ${t.worktreesCreated} · Merged: ${t.worktreesMerged} · Conflicts: ${t.mergeConflicts}`);
1036
+ sections.push(`- Orphans: ${t.orphansDetected} · Unmerged exits: ${t.exitsWithUnmergedWork} · Redirects (#4761): ${t.canonicalRedirects}`);
1037
+ if (t.orphansDetected > 0) {
1038
+ const breakdown = Object.entries(t.orphansByReason)
1039
+ .map(([r, n]) => `${r}=${n}`).join(", ");
1040
+ sections.push(`- Orphan reasons: ${breakdown}`);
1041
+ }
1042
+ // #4765 — slice-cadence counters (only shown when the feature was exercised)
1043
+ if (t.slicesMerged + t.sliceMergeConflicts + t.milestoneResquashes > 0) {
1044
+ sections.push(`- Slices merged: ${t.slicesMerged} · Slice conflicts: ${t.sliceMergeConflicts} · Re-squashes: ${t.milestoneResquashes}`);
1045
+ }
1046
+ sections.push("");
1047
+ }
1048
+ }
943
1049
  // Activity log metadata
944
1050
  if (report.activityLogMeta) {
945
1051
  const meta = report.activityLogMeta;
@@ -1031,6 +1031,29 @@ export function validatePreferences(preferences) {
1031
1031
  if (g.merge_to_main !== undefined) {
1032
1032
  warnings.push("git.merge_to_main is deprecated — milestone-level merge is now always used. Remove this setting.");
1033
1033
  }
1034
+ // #4765 — collapse cadence + milestone resquash
1035
+ if (g.collapse_cadence !== undefined) {
1036
+ const validCadence = new Set(["milestone", "slice"]);
1037
+ if (typeof g.collapse_cadence === "string" && validCadence.has(g.collapse_cadence)) {
1038
+ git.collapse_cadence = g.collapse_cadence;
1039
+ }
1040
+ else {
1041
+ errors.push("git.collapse_cadence must be one of: milestone, slice");
1042
+ }
1043
+ }
1044
+ if (g.milestone_resquash !== undefined) {
1045
+ if (typeof g.milestone_resquash === "boolean") {
1046
+ git.milestone_resquash = g.milestone_resquash;
1047
+ const cadence = git.collapse_cadence
1048
+ ?? (typeof g.collapse_cadence === "string" ? g.collapse_cadence : undefined);
1049
+ if (cadence !== "slice") {
1050
+ warnings.push('git.milestone_resquash is ignored unless git.collapse_cadence is "slice"');
1051
+ }
1052
+ }
1053
+ else {
1054
+ errors.push("git.milestone_resquash must be a boolean");
1055
+ }
1056
+ }
1034
1057
  if (Object.keys(git).length > 0) {
1035
1058
  validated.git = git;
1036
1059
  }
@@ -24,6 +24,10 @@ const SEMI_STATIC_LABELS = new Set([
24
24
  "prior-summaries",
25
25
  "project-context",
26
26
  "overrides",
27
+ // KNOWLEDGE is milestone-scoped (stable within a session), so it belongs
28
+ // in the cacheable prefix. See issue #4719.
29
+ "knowledge",
30
+ "project-knowledge",
27
31
  ]);
28
32
  /** Labels that change per-task */
29
33
  const DYNAMIC_LABELS = new Set([