gsd-pi 2.41.0-dev.0acbce9 → 2.41.0-dev.3557dc4
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/cli-web-branch.d.ts +6 -0
- package/dist/cli-web-branch.js +17 -0
- package/dist/onboarding.js +2 -1
- package/dist/resources/extensions/gsd/auto/loop.js +9 -1
- package/dist/resources/extensions/gsd/auto/phases.js +26 -8
- package/dist/resources/extensions/gsd/auto-dashboard.js +6 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +19 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +12 -4
- package/dist/resources/extensions/gsd/auto-start.js +8 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +147 -13
- package/dist/resources/extensions/gsd/auto.js +36 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +199 -164
- package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +62 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +16 -0
- package/dist/resources/extensions/gsd/commands/catalog.js +8 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/context-store.js +4 -3
- package/dist/resources/extensions/gsd/db-writer.js +5 -2
- package/dist/resources/extensions/gsd/detection.js +1 -1
- package/dist/resources/extensions/gsd/doctor.js +11 -1
- package/dist/resources/extensions/gsd/exit-command.js +12 -2
- package/dist/resources/extensions/gsd/export.js +9 -13
- package/dist/resources/extensions/gsd/extension-manifest.json +2 -2
- package/dist/resources/extensions/gsd/files.js +28 -11
- package/dist/resources/extensions/gsd/forensics.js +10 -3
- package/dist/resources/extensions/gsd/git-service.js +5 -1
- package/dist/resources/extensions/gsd/gsd-db.js +25 -8
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +7 -3
- package/dist/resources/extensions/gsd/journal.js +85 -0
- package/dist/resources/extensions/gsd/md-importer.js +5 -0
- package/dist/resources/extensions/gsd/milestone-ids.js +1 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +2 -2
- package/dist/resources/extensions/gsd/post-unit-hooks.js +24 -412
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences.js +1 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +34 -4
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
- package/dist/resources/extensions/gsd/repo-identity.js +46 -2
- package/dist/resources/extensions/gsd/rule-registry.js +489 -0
- package/dist/resources/extensions/gsd/rule-types.js +6 -0
- package/dist/resources/extensions/gsd/service-tier.js +138 -0
- package/dist/resources/extensions/gsd/structured-data-formatter.js +2 -1
- package/dist/resources/extensions/gsd/templates/decisions.md +2 -2
- package/dist/resources/extensions/gsd/workflow-templates.js +13 -1
- package/dist/resources/extensions/gsd/worktree-manager.js +20 -6
- package/dist/resources/extensions/gsd/worktree-resolver.js +19 -2
- package/dist/resources/extensions/subagent/index.js +7 -3
- package/dist/resources/extensions/voice/index.js +4 -4
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- 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/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +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 +13 -13
- package/dist/web/standalone/.next/server/chunks/229.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/4024.c195dc1fdd2adbea.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-9afaaebf6042a1d7.js → webpack-fa307370fcf9fb2c.js} +1 -1
- package/dist/web-mode.d.ts +2 -0
- package/dist/web-mode.js +29 -7
- package/package.json +1 -1
- package/packages/native/src/__tests__/text.test.mjs +33 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +3 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +10 -7
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +4 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +11 -7
- package/src/resources/extensions/gsd/auto/loop-deps.ts +5 -1
- package/src/resources/extensions/gsd/auto/loop.ts +10 -1
- package/src/resources/extensions/gsd/auto/phases.ts +28 -8
- package/src/resources/extensions/gsd/auto/types.ts +4 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +7 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +25 -5
- package/src/resources/extensions/gsd/auto-post-unit.ts +8 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +12 -4
- package/src/resources/extensions/gsd/auto-start.ts +8 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +162 -18
- package/src/resources/extensions/gsd/auto.ts +40 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +209 -162
- package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +62 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -0
- package/src/resources/extensions/gsd/commands/catalog.ts +8 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/context-store.ts +4 -3
- package/src/resources/extensions/gsd/db-writer.ts +6 -2
- package/src/resources/extensions/gsd/detection.ts +1 -1
- package/src/resources/extensions/gsd/doctor.ts +12 -1
- package/src/resources/extensions/gsd/exit-command.ts +14 -2
- package/src/resources/extensions/gsd/export.ts +8 -15
- package/src/resources/extensions/gsd/extension-manifest.json +2 -2
- package/src/resources/extensions/gsd/files.ts +29 -12
- package/src/resources/extensions/gsd/forensics.ts +9 -3
- package/src/resources/extensions/gsd/git-service.ts +5 -4
- package/src/resources/extensions/gsd/gsd-db.ts +37 -8
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +7 -3
- package/src/resources/extensions/gsd/journal.ts +134 -0
- package/src/resources/extensions/gsd/md-importer.ts +6 -0
- package/src/resources/extensions/gsd/milestone-ids.ts +1 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +2 -2
- package/src/resources/extensions/gsd/post-unit-hooks.ts +24 -462
- package/src/resources/extensions/gsd/preferences-types.ts +3 -0
- package/src/resources/extensions/gsd/preferences.ts +1 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +35 -4
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +1 -1
- package/src/resources/extensions/gsd/repo-identity.ts +47 -2
- package/src/resources/extensions/gsd/rule-registry.ts +599 -0
- package/src/resources/extensions/gsd/rule-types.ts +68 -0
- package/src/resources/extensions/gsd/service-tier.ts +171 -0
- package/src/resources/extensions/gsd/structured-data-formatter.ts +3 -1
- package/src/resources/extensions/gsd/templates/decisions.md +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +202 -0
- package/src/resources/extensions/gsd/tests/context-store.test.ts +10 -5
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +15 -10
- package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +5 -4
- package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/doctor-task-done-missing-summary-slice-loop.test.ts +174 -0
- package/src/resources/extensions/gsd/tests/exit-command.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +7 -7
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +513 -0
- package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/journal.test.ts +386 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +31 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/milestone-id-reservation.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/parsers.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -25
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +61 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +11 -22
- package/src/resources/extensions/gsd/tests/rule-registry.test.ts +413 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +178 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -3
- package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +74 -0
- package/src/resources/extensions/gsd/types.ts +3 -0
- package/src/resources/extensions/gsd/workflow-templates.ts +12 -1
- package/src/resources/extensions/gsd/worktree-manager.ts +21 -6
- package/src/resources/extensions/gsd/worktree-resolver.ts +30 -9
- package/src/resources/extensions/subagent/index.ts +7 -3
- package/src/resources/extensions/voice/index.ts +4 -4
- package/dist/web/standalone/.next/static/chunks/4024.279c423e4661ece1.js +0 -9
- /package/dist/web/standalone/.next/static/{SwbKZ7JPNFlEmU4f8pKEv → JBSIr4fSfHXs5g5x2ZBSC}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{SwbKZ7JPNFlEmU4f8pKEv → JBSIr4fSfHXs5g5x2ZBSC}/_ssgManifest.js +0 -0
|
@@ -26,6 +26,7 @@ import { runUnit } from "./run-unit.js";
|
|
|
26
26
|
import { debugLog } from "../debug-logger.js";
|
|
27
27
|
import { gsdRoot } from "../paths.js";
|
|
28
28
|
import { atomicWriteSync } from "../atomic-write.js";
|
|
29
|
+
import { PROJECT_FILES } from "../detection.js";
|
|
29
30
|
import { join } from "node:path";
|
|
30
31
|
|
|
31
32
|
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
@@ -192,6 +193,7 @@ export async function runPreDispatch(
|
|
|
192
193
|
|
|
193
194
|
// ── Milestone transition ────────────────────────────────────────────
|
|
194
195
|
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
196
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "milestone-transition", data: { from: s.currentMilestoneId, to: mid } });
|
|
195
197
|
ctx.ui.notify(
|
|
196
198
|
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`,
|
|
197
199
|
"info",
|
|
@@ -386,6 +388,7 @@ export async function runPreDispatch(
|
|
|
386
388
|
);
|
|
387
389
|
}
|
|
388
390
|
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
391
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "no-active-milestone" } });
|
|
389
392
|
return { action: "break", reason: "no-active-milestone" };
|
|
390
393
|
}
|
|
391
394
|
|
|
@@ -454,6 +457,7 @@ export async function runPreDispatch(
|
|
|
454
457
|
);
|
|
455
458
|
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
456
459
|
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
460
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "milestone-complete", milestoneId: mid } });
|
|
457
461
|
return { action: "break", reason: "milestone-complete" };
|
|
458
462
|
}
|
|
459
463
|
|
|
@@ -465,6 +469,7 @@ export async function runPreDispatch(
|
|
|
465
469
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
466
470
|
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
467
471
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
472
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
|
|
468
473
|
return { action: "break", reason: "blocked" };
|
|
469
474
|
}
|
|
470
475
|
|
|
@@ -497,6 +502,7 @@ export async function runDispatch(
|
|
|
497
502
|
});
|
|
498
503
|
|
|
499
504
|
if (dispatchResult.action === "stop") {
|
|
505
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "dispatch-stop", rule: dispatchResult.matchedRule, data: { reason: dispatchResult.reason } });
|
|
500
506
|
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
501
507
|
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
502
508
|
return { action: "break", reason: "dispatch-stop" };
|
|
@@ -508,6 +514,8 @@ export async function runDispatch(
|
|
|
508
514
|
return { action: "continue" };
|
|
509
515
|
}
|
|
510
516
|
|
|
517
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "dispatch-match", rule: dispatchResult.matchedRule, data: { unitType: dispatchResult.unitType, unitId: dispatchResult.unitId } });
|
|
518
|
+
|
|
511
519
|
let unitType = dispatchResult.unitType;
|
|
512
520
|
let unitId = dispatchResult.unitId;
|
|
513
521
|
let prompt = dispatchResult.prompt;
|
|
@@ -600,6 +608,7 @@ export async function runDispatch(
|
|
|
600
608
|
`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
|
|
601
609
|
"info",
|
|
602
610
|
);
|
|
611
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
|
|
603
612
|
}
|
|
604
613
|
if (preDispatchResult.action === "skip") {
|
|
605
614
|
ctx.ui.notify(
|
|
@@ -809,25 +818,27 @@ export async function runUnitPhase(
|
|
|
809
818
|
unitId,
|
|
810
819
|
});
|
|
811
820
|
|
|
812
|
-
// ── Worktree health check (#1833)
|
|
821
|
+
// ── Worktree health check (#1833, #1843) ────────────────────────────
|
|
813
822
|
// Verify the working directory is a valid git checkout with project
|
|
814
823
|
// files before dispatching work. A broken worktree causes agents to
|
|
815
824
|
// hallucinate summaries since they cannot read or write any files.
|
|
825
|
+
// Uses the shared PROJECT_FILES list from detection.ts to support all
|
|
826
|
+
// ecosystems (Rust, Go, Python, Java, etc.), not just JS.
|
|
816
827
|
if (s.basePath && unitType === "execute-task") {
|
|
817
828
|
const gitMarker = join(s.basePath, ".git");
|
|
818
829
|
const hasGit = deps.existsSync(gitMarker);
|
|
819
|
-
const hasPackageJson = deps.existsSync(join(s.basePath, "package.json"));
|
|
820
|
-
const hasSrcDir = deps.existsSync(join(s.basePath, "src"));
|
|
821
830
|
if (!hasGit) {
|
|
822
831
|
const msg = `Worktree health check failed: ${s.basePath} has no .git — refusing to dispatch ${unitType} ${unitId}`;
|
|
823
|
-
debugLog("runUnitPhase", { phase: "worktree-health-fail", basePath: s.basePath, hasGit
|
|
832
|
+
debugLog("runUnitPhase", { phase: "worktree-health-fail", basePath: s.basePath, hasGit });
|
|
824
833
|
ctx.ui.notify(msg, "error");
|
|
825
834
|
await deps.stopAuto(ctx, pi, msg);
|
|
826
835
|
return { action: "break", reason: "worktree-invalid" };
|
|
827
836
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
837
|
+
const hasProjectFile = PROJECT_FILES.some((f) => deps.existsSync(join(s.basePath, f)));
|
|
838
|
+
const hasSrcDir = deps.existsSync(join(s.basePath, "src"));
|
|
839
|
+
if (!hasProjectFile && !hasSrcDir) {
|
|
840
|
+
const msg = `Worktree health check failed: ${s.basePath} has no recognized project files — refusing to dispatch ${unitType} ${unitId}`;
|
|
841
|
+
debugLog("runUnitPhase", { phase: "worktree-health-fail", basePath: s.basePath, hasProjectFile, hasSrcDir });
|
|
831
842
|
ctx.ui.notify(msg, "error");
|
|
832
843
|
await deps.stopAuto(ctx, pi, msg);
|
|
833
844
|
return { action: "break", reason: "worktree-invalid" };
|
|
@@ -843,6 +854,8 @@ export async function runUnitPhase(
|
|
|
843
854
|
const previousTier = s.currentUnitRouting?.tier;
|
|
844
855
|
|
|
845
856
|
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
857
|
+
const unitStartSeq = ic.nextSeq();
|
|
858
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
|
|
846
859
|
deps.captureAvailableSkills();
|
|
847
860
|
deps.writeUnitRuntimeRecord(
|
|
848
861
|
s.basePath,
|
|
@@ -988,7 +1001,12 @@ export async function runUnitPhase(
|
|
|
988
1001
|
unitId,
|
|
989
1002
|
prefs,
|
|
990
1003
|
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
991
|
-
buildRecoveryContext: () => ({
|
|
1004
|
+
buildRecoveryContext: () => ({
|
|
1005
|
+
basePath: s.basePath,
|
|
1006
|
+
verbose: s.verbose,
|
|
1007
|
+
currentUnitStartedAt: s.currentUnit?.startedAt ?? Date.now(),
|
|
1008
|
+
unitRecoveryCount: s.unitRecoveryCount,
|
|
1009
|
+
}),
|
|
992
1010
|
pauseAuto: deps.pauseAuto,
|
|
993
1011
|
});
|
|
994
1012
|
|
|
@@ -1141,6 +1159,8 @@ export async function runUnitPhase(
|
|
|
1141
1159
|
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
1142
1160
|
}
|
|
1143
1161
|
|
|
1162
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "unit-end", data: { unitType, unitId, status: unitResult.status, artifactVerified }, causedBy: { flowId: ic.flowId, seq: unitStartSeq } });
|
|
1163
|
+
|
|
1144
1164
|
return { action: "next", data: { unitStartedAt: s.currentUnit.startedAt } };
|
|
1145
1165
|
}
|
|
1146
1166
|
|
|
@@ -69,6 +69,10 @@ export interface IterationContext {
|
|
|
69
69
|
deps: LoopDeps;
|
|
70
70
|
prefs: GSDPreferences | undefined;
|
|
71
71
|
iteration: number;
|
|
72
|
+
/** UUID grouping all journal events for this iteration. */
|
|
73
|
+
flowId: string;
|
|
74
|
+
/** Returns the next monotonically increasing sequence number (1-based, reset per iteration). */
|
|
75
|
+
nextSeq: () => number;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
export interface LoopState {
|
|
@@ -24,6 +24,7 @@ import { GLYPH, INDENT } from "../shared/mod.js";
|
|
|
24
24
|
import { computeProgressScore } from "./progress-score.js";
|
|
25
25
|
import { getActiveWorktreeName } from "./worktree-command.js";
|
|
26
26
|
import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
|
|
27
|
+
import { resolveServiceTierIcon, getEffectiveServiceTier } from "./service-tier.js";
|
|
27
28
|
|
|
28
29
|
// ─── UAT Slice Extraction ─────────────────────────────────────────────────────
|
|
29
30
|
|
|
@@ -460,6 +461,9 @@ export function updateProgressWidget(
|
|
|
460
461
|
// Pre-fetch last commit for display
|
|
461
462
|
refreshLastCommit(accessors.getBasePath());
|
|
462
463
|
|
|
464
|
+
// Cache the effective service tier at widget creation time (reads preferences)
|
|
465
|
+
const effectiveServiceTier = getEffectiveServiceTier();
|
|
466
|
+
|
|
463
467
|
ctx.ui.setWidget("gsd-progress", (tui, theme) => {
|
|
464
468
|
let pulseBright = true;
|
|
465
469
|
let cachedLines: string[] | undefined;
|
|
@@ -572,9 +576,10 @@ export function updateProgressWidget(
|
|
|
572
576
|
// Model display — shown in context section, not stats
|
|
573
577
|
const modelId = cmdCtx?.model?.id ?? "";
|
|
574
578
|
const modelProvider = cmdCtx?.model?.provider ?? "";
|
|
575
|
-
const
|
|
579
|
+
const tierIcon = resolveServiceTierIcon(effectiveServiceTier, modelId);
|
|
580
|
+
const modelDisplay = (modelProvider && modelId
|
|
576
581
|
? `${modelProvider}/${modelId}`
|
|
577
|
-
: modelId;
|
|
582
|
+
: modelId) + (tierIcon ? ` ${tierIcon}` : "");
|
|
578
583
|
|
|
579
584
|
// ── Mode: off — return empty ──────────────────────────────────
|
|
580
585
|
if (widgetMode === "off") {
|
|
@@ -54,9 +54,11 @@ export type DispatchAction =
|
|
|
54
54
|
unitId: string;
|
|
55
55
|
prompt: string;
|
|
56
56
|
pauseAfterDispatch?: boolean;
|
|
57
|
+
/** Name of the matched dispatch rule from the unified registry (journal provenance). */
|
|
58
|
+
matchedRule?: string;
|
|
57
59
|
}
|
|
58
|
-
| { action: "stop"; reason: string; level: "info" | "warning" | "error" }
|
|
59
|
-
| { action: "skip" };
|
|
60
|
+
| { action: "stop"; reason: string; level: "info" | "warning" | "error"; matchedRule?: string }
|
|
61
|
+
| { action: "skip"; matchedRule?: string };
|
|
60
62
|
|
|
61
63
|
export interface DispatchContext {
|
|
62
64
|
basePath: string;
|
|
@@ -67,7 +69,7 @@ export interface DispatchContext {
|
|
|
67
69
|
session?: import("./auto/session.js").AutoSession;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
interface DispatchRule {
|
|
72
|
+
export interface DispatchRule {
|
|
71
73
|
/** Human-readable name for debugging and test identification */
|
|
72
74
|
name: string;
|
|
73
75
|
/** Return a DispatchAction if this rule matches, null to fall through */
|
|
@@ -88,7 +90,7 @@ const MAX_REWRITE_ATTEMPTS = 3;
|
|
|
88
90
|
|
|
89
91
|
// ─── Rules ────────────────────────────────────────────────────────────────
|
|
90
92
|
|
|
91
|
-
const DISPATCH_RULES: DispatchRule[] = [
|
|
93
|
+
export const DISPATCH_RULES: DispatchRule[] = [
|
|
92
94
|
{
|
|
93
95
|
name: "rewrite-docs (override gate)",
|
|
94
96
|
match: async ({ mid, midTitle, state, basePath, session }) => {
|
|
@@ -608,18 +610,35 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
608
610
|
},
|
|
609
611
|
];
|
|
610
612
|
|
|
613
|
+
import { getRegistry } from "./rule-registry.js";
|
|
614
|
+
|
|
611
615
|
// ─── Resolver ─────────────────────────────────────────────────────────────
|
|
612
616
|
|
|
613
617
|
/**
|
|
614
618
|
* Evaluate dispatch rules in order. Returns the first matching action,
|
|
615
619
|
* or a "stop" action if no rule matches (unhandled phase).
|
|
620
|
+
*
|
|
621
|
+
* Delegates to the RuleRegistry when initialized; falls back to inline
|
|
622
|
+
* loop over DISPATCH_RULES for backward compatibility (tests that import
|
|
623
|
+
* resolveDispatch directly without registry initialization).
|
|
616
624
|
*/
|
|
617
625
|
export async function resolveDispatch(
|
|
618
626
|
ctx: DispatchContext,
|
|
619
627
|
): Promise<DispatchAction> {
|
|
628
|
+
// Delegate to registry when available
|
|
629
|
+
try {
|
|
630
|
+
const registry = getRegistry();
|
|
631
|
+
return await registry.evaluateDispatch(ctx);
|
|
632
|
+
} catch {
|
|
633
|
+
// Registry not initialized — fall back to inline loop
|
|
634
|
+
}
|
|
635
|
+
|
|
620
636
|
for (const rule of DISPATCH_RULES) {
|
|
621
637
|
const result = await rule.match(ctx);
|
|
622
|
-
if (result)
|
|
638
|
+
if (result) {
|
|
639
|
+
if (result.action !== "skip") result.matchedRule = rule.name;
|
|
640
|
+
return result;
|
|
641
|
+
}
|
|
623
642
|
}
|
|
624
643
|
|
|
625
644
|
// No rule matched — unhandled phase
|
|
@@ -627,6 +646,7 @@ export async function resolveDispatch(
|
|
|
627
646
|
action: "stop",
|
|
628
647
|
reason: `Unhandled phase "${ctx.state.phase}" — run /gsd doctor to diagnose.`,
|
|
629
648
|
level: "info",
|
|
649
|
+
matchedRule: "<no-match>",
|
|
630
650
|
};
|
|
631
651
|
}
|
|
632
652
|
|
|
@@ -59,6 +59,7 @@ import { existsSync, unlinkSync } from "node:fs";
|
|
|
59
59
|
import { join } from "node:path";
|
|
60
60
|
import { uncheckTaskInPlan } from "./undo.js";
|
|
61
61
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
62
|
+
import { _resetHasChangesCache } from "./native-git-bridge.js";
|
|
62
63
|
|
|
63
64
|
/** Throttle STATE.md rebuilds — at most once per 30 seconds */
|
|
64
65
|
const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
@@ -156,6 +157,13 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
// Invalidate the nativeHasChanges cache before auto-commit (#1853).
|
|
161
|
+
// The cache has a 10-second TTL and is keyed by basePath. A stale
|
|
162
|
+
// `false` result causes autoCommit to skip staging entirely, leaving
|
|
163
|
+
// code files only in the working tree where they are destroyed by
|
|
164
|
+
// `git worktree remove --force` during teardown.
|
|
165
|
+
_resetHasChangesCache();
|
|
166
|
+
|
|
159
167
|
const commitMsg = autoCommitCurrentBranch(s.basePath, s.currentUnit.type, s.currentUnit.id, taskContext);
|
|
160
168
|
if (commitMsg) {
|
|
161
169
|
ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
|
|
@@ -319,10 +319,15 @@ export function verifyExpectedArtifact(
|
|
|
319
319
|
// plan has no tasks, creating an infinite skip loop (#699).
|
|
320
320
|
if (unitType === "plan-slice") {
|
|
321
321
|
const planContent = readFileSync(absPath, "utf-8");
|
|
322
|
-
|
|
322
|
+
// Accept checkbox-style (- [x] **T01: ...) or heading-style (### T01 -- / ### T01: / ### T01 —)
|
|
323
|
+
const hasCheckboxTask = /^- \[[xX ]\] \*\*T\d+:/m.test(planContent);
|
|
324
|
+
const hasHeadingTask = /^#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
|
|
325
|
+
if (!hasCheckboxTask && !hasHeadingTask) return false;
|
|
323
326
|
}
|
|
324
327
|
|
|
325
|
-
// execute-task must also have its checkbox marked [x] in the slice plan
|
|
328
|
+
// execute-task must also have its checkbox marked [x] in the slice plan.
|
|
329
|
+
// Heading-style plans (### T01 -- Title) have no checkbox — the task summary
|
|
330
|
+
// file existence (checked above via resolveExpectedArtifactPath) is sufficient.
|
|
326
331
|
if (unitType === "execute-task") {
|
|
327
332
|
const parts = unitId.split("/");
|
|
328
333
|
const mid = parts[0];
|
|
@@ -333,8 +338,11 @@ export function verifyExpectedArtifact(
|
|
|
333
338
|
if (planAbs && existsSync(planAbs)) {
|
|
334
339
|
const planContent = readFileSync(planAbs, "utf-8");
|
|
335
340
|
const escapedTid = tid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
336
|
-
const
|
|
337
|
-
|
|
341
|
+
const cbRe = new RegExp(`^- \\[[xX]\\] \\*\\*${escapedTid}:`, "m");
|
|
342
|
+
const hdRe = new RegExp(`^#{2,4}\\s+${escapedTid}\\s*(?:--|—|:)`, "m");
|
|
343
|
+
// Heading-style entries count as verified (no checkbox to toggle);
|
|
344
|
+
// checkbox-style entries require [x].
|
|
345
|
+
if (!cbRe.test(planContent) && !hdRe.test(planContent)) return false;
|
|
338
346
|
}
|
|
339
347
|
}
|
|
340
348
|
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
resolveSkillDiscoveryMode,
|
|
21
21
|
getIsolationMode,
|
|
22
22
|
} from "./preferences.js";
|
|
23
|
-
import { ensureGsdSymlink, validateProjectId } from "./repo-identity.js";
|
|
23
|
+
import { ensureGsdSymlink, isInheritedRepo, validateProjectId } from "./repo-identity.js";
|
|
24
24
|
import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
|
|
25
25
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
26
26
|
import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
|
|
@@ -140,8 +140,13 @@ export async function bootstrapAutoSession(
|
|
|
140
140
|
return releaseLockAndReturn();
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
// Ensure git repo exists
|
|
144
|
-
if
|
|
143
|
+
// Ensure git repo exists.
|
|
144
|
+
// Guard against inherited repos: if `base` is a subdirectory of another
|
|
145
|
+
// git repo that has no .gsd (i.e. the parent project was never initialised
|
|
146
|
+
// with GSD), create a fresh git repo at `base` so it gets its own identity
|
|
147
|
+
// hash. Without this, repoIdentity() resolves to the parent repo's hash
|
|
148
|
+
// and loads milestones from an unrelated project (#1639).
|
|
149
|
+
if (!nativeIsRepo(base) || isInheritedRepo(base)) {
|
|
145
150
|
const mainBranch =
|
|
146
151
|
loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
|
|
147
152
|
nativeInit(base, mainBranch);
|
|
@@ -25,12 +25,13 @@ import {
|
|
|
25
25
|
isDbAvailable,
|
|
26
26
|
} from "./gsd-db.js";
|
|
27
27
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
28
|
-
import {
|
|
28
|
+
import { execFileSync } from "node:child_process";
|
|
29
29
|
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
|
|
30
30
|
import { gsdRoot } from "./paths.js";
|
|
31
31
|
import {
|
|
32
32
|
createWorktree,
|
|
33
33
|
removeWorktree,
|
|
34
|
+
resolveGitDir,
|
|
34
35
|
worktreePath,
|
|
35
36
|
} from "./worktree-manager.js";
|
|
36
37
|
import {
|
|
@@ -57,6 +58,8 @@ import {
|
|
|
57
58
|
nativeBranchDelete,
|
|
58
59
|
nativeBranchExists,
|
|
59
60
|
nativeDiffNumstat,
|
|
61
|
+
nativeUpdateRef,
|
|
62
|
+
nativeIsAncestor,
|
|
60
63
|
} from "./native-git-bridge.js";
|
|
61
64
|
|
|
62
65
|
// ─── Module State ──────────────────────────────────────────────────────────
|
|
@@ -182,7 +185,7 @@ export function syncGsdStateToWorktree(
|
|
|
182
185
|
const mainMilestones = readdirSync(mainMilestonesDir, {
|
|
183
186
|
withFileTypes: true,
|
|
184
187
|
})
|
|
185
|
-
.filter((d) => d.isDirectory()
|
|
188
|
+
.filter((d) => d.isDirectory())
|
|
186
189
|
.map((d) => d.name);
|
|
187
190
|
|
|
188
191
|
for (const mid of mainMilestones) {
|
|
@@ -339,7 +342,7 @@ export function syncWorktreeStateBack(
|
|
|
339
342
|
|
|
340
343
|
try {
|
|
341
344
|
const wtMilestones = readdirSync(wtMilestonesDir, { withFileTypes: true })
|
|
342
|
-
.filter((d) => d.isDirectory()
|
|
345
|
+
.filter((d) => d.isDirectory())
|
|
343
346
|
.map((d) => d.name);
|
|
344
347
|
|
|
345
348
|
for (const mid of wtMilestones) {
|
|
@@ -468,14 +471,22 @@ export function runWorktreePostCreateHook(
|
|
|
468
471
|
}
|
|
469
472
|
if (!hookPath) return null;
|
|
470
473
|
|
|
471
|
-
// Resolve relative paths against the source project root
|
|
472
|
-
|
|
474
|
+
// Resolve relative paths against the source project root.
|
|
475
|
+
// On Windows, convert 8.3 short paths (e.g. RUNNER~1) to long paths
|
|
476
|
+
// so execFileSync can locate the file correctly.
|
|
477
|
+
let resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
|
|
473
478
|
if (!existsSync(resolved)) {
|
|
474
479
|
return `Worktree post-create hook not found: ${resolved}`;
|
|
475
480
|
}
|
|
481
|
+
if (process.platform === "win32") {
|
|
482
|
+
try { resolved = realpathSync.native(resolved); } catch { /* keep original */ }
|
|
483
|
+
}
|
|
476
484
|
|
|
477
485
|
try {
|
|
478
|
-
|
|
486
|
+
// .bat/.cmd files on Windows require shell mode — execFileSync cannot
|
|
487
|
+
// spawn them directly (EINVAL).
|
|
488
|
+
const needsShell = process.platform === "win32" && /\.(bat|cmd)$/i.test(resolved);
|
|
489
|
+
execFileSync(resolved, [], {
|
|
479
490
|
cwd: worktreeDir,
|
|
480
491
|
env: {
|
|
481
492
|
...process.env,
|
|
@@ -485,6 +496,7 @@ export function runWorktreePostCreateHook(
|
|
|
485
496
|
stdio: ["ignore", "pipe", "pipe"],
|
|
486
497
|
encoding: "utf-8",
|
|
487
498
|
timeout: 30_000, // 30 second timeout
|
|
499
|
+
shell: needsShell,
|
|
488
500
|
});
|
|
489
501
|
return null;
|
|
490
502
|
} catch (err) {
|
|
@@ -761,6 +773,24 @@ export function teardownAutoWorktree(
|
|
|
761
773
|
branch,
|
|
762
774
|
deleteBranch: !preserveBranch,
|
|
763
775
|
});
|
|
776
|
+
|
|
777
|
+
// Verify cleanup succeeded — warn if the worktree directory is still on disk.
|
|
778
|
+
// On Windows, bash-based cleanup can silently fail when paths contain
|
|
779
|
+
// backslashes (#1436), leaving ~1 GB+ orphaned directories.
|
|
780
|
+
const wtDir = worktreePath(originalBasePath, milestoneId);
|
|
781
|
+
if (existsSync(wtDir)) {
|
|
782
|
+
console.error(
|
|
783
|
+
`[GSD] WARNING: Worktree directory still exists after teardown: ${wtDir}\n` +
|
|
784
|
+
` This is likely an orphaned directory consuming disk space.\n` +
|
|
785
|
+
` Remove it manually with: rm -rf "${wtDir.replaceAll("\\", "/")}"`,
|
|
786
|
+
);
|
|
787
|
+
// Attempt a direct filesystem removal as a fallback
|
|
788
|
+
try {
|
|
789
|
+
rmSync(wtDir, { recursive: true, force: true });
|
|
790
|
+
} catch {
|
|
791
|
+
// Non-fatal — the warning above tells the user how to clean up
|
|
792
|
+
}
|
|
793
|
+
}
|
|
764
794
|
}
|
|
765
795
|
|
|
766
796
|
/**
|
|
@@ -940,7 +970,7 @@ export function mergeMilestoneToMain(
|
|
|
940
970
|
originalBasePath_: string,
|
|
941
971
|
milestoneId: string,
|
|
942
972
|
roadmapContent: string,
|
|
943
|
-
): { commitMessage: string; pushed: boolean; prCreated: boolean } {
|
|
973
|
+
): { commitMessage: string; pushed: boolean; prCreated: boolean; codeFilesChanged: boolean } {
|
|
944
974
|
const worktreeCwd = process.cwd();
|
|
945
975
|
const milestoneBranch = autoWorktreeBranch(milestoneId);
|
|
946
976
|
|
|
@@ -1002,6 +1032,62 @@ export function mergeMilestoneToMain(
|
|
|
1002
1032
|
}
|
|
1003
1033
|
const commitMessage = subject + body;
|
|
1004
1034
|
|
|
1035
|
+
// 6b. Reconcile worktree HEAD with milestone branch ref (#1846).
|
|
1036
|
+
// When the worktree HEAD detaches and advances past the named branch,
|
|
1037
|
+
// the branch ref becomes stale. Squash-merging the stale ref silently
|
|
1038
|
+
// orphans all commits between the branch ref and the actual worktree HEAD.
|
|
1039
|
+
// Fix: fast-forward the branch ref to the worktree HEAD before merging.
|
|
1040
|
+
// Only applies when merging from an actual worktree (worktreeCwd differs
|
|
1041
|
+
// from originalBasePath_).
|
|
1042
|
+
if (worktreeCwd !== originalBasePath_) {
|
|
1043
|
+
try {
|
|
1044
|
+
const worktreeHead = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
1045
|
+
cwd: worktreeCwd,
|
|
1046
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1047
|
+
encoding: "utf-8",
|
|
1048
|
+
}).trim();
|
|
1049
|
+
const branchHead = execFileSync("git", ["rev-parse", milestoneBranch], {
|
|
1050
|
+
cwd: originalBasePath_,
|
|
1051
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1052
|
+
encoding: "utf-8",
|
|
1053
|
+
}).trim();
|
|
1054
|
+
|
|
1055
|
+
if (worktreeHead && branchHead && worktreeHead !== branchHead) {
|
|
1056
|
+
if (nativeIsAncestor(originalBasePath_, branchHead, worktreeHead)) {
|
|
1057
|
+
// Worktree HEAD is strictly ahead — fast-forward the branch ref
|
|
1058
|
+
nativeUpdateRef(
|
|
1059
|
+
originalBasePath_,
|
|
1060
|
+
`refs/heads/${milestoneBranch}`,
|
|
1061
|
+
worktreeHead,
|
|
1062
|
+
);
|
|
1063
|
+
debugLog("mergeMilestoneToMain", {
|
|
1064
|
+
action: "fast-forward-branch-ref",
|
|
1065
|
+
milestoneBranch,
|
|
1066
|
+
oldRef: branchHead.slice(0, 8),
|
|
1067
|
+
newRef: worktreeHead.slice(0, 8),
|
|
1068
|
+
});
|
|
1069
|
+
} else {
|
|
1070
|
+
// Diverged — fail loudly rather than silently losing commits
|
|
1071
|
+
process.chdir(previousCwd);
|
|
1072
|
+
throw new GSDError(
|
|
1073
|
+
GSD_GIT_ERROR,
|
|
1074
|
+
`Worktree HEAD (${worktreeHead.slice(0, 8)}) diverged from ` +
|
|
1075
|
+
`${milestoneBranch} (${branchHead.slice(0, 8)}). ` +
|
|
1076
|
+
`Manual reconciliation required before merge.`,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
// Re-throw GSDError (divergence); swallow rev-parse failures
|
|
1082
|
+
// (e.g. worktree dir already removed by external cleanup)
|
|
1083
|
+
if (err instanceof GSDError) throw err;
|
|
1084
|
+
debugLog("mergeMilestoneToMain", {
|
|
1085
|
+
action: "reconcile-skipped",
|
|
1086
|
+
reason: String(err),
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1005
1091
|
// 7. Squash merge — auto-resolve .gsd/ state file conflicts (#530)
|
|
1006
1092
|
const mergeResult = nativeMergeSquash(originalBasePath_, milestoneBranch);
|
|
1007
1093
|
|
|
@@ -1066,6 +1152,16 @@ export function mergeMilestoneToMain(
|
|
|
1066
1152
|
const commitResult = nativeCommit(originalBasePath_, commitMessage);
|
|
1067
1153
|
const nothingToCommit = commitResult === null;
|
|
1068
1154
|
|
|
1155
|
+
// 8a. Clean up SQUASH_MSG left by git merge --squash (#1853).
|
|
1156
|
+
// git only removes SQUASH_MSG when the commit reads it directly (plain
|
|
1157
|
+
// `git commit`). nativeCommit uses `-F -` (stdin) or libgit2, neither
|
|
1158
|
+
// of which trigger git's SQUASH_MSG cleanup. If left on disk, doctor
|
|
1159
|
+
// reports `corrupt_merge_state` on every subsequent run.
|
|
1160
|
+
try {
|
|
1161
|
+
const squashMsgPath = join(resolveGitDir(originalBasePath_), "SQUASH_MSG");
|
|
1162
|
+
if (existsSync(squashMsgPath)) unlinkSync(squashMsgPath);
|
|
1163
|
+
} catch { /* best-effort */ }
|
|
1164
|
+
|
|
1069
1165
|
// 8b. Safety check (#1792): if nothing was committed, verify the milestone
|
|
1070
1166
|
// work is already on the integration branch before allowing teardown.
|
|
1071
1167
|
// Compare only non-.gsd/ paths — .gsd/ state files diverge normally and
|
|
@@ -1091,12 +1187,33 @@ export function mergeMilestoneToMain(
|
|
|
1091
1187
|
}
|
|
1092
1188
|
}
|
|
1093
1189
|
|
|
1190
|
+
// 8c. Detect whether any non-.gsd/ code files were actually merged (#1906).
|
|
1191
|
+
// When a milestone only produced .gsd/ metadata (summaries, roadmaps) but no
|
|
1192
|
+
// real code, the user sees "milestone complete" but nothing changed in their
|
|
1193
|
+
// codebase. Surface this so the caller can warn the user.
|
|
1194
|
+
let codeFilesChanged = false;
|
|
1195
|
+
if (!nothingToCommit) {
|
|
1196
|
+
try {
|
|
1197
|
+
const mergedFiles = nativeDiffNumstat(
|
|
1198
|
+
originalBasePath_,
|
|
1199
|
+
"HEAD~1",
|
|
1200
|
+
"HEAD",
|
|
1201
|
+
);
|
|
1202
|
+
codeFilesChanged = mergedFiles.some(
|
|
1203
|
+
(entry) => !entry.path.startsWith(".gsd/"),
|
|
1204
|
+
);
|
|
1205
|
+
} catch {
|
|
1206
|
+
// If HEAD~1 doesn't exist (first commit), assume code was changed
|
|
1207
|
+
codeFilesChanged = true;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1094
1211
|
// 9. Auto-push if enabled
|
|
1095
1212
|
let pushed = false;
|
|
1096
1213
|
if (prefs.auto_push === true && !nothingToCommit) {
|
|
1097
1214
|
const remote = prefs.remote ?? "origin";
|
|
1098
1215
|
try {
|
|
1099
|
-
|
|
1216
|
+
execFileSync("git", ["push", remote, mainBranch], {
|
|
1100
1217
|
cwd: originalBasePath_,
|
|
1101
1218
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1102
1219
|
encoding: "utf-8",
|
|
@@ -1114,20 +1231,23 @@ export function mergeMilestoneToMain(
|
|
|
1114
1231
|
const prTarget = prefs.pr_target_branch ?? mainBranch;
|
|
1115
1232
|
try {
|
|
1116
1233
|
// Push the milestone branch to remote first
|
|
1117
|
-
|
|
1234
|
+
execFileSync("git", ["push", remote, milestoneBranch], {
|
|
1118
1235
|
cwd: originalBasePath_,
|
|
1119
1236
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1120
1237
|
encoding: "utf-8",
|
|
1121
1238
|
});
|
|
1122
1239
|
// Create PR via gh CLI
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1240
|
+
execFileSync("gh", [
|
|
1241
|
+
"pr", "create",
|
|
1242
|
+
"--base", prTarget,
|
|
1243
|
+
"--head", milestoneBranch,
|
|
1244
|
+
"--title", `Milestone ${milestoneId} complete`,
|
|
1245
|
+
"--body", "Auto-created by GSD on milestone completion.",
|
|
1246
|
+
], {
|
|
1247
|
+
cwd: originalBasePath_,
|
|
1248
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1249
|
+
encoding: "utf-8",
|
|
1250
|
+
});
|
|
1131
1251
|
prCreated = true;
|
|
1132
1252
|
} catch {
|
|
1133
1253
|
// PR creation failure is non-fatal — gh may not be installed or authenticated
|
|
@@ -1138,6 +1258,30 @@ export function mergeMilestoneToMain(
|
|
|
1138
1258
|
// throws only when the milestone has unanchored code changes, passes
|
|
1139
1259
|
// through when the code is genuinely already on the integration branch.
|
|
1140
1260
|
|
|
1261
|
+
// 10a. Pre-teardown safety net (#1853): if the worktree still has uncommitted
|
|
1262
|
+
// changes (e.g. nativeHasChanges cache returned stale false, or auto-commit
|
|
1263
|
+
// silently failed), force one final commit so code is not destroyed by
|
|
1264
|
+
// `git worktree remove --force`.
|
|
1265
|
+
if (existsSync(worktreeCwd)) {
|
|
1266
|
+
try {
|
|
1267
|
+
const dirtyCheck = nativeWorkingTreeStatus(worktreeCwd);
|
|
1268
|
+
if (dirtyCheck) {
|
|
1269
|
+
debugLog("mergeMilestoneToMain", {
|
|
1270
|
+
phase: "pre-teardown-dirty",
|
|
1271
|
+
worktreeCwd,
|
|
1272
|
+
status: dirtyCheck.slice(0, 200),
|
|
1273
|
+
});
|
|
1274
|
+
nativeAddAllWithExclusions(worktreeCwd, RUNTIME_EXCLUSION_PATHS);
|
|
1275
|
+
nativeCommit(worktreeCwd, "chore: pre-teardown auto-commit of uncommitted worktree changes");
|
|
1276
|
+
}
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
debugLog("mergeMilestoneToMain", {
|
|
1279
|
+
phase: "pre-teardown-commit-error",
|
|
1280
|
+
error: String(e),
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1141
1285
|
// 11. Remove worktree directory first (must happen before branch deletion)
|
|
1142
1286
|
try {
|
|
1143
1287
|
removeWorktree(originalBasePath_, milestoneId, {
|
|
@@ -1159,5 +1303,5 @@ export function mergeMilestoneToMain(
|
|
|
1159
1303
|
originalBase = null;
|
|
1160
1304
|
nudgeGitBranchCache(previousCwd);
|
|
1161
1305
|
|
|
1162
|
-
return { commitMessage, pushed, prCreated };
|
|
1306
|
+
return { commitMessage, pushed, prCreated, codeFilesChanged };
|
|
1163
1307
|
}
|