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
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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(
|
|
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([
|