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
|
@@ -178,6 +178,12 @@ export class AutoSession {
|
|
|
178
178
|
* stopAuto does not attempt the same merge a second time (#2645). */
|
|
179
179
|
milestoneMergedInPhases = false;
|
|
180
180
|
|
|
181
|
+
// #4765 — slice-cadence collapse: main-branch SHAs at the moment each
|
|
182
|
+
// milestone's first slice merge began. Used by resquashMilestoneOnMain at
|
|
183
|
+
// milestone completion to collapse N slice commits into one. Cleared when
|
|
184
|
+
// the milestone finishes (or resquash runs).
|
|
185
|
+
milestoneStartShas: Map<string, string> = new Map();
|
|
186
|
+
|
|
181
187
|
// ── Dispatch circuit breakers ──────────────────────────────────────
|
|
182
188
|
rewriteAttemptCount = 0;
|
|
183
189
|
/** Tracks consecutive bootstrap attempts that found phase === "complete".
|
|
@@ -299,6 +305,7 @@ export class AutoSession {
|
|
|
299
305
|
this.lastGitActionStatus = null;
|
|
300
306
|
this.isolationDegraded = false;
|
|
301
307
|
this.milestoneMergedInPhases = false;
|
|
308
|
+
this.milestoneStartShas = new Map();
|
|
302
309
|
this.checkpointSha = null;
|
|
303
310
|
|
|
304
311
|
// Signal handler
|
|
@@ -618,6 +618,87 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
618
618
|
clearReactiveState(s.basePath, mid, sid);
|
|
619
619
|
}
|
|
620
620
|
});
|
|
621
|
+
|
|
622
|
+
// #4765 — slice-cadence collapse. When `git.collapse_cadence: "slice"`
|
|
623
|
+
// is set, squash-merge the slice's commits from the milestone branch
|
|
624
|
+
// onto main right here, so orphan risk shrinks from milestone-size to
|
|
625
|
+
// slice-size. Only runs in worktree isolation mode — the feature needs
|
|
626
|
+
// a milestone branch to squash from.
|
|
627
|
+
let sliceMergeStopped = false;
|
|
628
|
+
await runSafely("postUnit", "slice-cadence-merge", async () => {
|
|
629
|
+
const prefsResult = loadEffectiveGSDPreferences(s.basePath);
|
|
630
|
+
const prefs = prefsResult?.preferences;
|
|
631
|
+
const { getCollapseCadence, mergeSliceToMain } = await import("./slice-cadence.js");
|
|
632
|
+
if (getCollapseCadence(prefs) !== "slice") return;
|
|
633
|
+
if (prefs?.git?.isolation !== "worktree") return;
|
|
634
|
+
if (s.isolationDegraded) return;
|
|
635
|
+
|
|
636
|
+
const projectRoot = s.originalBasePath || s.basePath;
|
|
637
|
+
const { milestone: mid, slice: sid } = parseUnitId(unit.id);
|
|
638
|
+
if (!mid || !sid) return;
|
|
639
|
+
|
|
640
|
+
// Record the milestone start SHA before the first slice merge, so
|
|
641
|
+
// resquashMilestoneOnMain has a target at milestone completion.
|
|
642
|
+
// Resolve main branch dynamically — hard-coding "main" breaks repos
|
|
643
|
+
// that use "master" or a custom default branch.
|
|
644
|
+
if (!s.milestoneStartShas.has(mid)) {
|
|
645
|
+
try {
|
|
646
|
+
const { nativeDetectMainBranch } = await import("./native-git-bridge.js");
|
|
647
|
+
const mainBranch = nativeDetectMainBranch(projectRoot);
|
|
648
|
+
const { execFileSync } = await import("node:child_process");
|
|
649
|
+
const sha = execFileSync("git", ["rev-parse", mainBranch], {
|
|
650
|
+
cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8",
|
|
651
|
+
}).trim();
|
|
652
|
+
if (sha) s.milestoneStartShas.set(mid, sha);
|
|
653
|
+
} catch (err) {
|
|
654
|
+
logWarning("engine", `slice-cadence: failed to record milestone start SHA: ${err instanceof Error ? err.message : String(err)}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
try {
|
|
659
|
+
const result = mergeSliceToMain(projectRoot, mid, sid);
|
|
660
|
+
if (result.skipped) {
|
|
661
|
+
logWarning("engine", `slice-cadence: merge skipped for ${sid} — ${result.skippedReason}`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
ctx.ui.notify(
|
|
665
|
+
`slice-cadence: ${sid} merged to main (${result.durationMs}ms).`,
|
|
666
|
+
"info",
|
|
667
|
+
);
|
|
668
|
+
} catch (err) {
|
|
669
|
+
const { MergeConflictError } = await import("./git-service.js");
|
|
670
|
+
if (err instanceof MergeConflictError) {
|
|
671
|
+
ctx.ui.notify(
|
|
672
|
+
`slice-cadence merge conflict in ${sid}: ${err.conflictedFiles.join(", ")}. ` +
|
|
673
|
+
`Resolve manually on main and run \`/gsd auto\` to resume.`,
|
|
674
|
+
"error",
|
|
675
|
+
);
|
|
676
|
+
// Stop auto AND signal the outer postUnit flow to exit early.
|
|
677
|
+
// Without the flag, subsequent hooks (triage, rogue detection,
|
|
678
|
+
// DB writes) would keep running against a conflicted main
|
|
679
|
+
// checkout after the loop was already told to stop.
|
|
680
|
+
const { stopAuto } = await import("./auto.js");
|
|
681
|
+
await stopAuto(ctx, undefined, `slice-merge-conflict on ${sid}`);
|
|
682
|
+
sliceMergeStopped = true;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
logError("engine", `slice-cadence merge failed for ${sid}`, {
|
|
686
|
+
error: err instanceof Error ? err.message : String(err),
|
|
687
|
+
});
|
|
688
|
+
// Non-conflict failures (dirty main, rev-walk error, etc.) can
|
|
689
|
+
// leave the checkout in an unexpected state. Stop auto-mode so
|
|
690
|
+
// the next slice doesn't dispatch on top of it.
|
|
691
|
+
const { stopAuto } = await import("./auto.js");
|
|
692
|
+
await stopAuto(ctx, undefined, `slice-merge-error on ${sid}`);
|
|
693
|
+
sliceMergeStopped = true;
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
// Exit early after stopAuto so the rest of post-unit processing
|
|
697
|
+
// (triage, rogue detection, hook dispatch, DB writes) doesn't run
|
|
698
|
+
// against a conflicted main checkout. Return "dispatched" to match
|
|
699
|
+
// the convention used by other stop/pauseAuto paths in this function
|
|
700
|
+
// (see signal handling earlier: stop/pause also return "dispatched").
|
|
701
|
+
if (sliceMergeStopped) return "dispatched";
|
|
621
702
|
}
|
|
622
703
|
|
|
623
704
|
// Post-triage: execute actionable resolutions
|
|
@@ -514,6 +514,48 @@ export async function inlineKnowledgeScoped(
|
|
|
514
514
|
return `### Project Knowledge (scoped)\nSource: \`${relGsdRootFile("KNOWLEDGE")}\`\n\n${scoped.trim()}`;
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Budget-capped knowledge inline for milestone-level prompt assembly.
|
|
519
|
+
*
|
|
520
|
+
* Addresses issue #4719: the six milestone-phase prompts (research-milestone,
|
|
521
|
+
* plan-milestone, complete-slice, complete-milestone, validate-milestone,
|
|
522
|
+
* reassess-roadmap) previously injected the full KNOWLEDGE.md (~226KB for a
|
|
523
|
+
* real project) on every invocation. This helper scopes by caller-supplied
|
|
524
|
+
* keywords and caps the payload at `maxChars` (default 30,000 chars).
|
|
525
|
+
*
|
|
526
|
+
* Returns null when no KNOWLEDGE.md exists or no entries match any keyword.
|
|
527
|
+
*/
|
|
528
|
+
export async function inlineKnowledgeBudgeted(
|
|
529
|
+
base: string,
|
|
530
|
+
keywords: string[],
|
|
531
|
+
options?: { maxChars?: number },
|
|
532
|
+
): Promise<string | null> {
|
|
533
|
+
const DEFAULT_MAX_CHARS = 30_000;
|
|
534
|
+
const HARD_MAX_CHARS = 100_000;
|
|
535
|
+
const raw = Number(options?.maxChars ?? DEFAULT_MAX_CHARS);
|
|
536
|
+
const maxChars = Number.isFinite(raw)
|
|
537
|
+
? Math.max(0, Math.min(Math.floor(raw), HARD_MAX_CHARS))
|
|
538
|
+
: DEFAULT_MAX_CHARS;
|
|
539
|
+
|
|
540
|
+
const knowledgePath = resolveGsdRootFile(base, "KNOWLEDGE");
|
|
541
|
+
if (!existsSync(knowledgePath)) return null;
|
|
542
|
+
|
|
543
|
+
const content = await loadFile(knowledgePath);
|
|
544
|
+
if (!content) return null;
|
|
545
|
+
|
|
546
|
+
const { queryKnowledge } = await import("./context-store.js");
|
|
547
|
+
const scoped = await queryKnowledge(content, keywords);
|
|
548
|
+
if (!scoped) return null;
|
|
549
|
+
|
|
550
|
+
const trimmed = scoped.trim();
|
|
551
|
+
const truncated =
|
|
552
|
+
trimmed.length > maxChars
|
|
553
|
+
? `${trimmed.slice(0, maxChars)}\n\n[...truncated ${trimmed.length - maxChars} chars; rerun with narrower scope if needed]`
|
|
554
|
+
: trimmed;
|
|
555
|
+
|
|
556
|
+
return `### Project Knowledge (scoped)\nSource: \`${relGsdRootFile("KNOWLEDGE")}\`\n\n${truncated}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
517
559
|
/**
|
|
518
560
|
* Inline a roadmap excerpt for a specific slice.
|
|
519
561
|
* Reads full roadmap, extracts minimal excerpt with header + predecessor + target row.
|
|
@@ -1095,7 +1137,8 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
|
|
|
1095
1137
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
1096
1138
|
const decisionsInline = await inlineDecisionsFromDb(base, mid);
|
|
1097
1139
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
1098
|
-
|
|
1140
|
+
// Scoped + budgeted — see issue #4719
|
|
1141
|
+
const knowledgeInlineRM = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
|
|
1099
1142
|
if (knowledgeInlineRM) inlined.push(knowledgeInlineRM);
|
|
1100
1143
|
inlined.push(inlineTemplate("research", "Research"));
|
|
1101
1144
|
|
|
@@ -1156,7 +1199,8 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
|
|
|
1156
1199
|
);
|
|
1157
1200
|
inlined.push(queueInline);
|
|
1158
1201
|
}
|
|
1159
|
-
|
|
1202
|
+
// Scoped + budgeted — see issue #4719
|
|
1203
|
+
const knowledgeInlinePM = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
|
|
1160
1204
|
if (knowledgeInlinePM) inlined.push(knowledgeInlinePM);
|
|
1161
1205
|
inlined.push(inlineTemplate("roadmap", "Roadmap"));
|
|
1162
1206
|
if (inlineLevel === "full") {
|
|
@@ -1655,7 +1699,7 @@ export async function buildExecuteTaskPrompt(
|
|
|
1655
1699
|
}
|
|
1656
1700
|
|
|
1657
1701
|
export async function buildCompleteSlicePrompt(
|
|
1658
|
-
mid: string,
|
|
1702
|
+
mid: string, midTitle: string, sid: string, sTitle: string, base: string, level?: InlineLevel,
|
|
1659
1703
|
): Promise<string> {
|
|
1660
1704
|
const inlineLevel = level ?? resolveInlineLevel();
|
|
1661
1705
|
|
|
@@ -1675,7 +1719,12 @@ export async function buildCompleteSlicePrompt(
|
|
|
1675
1719
|
const requirementsInline = await inlineRequirementsFromDb(base, mid, sid, inlineLevel);
|
|
1676
1720
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
1677
1721
|
}
|
|
1678
|
-
|
|
1722
|
+
// Scoped + budgeted — see issue #4719. Slice context is richer than
|
|
1723
|
+
// milestone context at complete-slice time, so combine both title sources.
|
|
1724
|
+
const knowledgeInlineCS = await inlineKnowledgeBudgeted(
|
|
1725
|
+
base,
|
|
1726
|
+
[...extractKeywords(midTitle), ...extractKeywords(sTitle)],
|
|
1727
|
+
);
|
|
1679
1728
|
if (knowledgeInlineCS) inlined.push(knowledgeInlineCS);
|
|
1680
1729
|
|
|
1681
1730
|
// Inline all task summaries for this slice
|
|
@@ -1778,7 +1827,8 @@ export async function buildCompleteMilestonePrompt(
|
|
|
1778
1827
|
const projectInline = await inlineProjectFromDb(base);
|
|
1779
1828
|
if (projectInline) inlined.push(projectInline);
|
|
1780
1829
|
}
|
|
1781
|
-
|
|
1830
|
+
// Scoped + budgeted — see issue #4719
|
|
1831
|
+
const knowledgeInlineCM = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
|
|
1782
1832
|
if (knowledgeInlineCM) inlined.push(knowledgeInlineCM);
|
|
1783
1833
|
// Inline milestone context file (milestone-level, not GSD root)
|
|
1784
1834
|
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
@@ -1914,7 +1964,8 @@ export async function buildValidateMilestonePrompt(
|
|
|
1914
1964
|
const projectInline = await inlineProjectFromDb(base);
|
|
1915
1965
|
if (projectInline) inlined.push(projectInline);
|
|
1916
1966
|
}
|
|
1917
|
-
|
|
1967
|
+
// Scoped + budgeted — see issue #4719
|
|
1968
|
+
const knowledgeInline = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
|
|
1918
1969
|
if (knowledgeInline) inlined.push(knowledgeInline);
|
|
1919
1970
|
// Inline milestone context file
|
|
1920
1971
|
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
@@ -2099,7 +2150,8 @@ export async function buildReassessRoadmapPrompt(
|
|
|
2099
2150
|
const decisionsInline = await inlineDecisionsFromDb(base, mid, undefined, inlineLevel);
|
|
2100
2151
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
2101
2152
|
}
|
|
2102
|
-
|
|
2153
|
+
// Scoped + budgeted — see issue #4719
|
|
2154
|
+
const knowledgeInlineRA = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
|
|
2103
2155
|
if (knowledgeInlineRA) inlined.push(knowledgeInlineRA);
|
|
2104
2156
|
|
|
2105
2157
|
const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
nativeBranchListMerged,
|
|
46
46
|
nativeBranchDelete,
|
|
47
47
|
nativeWorktreeRemove,
|
|
48
|
+
nativeCommitCountBetween,
|
|
48
49
|
} from "./native-git-bridge.js";
|
|
49
50
|
import { GitServiceImpl } from "./git-service.js";
|
|
50
51
|
import {
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
import { getAutoWorktreePath, isInAutoWorktree } from "./auto-worktree.js";
|
|
56
57
|
import { readResourceVersion, cleanStaleRuntimeUnits } from "./auto-worktree.js";
|
|
57
58
|
import { worktreePath as getWorktreeDir, isInsideWorktreesDir } from "./worktree-manager.js";
|
|
59
|
+
import { emitWorktreeOrphaned } from "./worktree-telemetry.js";
|
|
58
60
|
import { initMetrics } from "./metrics.js";
|
|
59
61
|
import { initRoutingHistory } from "./routing-history.js";
|
|
60
62
|
import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
|
|
@@ -187,11 +189,61 @@ export function auditOrphanedMilestoneBranches(
|
|
|
187
189
|
const milestoneId = branch.replace(/^milestone\//, "");
|
|
188
190
|
const milestone = getMilestone(milestoneId);
|
|
189
191
|
|
|
190
|
-
|
|
191
|
-
if (!milestone || milestone.status !== "complete") continue;
|
|
192
|
+
if (!milestone) continue;
|
|
192
193
|
|
|
193
194
|
const isMerged = mergedBranches.has(branch);
|
|
194
195
|
|
|
196
|
+
// #4762 — in-progress milestone branch with unmerged commits ahead of
|
|
197
|
+
// main. This is the pre-completion orphan case: auto-mode exited without
|
|
198
|
+
// completing the milestone (pause, stop, crash, merge error, blocker) and
|
|
199
|
+
// work is stranded on the branch or in the worktree. Data safety first:
|
|
200
|
+
// we never delete or touch; we just surface a warning so the user knows
|
|
201
|
+
// where to look.
|
|
202
|
+
//
|
|
203
|
+
// Gate on isClosedStatus so we only warn about genuinely open milestones.
|
|
204
|
+
// Parked/other closed statuses go through the legacy complete/unmerged
|
|
205
|
+
// path below where appropriate.
|
|
206
|
+
if (!isClosedStatus(milestone.status)) {
|
|
207
|
+
if (isMerged) continue; // nothing to recover
|
|
208
|
+
let commitsAhead = 0;
|
|
209
|
+
try {
|
|
210
|
+
commitsAhead = nativeCommitCountBetween(basePath, mainBranch, branch);
|
|
211
|
+
} catch {
|
|
212
|
+
// Rev-walk failure — skip rather than noise
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (commitsAhead === 0) continue;
|
|
216
|
+
|
|
217
|
+
const wtDir = getWorktreeDir(basePath, milestoneId);
|
|
218
|
+
const wtDirExists = existsSync(wtDir);
|
|
219
|
+
const wtSuffix = wtDirExists
|
|
220
|
+
? ` Worktree directory at .gsd/worktrees/${milestoneId}/ holds the live work.`
|
|
221
|
+
: "";
|
|
222
|
+
warnings.push(
|
|
223
|
+
`Branch ${branch} has ${commitsAhead} commit(s) ahead of ${mainBranch} for in-progress milestone ${milestoneId}.` +
|
|
224
|
+
wtSuffix +
|
|
225
|
+
` Run \`/gsd auto\` to resume, or merge manually if abandoning.`,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// #4764 telemetry
|
|
229
|
+
try {
|
|
230
|
+
emitWorktreeOrphaned(basePath, milestoneId, {
|
|
231
|
+
reason: "in-progress-unmerged",
|
|
232
|
+
commitsAhead,
|
|
233
|
+
worktreeDirExists: wtDirExists,
|
|
234
|
+
});
|
|
235
|
+
} catch (err) {
|
|
236
|
+
logWarning("engine", `worktree-orphaned telemetry failed for ${milestoneId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Only the "complete" status participates in the merged/unmerged cleanup
|
|
243
|
+
// paths below — other closed statuses (parked, etc.) are intentionally
|
|
244
|
+
// left alone.
|
|
245
|
+
if (milestone.status !== "complete") continue;
|
|
246
|
+
|
|
195
247
|
if (isMerged) {
|
|
196
248
|
// Branch is merged — safe to delete branch and clean up worktree dir
|
|
197
249
|
try {
|
|
@@ -236,6 +288,16 @@ export function auditOrphanedMilestoneBranches(
|
|
|
236
288
|
`Branch ${branch} exists for completed milestone ${milestoneId} but is NOT merged into ${mainBranch}. ` +
|
|
237
289
|
`This may contain unmerged work. Merge manually or run \`/gsd health --fix\` to resolve.`,
|
|
238
290
|
);
|
|
291
|
+
|
|
292
|
+
// #4764 telemetry
|
|
293
|
+
try {
|
|
294
|
+
emitWorktreeOrphaned(basePath, milestoneId, {
|
|
295
|
+
reason: "complete-unmerged",
|
|
296
|
+
worktreeDirExists: existsSync(getWorktreeDir(basePath, milestoneId)),
|
|
297
|
+
});
|
|
298
|
+
} catch (err) {
|
|
299
|
+
logWarning("engine", `worktree-orphaned telemetry failed for ${milestoneId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
300
|
+
}
|
|
239
301
|
}
|
|
240
302
|
}
|
|
241
303
|
|
|
@@ -796,6 +796,43 @@ export async function stopAuto(
|
|
|
796
796
|
const loadedPreferences = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences;
|
|
797
797
|
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
798
798
|
|
|
799
|
+
// #4764 — telemetry: record the exit reason and whether the current milestone
|
|
800
|
+
// was merged before we entered stopAuto. This is the producer-side signal for
|
|
801
|
+
// the #4761 orphan class: milestoneMerged=false + currentMilestoneId present
|
|
802
|
+
// is exactly the pattern that strands work.
|
|
803
|
+
try {
|
|
804
|
+
const { emitAutoExit } = await import("./worktree-telemetry.js");
|
|
805
|
+
type AutoExitReason =
|
|
806
|
+
| "pause" | "stop" | "blocked" | "merge-conflict" | "merge-failed"
|
|
807
|
+
| "slice-merge-conflict" | "all-complete" | "no-active-milestone" | "other";
|
|
808
|
+
// Normalize the free-form reason to a closed set so the telemetry
|
|
809
|
+
// aggregator buckets stably. Raw detail is preserved in the phases.ts
|
|
810
|
+
// notification and the notify'd error string.
|
|
811
|
+
const rawReason = reason ?? "stop";
|
|
812
|
+
const normalizedReason: AutoExitReason = rawReason.startsWith("Blocked:")
|
|
813
|
+
? "blocked"
|
|
814
|
+
: rawReason.startsWith("Merge conflict")
|
|
815
|
+
? "merge-conflict"
|
|
816
|
+
: rawReason.startsWith("Merge error") || rawReason.startsWith("Merge failed")
|
|
817
|
+
? "merge-failed"
|
|
818
|
+
: rawReason.startsWith("slice-merge-conflict")
|
|
819
|
+
? "slice-merge-conflict"
|
|
820
|
+
: rawReason === "All milestones complete"
|
|
821
|
+
? "all-complete"
|
|
822
|
+
: rawReason === "No active milestone"
|
|
823
|
+
? "no-active-milestone"
|
|
824
|
+
: rawReason === "stop" || rawReason === "pause"
|
|
825
|
+
? rawReason
|
|
826
|
+
: "other";
|
|
827
|
+
emitAutoExit(s.originalBasePath || s.basePath, {
|
|
828
|
+
reason: normalizedReason,
|
|
829
|
+
milestoneId: s.currentMilestoneId ?? undefined,
|
|
830
|
+
milestoneMerged: s.milestoneMergedInPhases === true,
|
|
831
|
+
});
|
|
832
|
+
} catch (err) {
|
|
833
|
+
logWarning("engine", `auto-exit telemetry failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
834
|
+
}
|
|
835
|
+
|
|
799
836
|
try {
|
|
800
837
|
// ── Step 1: Timers and locks ──
|
|
801
838
|
try {
|
|
@@ -211,7 +211,13 @@ export function queryProject(): string | null {
|
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
213
|
* Filter KNOWLEDGE.md sections by keyword matching.
|
|
214
|
-
*
|
|
214
|
+
*
|
|
215
|
+
* Structure-adaptive (issue #4719): files that organise entries as H3 items
|
|
216
|
+
* under one or more H2 topics are filtered at H3 granularity. Files with only
|
|
217
|
+
* H2 topic headers (no H3) fall back to H2-level filtering for backwards
|
|
218
|
+
* compatibility.
|
|
219
|
+
*
|
|
220
|
+
* Matches keywords case-insensitively against:
|
|
215
221
|
* 1. Section header text
|
|
216
222
|
* 2. First paragraph of section content (up to first blank line or next heading)
|
|
217
223
|
*
|
|
@@ -220,7 +226,7 @@ export function queryProject(): string | null {
|
|
|
220
226
|
*
|
|
221
227
|
* @param content - Full KNOWLEDGE.md content
|
|
222
228
|
* @param keywords - Keywords to match (case-insensitive)
|
|
223
|
-
* @returns Concatenated matching sections with
|
|
229
|
+
* @returns Concatenated matching sections with their original heading prefix, or empty string
|
|
224
230
|
*/
|
|
225
231
|
export async function queryKnowledge(content: string, keywords: string[]): Promise<string> {
|
|
226
232
|
if (!content || keywords.length === 0) return '';
|
|
@@ -228,11 +234,23 @@ export async function queryKnowledge(content: string, keywords: string[]): Promi
|
|
|
228
234
|
// Lazy import to avoid circular dependency
|
|
229
235
|
const { extractAllSections } = await import('./files.js');
|
|
230
236
|
|
|
231
|
-
|
|
237
|
+
// Prefer H3 granularity when available; fall back to H2 for H2-only files.
|
|
238
|
+
// This prevents single-H2-with-many-H3 layouts from returning the entire
|
|
239
|
+
// file on a keyword match against the H2 header or its first paragraph.
|
|
240
|
+
const h3Sections = extractAllSections(content, 3);
|
|
241
|
+
const useH3 = h3Sections.size > 0;
|
|
242
|
+
const sections = useH3 ? h3Sections : extractAllSections(content, 2);
|
|
232
243
|
if (sections.size === 0) return '';
|
|
244
|
+
const prefix = useH3 ? '###' : '##';
|
|
233
245
|
|
|
234
|
-
//
|
|
235
|
-
|
|
246
|
+
// Trim, lowercase, drop empties, and de-dupe so callers can pass raw
|
|
247
|
+
// user-provided strings without risking empty-string / whitespace matches.
|
|
248
|
+
const normalizedKeywords = [...new Set(
|
|
249
|
+
keywords
|
|
250
|
+
.map(k => k.trim().toLowerCase())
|
|
251
|
+
.filter(k => k.length > 0),
|
|
252
|
+
)];
|
|
253
|
+
if (normalizedKeywords.length === 0) return '';
|
|
236
254
|
|
|
237
255
|
const matchingSections: string[] = [];
|
|
238
256
|
|
|
@@ -240,16 +258,15 @@ export async function queryKnowledge(content: string, keywords: string[]): Promi
|
|
|
240
258
|
// Extract first paragraph: everything up to first blank line or next heading
|
|
241
259
|
const firstParagraph = body.split(/\n\s*\n|\n#/)[0] || '';
|
|
242
260
|
|
|
243
|
-
// Check if any keyword matches header or first paragraph
|
|
244
261
|
const headerLower = header.toLowerCase();
|
|
245
262
|
const paragraphLower = firstParagraph.toLowerCase();
|
|
246
263
|
|
|
247
264
|
const matches = normalizedKeywords.some(kw =>
|
|
248
|
-
headerLower.includes(kw) || paragraphLower.includes(kw)
|
|
265
|
+
headerLower.includes(kw) || paragraphLower.includes(kw),
|
|
249
266
|
);
|
|
250
267
|
|
|
251
268
|
if (matches) {
|
|
252
|
-
matchingSections.push(
|
|
269
|
+
matchingSections.push(`${prefix} ${header}\n\n${body}`);
|
|
253
270
|
}
|
|
254
271
|
}
|
|
255
272
|
|
|
@@ -35,11 +35,12 @@ import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
|
35
35
|
import { loadEffectiveGSDPreferences, loadGlobalGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
|
|
36
36
|
import { showNextAction } from "../shared/tui.js";
|
|
37
37
|
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
|
|
38
|
+
import { summarizeWorktreeTelemetry, percentile, type WorktreeTelemetrySummary } from "./worktree-telemetry.js";
|
|
38
39
|
|
|
39
40
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
40
41
|
|
|
41
42
|
export interface ForensicAnomaly {
|
|
42
|
-
type: "stuck-loop" | "cost-spike" | "timeout" | "missing-artifact" | "crash" | "doctor-issue" | "error-trace" | "journal-stuck" | "journal-guard-block" | "journal-rapid-iterations" | "journal-worktree-failure";
|
|
43
|
+
type: "stuck-loop" | "cost-spike" | "timeout" | "missing-artifact" | "crash" | "doctor-issue" | "error-trace" | "journal-stuck" | "journal-guard-block" | "journal-rapid-iterations" | "journal-worktree-failure" | "worktree-orphan" | "worktree-unmerged-exit";
|
|
43
44
|
severity: "info" | "warning" | "error";
|
|
44
45
|
unitType?: string;
|
|
45
46
|
unitId?: string;
|
|
@@ -113,6 +114,8 @@ interface ForensicReport {
|
|
|
113
114
|
recentUnits: { type: string; id: string; cost: number; duration: number; model: string; finishedAt: number }[];
|
|
114
115
|
journalSummary: JournalSummary | null;
|
|
115
116
|
activityLogMeta: ActivityLogMeta | null;
|
|
117
|
+
/** #4764 — worktree lifespan / divergence telemetry aggregates. */
|
|
118
|
+
worktreeTelemetry: WorktreeTelemetrySummary | null;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
// ─── Duplicate Detection ──────────────────────────────────────────────────────
|
|
@@ -337,6 +340,16 @@ export async function buildForensicReport(basePath: string): Promise<ForensicRep
|
|
|
337
340
|
detectCrash(crashLock, anomalies);
|
|
338
341
|
detectDoctorIssues(doctorIssues, anomalies);
|
|
339
342
|
detectErrorTraces(unitTraces, anomalies);
|
|
343
|
+
|
|
344
|
+
// 11b. #4764 — worktree lifecycle telemetry
|
|
345
|
+
let worktreeTelemetry: WorktreeTelemetrySummary | null = null;
|
|
346
|
+
try {
|
|
347
|
+
worktreeTelemetry = summarizeWorktreeTelemetry(basePath);
|
|
348
|
+
detectWorktreeOrphans(worktreeTelemetry, anomalies);
|
|
349
|
+
} catch {
|
|
350
|
+
// Telemetry is best-effort — do not let an aggregator failure block the
|
|
351
|
+
// rest of the forensic report.
|
|
352
|
+
}
|
|
340
353
|
detectJournalAnomalies(journalSummary, anomalies);
|
|
341
354
|
|
|
342
355
|
return {
|
|
@@ -356,6 +369,7 @@ export async function buildForensicReport(basePath: string): Promise<ForensicRep
|
|
|
356
369
|
recentUnits,
|
|
357
370
|
journalSummary,
|
|
358
371
|
activityLogMeta,
|
|
372
|
+
worktreeTelemetry,
|
|
359
373
|
};
|
|
360
374
|
}
|
|
361
375
|
|
|
@@ -783,6 +797,51 @@ function detectMissingArtifacts(completedKeys: string[], basePath: string, activ
|
|
|
783
797
|
}
|
|
784
798
|
}
|
|
785
799
|
|
|
800
|
+
/**
|
|
801
|
+
* #4764 — surface worktree lifecycle and orphan signals in the forensic report.
|
|
802
|
+
*
|
|
803
|
+
* Consumes only the aggregated summary (not raw journal events) to respect
|
|
804
|
+
* the forensics memory-bloat guard in forensics-journal.test.ts — per-event
|
|
805
|
+
* detail stays in the journal itself where the LLM can query it on demand.
|
|
806
|
+
*/
|
|
807
|
+
function detectWorktreeOrphans(
|
|
808
|
+
summary: WorktreeTelemetrySummary,
|
|
809
|
+
anomalies: ForensicAnomaly[],
|
|
810
|
+
): void {
|
|
811
|
+
// 1. Orphan aggregate — severity depends on reason. In-progress orphans are
|
|
812
|
+
// the #4761 consumer-side signal (live work sitting on an unmerged branch).
|
|
813
|
+
for (const [reason, count] of Object.entries(summary.orphansByReason)) {
|
|
814
|
+
if (count <= 0) continue;
|
|
815
|
+
const severity: ForensicAnomaly["severity"] =
|
|
816
|
+
reason === "in-progress-unmerged" ? "warning" : "info";
|
|
817
|
+
anomalies.push({
|
|
818
|
+
type: "worktree-orphan",
|
|
819
|
+
severity,
|
|
820
|
+
summary: `${count} worktree orphan(s) detected (${reason})`,
|
|
821
|
+
details:
|
|
822
|
+
reason === "in-progress-unmerged"
|
|
823
|
+
? "Auto-mode exited without completing a milestone; live work sits on an unmerged milestone branch. Run `/gsd auto` to resume, or merge manually."
|
|
824
|
+
: reason === "complete-unmerged"
|
|
825
|
+
? "A completed milestone's branch was never merged back to main. Run `/gsd health --fix` to resolve."
|
|
826
|
+
: `Reason: ${reason}.`,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// 2. Auto-exit producer signal — #4761's upstream cause.
|
|
831
|
+
if (summary.exitsWithUnmergedWork > 0) {
|
|
832
|
+
const reasonBreakdown = Object.entries(summary.exitsByReason)
|
|
833
|
+
.filter(([, n]) => n > 0)
|
|
834
|
+
.map(([r, n]) => `${r}=${n}`)
|
|
835
|
+
.join(", ");
|
|
836
|
+
anomalies.push({
|
|
837
|
+
type: "worktree-unmerged-exit",
|
|
838
|
+
severity: "warning",
|
|
839
|
+
summary: `${summary.exitsWithUnmergedWork} auto-exit(s) left milestone work unmerged`,
|
|
840
|
+
details: `Exit reasons: ${reasonBreakdown || "(none)"} · Producer-side signal for #4761-class orphans. Inspect .gsd/journal/*.jsonl with eventType:"auto-exit" for per-exit detail.`,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
786
845
|
function detectCrash(crashLock: LockData | null, anomalies: ForensicAnomaly[]): void {
|
|
787
846
|
if (!crashLock) return;
|
|
788
847
|
if (isLockProcessAlive(crashLock)) return; // Process still running, not a crash
|
|
@@ -972,6 +1031,40 @@ function saveForensicReport(basePath: string, report: ForensicReport, problemDes
|
|
|
972
1031
|
sections.push(``);
|
|
973
1032
|
}
|
|
974
1033
|
|
|
1034
|
+
// #4764 — Worktree telemetry summary
|
|
1035
|
+
if (report.worktreeTelemetry) {
|
|
1036
|
+
const t = report.worktreeTelemetry;
|
|
1037
|
+
const p50 = percentile(t.mergeDurationsMs, 0.5);
|
|
1038
|
+
const p95 = percentile(t.mergeDurationsMs, 0.95);
|
|
1039
|
+
sections.push(`## Worktree Telemetry`, ``);
|
|
1040
|
+
sections.push(`- Worktrees created: ${t.worktreesCreated}`);
|
|
1041
|
+
sections.push(`- Worktrees merged: ${t.worktreesMerged}`);
|
|
1042
|
+
sections.push(`- Orphans detected: ${t.orphansDetected}`);
|
|
1043
|
+
if (t.orphansDetected > 0) {
|
|
1044
|
+
const breakdown = Object.entries(t.orphansByReason)
|
|
1045
|
+
.map(([r, n]) => `${r}=${n}`).join(", ");
|
|
1046
|
+
sections.push(` - By reason: ${breakdown}`);
|
|
1047
|
+
}
|
|
1048
|
+
sections.push(`- Merge conflicts: ${t.mergeConflicts}`);
|
|
1049
|
+
if (t.mergeDurationsMs.length > 0) {
|
|
1050
|
+
sections.push(`- Merge duration p50 / p95: ${p50 ?? "-"} / ${p95 ?? "-"} ms (n=${t.mergeDurationsMs.length})`);
|
|
1051
|
+
}
|
|
1052
|
+
sections.push(`- Auto-exits leaving unmerged work: ${t.exitsWithUnmergedWork}`);
|
|
1053
|
+
if (Object.keys(t.exitsByReason).length > 0) {
|
|
1054
|
+
const breakdown = Object.entries(t.exitsByReason)
|
|
1055
|
+
.sort((a, b) => b[1] - a[1])
|
|
1056
|
+
.map(([r, n]) => `${r}=${n}`).join(", ");
|
|
1057
|
+
sections.push(` - Exit reasons: ${breakdown}`);
|
|
1058
|
+
}
|
|
1059
|
+
sections.push(`- Canonical-root redirects (#4761 fix fired): ${t.canonicalRedirects}`);
|
|
1060
|
+
// #4765 slice-cadence counters
|
|
1061
|
+
if (t.slicesMerged + t.sliceMergeConflicts + t.milestoneResquashes > 0) {
|
|
1062
|
+
sections.push(`- Slices merged: ${t.slicesMerged} · Slice merge conflicts: ${t.sliceMergeConflicts}`);
|
|
1063
|
+
sections.push(`- Milestone re-squashes: ${t.milestoneResquashes}`);
|
|
1064
|
+
}
|
|
1065
|
+
sections.push(``);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
975
1068
|
// Journal summary
|
|
976
1069
|
if (report.journalSummary) {
|
|
977
1070
|
const js = report.journalSummary;
|
|
@@ -1117,6 +1210,30 @@ function formatReportForPrompt(report: ForensicReport): string {
|
|
|
1117
1210
|
sections.push("");
|
|
1118
1211
|
}
|
|
1119
1212
|
|
|
1213
|
+
// #4764 — worktree telemetry (compact prompt form)
|
|
1214
|
+
if (report.worktreeTelemetry) {
|
|
1215
|
+
const t = report.worktreeTelemetry;
|
|
1216
|
+
const hasSignal =
|
|
1217
|
+
t.worktreesCreated + t.worktreesMerged + t.orphansDetected +
|
|
1218
|
+
t.exitsWithUnmergedWork + t.canonicalRedirects +
|
|
1219
|
+
t.slicesMerged + t.milestoneResquashes > 0;
|
|
1220
|
+
if (hasSignal) {
|
|
1221
|
+
sections.push("### Worktree Telemetry");
|
|
1222
|
+
sections.push(`- Created: ${t.worktreesCreated} · Merged: ${t.worktreesMerged} · Conflicts: ${t.mergeConflicts}`);
|
|
1223
|
+
sections.push(`- Orphans: ${t.orphansDetected} · Unmerged exits: ${t.exitsWithUnmergedWork} · Redirects (#4761): ${t.canonicalRedirects}`);
|
|
1224
|
+
if (t.orphansDetected > 0) {
|
|
1225
|
+
const breakdown = Object.entries(t.orphansByReason)
|
|
1226
|
+
.map(([r, n]) => `${r}=${n}`).join(", ");
|
|
1227
|
+
sections.push(`- Orphan reasons: ${breakdown}`);
|
|
1228
|
+
}
|
|
1229
|
+
// #4765 — slice-cadence counters (only shown when the feature was exercised)
|
|
1230
|
+
if (t.slicesMerged + t.sliceMergeConflicts + t.milestoneResquashes > 0) {
|
|
1231
|
+
sections.push(`- Slices merged: ${t.slicesMerged} · Slice conflicts: ${t.sliceMergeConflicts} · Re-squashes: ${t.milestoneResquashes}`);
|
|
1232
|
+
}
|
|
1233
|
+
sections.push("");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1120
1237
|
// Activity log metadata
|
|
1121
1238
|
if (report.activityLogMeta) {
|
|
1122
1239
|
const meta = report.activityLogMeta;
|
|
@@ -85,6 +85,22 @@ export interface GitPreferences {
|
|
|
85
85
|
* for forensic inspection.
|
|
86
86
|
*/
|
|
87
87
|
absorb_snapshot_commits?: boolean;
|
|
88
|
+
/** #4765 — when to collapse worktree commits back to main.
|
|
89
|
+
* - "milestone" (default): existing behavior — squash-merge happens once
|
|
90
|
+
* at milestone completion or transition.
|
|
91
|
+
* - "slice": squash-merge each slice's commits to main as soon as the
|
|
92
|
+
* slice passes validation. Shrinks the orphan window from
|
|
93
|
+
* milestone-size to slice-size and surfaces merge conflicts per slice
|
|
94
|
+
* rather than all at once at milestone end.
|
|
95
|
+
*/
|
|
96
|
+
collapse_cadence?: "milestone" | "slice";
|
|
97
|
+
/** #4765 — when `collapse_cadence: "slice"`, optionally re-squash the per-
|
|
98
|
+
* slice commits on main into one milestone commit at milestone completion.
|
|
99
|
+
* Preserves the "one commit per milestone in main" history shape that
|
|
100
|
+
* `collapse_cadence: "milestone"` produces today.
|
|
101
|
+
* Default: true when collapse_cadence is "slice", ignored otherwise.
|
|
102
|
+
*/
|
|
103
|
+
milestone_resquash?: boolean;
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
|
|
@@ -50,7 +50,17 @@ export type JournalEventType =
|
|
|
50
50
|
| "worktree-skip"
|
|
51
51
|
| "worktree-merge-start"
|
|
52
52
|
| "worktree-merge-failed"
|
|
53
|
-
| "artifact-verification-retry"
|
|
53
|
+
| "artifact-verification-retry"
|
|
54
|
+
// #4764 — worktree lifespan / divergence telemetry
|
|
55
|
+
| "worktree-created"
|
|
56
|
+
| "worktree-merged"
|
|
57
|
+
| "worktree-orphaned"
|
|
58
|
+
| "auto-exit"
|
|
59
|
+
| "worktree-sync"
|
|
60
|
+
| "canonical-root-redirect"
|
|
61
|
+
// #4765 — slice-cadence collapse
|
|
62
|
+
| "slice-merged"
|
|
63
|
+
| "milestone-resquash";
|
|
54
64
|
|
|
55
65
|
/** A single structured event in the journal. */
|
|
56
66
|
export interface JournalEntry {
|