gsd-pi 2.79.0-dev.5c910bb05 → 2.79.0-dev.9941c9c24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/contracts.js +1 -0
  3. package/dist/resources/extensions/gsd/auto/orchestrator.js +146 -0
  4. package/dist/resources/extensions/gsd/auto/phases.js +61 -7
  5. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  6. package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  8. package/dist/resources/extensions/gsd/auto-recovery.js +63 -55
  9. package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
  10. package/dist/resources/extensions/gsd/auto-start.js +3 -2
  11. package/dist/resources/extensions/gsd/auto.js +159 -2
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -8
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
  15. package/dist/resources/extensions/gsd/gsd-db.js +34 -1
  16. package/dist/resources/extensions/gsd/guided-flow.js +40 -0
  17. package/dist/resources/extensions/gsd/paths.js +5 -1
  18. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +45 -4
  19. package/dist/resources/extensions/gsd/uok/audit.js +23 -9
  20. package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
  21. package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
  22. package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
  23. package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
  24. package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
  25. package/dist/resources/extensions/shared/interview-ui.js +15 -4
  26. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  27. package/dist/web/standalone/.next/BUILD_ID +1 -1
  28. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  29. package/dist/web/standalone/.next/build-manifest.json +2 -2
  30. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.html +1 -1
  48. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  55. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  57. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  58. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  59. package/package.json +1 -1
  60. package/packages/mcp-server/src/workflow-tools.test.ts +13 -2
  61. package/src/resources/extensions/gsd/auto/contracts.ts +87 -0
  62. package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -3
  63. package/src/resources/extensions/gsd/auto/orchestrator.ts +161 -0
  64. package/src/resources/extensions/gsd/auto/phases.ts +88 -9
  65. package/src/resources/extensions/gsd/auto/session.ts +11 -0
  66. package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
  67. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  68. package/src/resources/extensions/gsd/auto-recovery.ts +59 -53
  69. package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
  70. package/src/resources/extensions/gsd/auto-start.ts +3 -2
  71. package/src/resources/extensions/gsd/auto.ts +167 -1
  72. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +14 -1
  73. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +11 -8
  74. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
  75. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
  76. package/src/resources/extensions/gsd/gsd-db.ts +35 -1
  77. package/src/resources/extensions/gsd/guided-flow.ts +47 -0
  78. package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
  79. package/src/resources/extensions/gsd/paths.ts +6 -1
  80. package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +32 -0
  81. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +353 -0
  82. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
  83. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +39 -0
  84. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +3 -0
  85. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
  86. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
  87. package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
  88. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
  89. package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
  90. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
  91. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +79 -0
  92. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +134 -0
  93. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +2 -0
  94. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +27 -0
  95. package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
  96. package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
  97. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +36 -7
  98. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +47 -4
  99. package/src/resources/extensions/gsd/uok/audit.ts +25 -9
  100. package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
  101. package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
  102. package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
  103. package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
  104. package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
  105. package/src/resources/extensions/shared/interview-ui.ts +18 -5
  106. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
  107. package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
  108. /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → zSHLdEn5cpkqivbJZq0Qq}/_buildManifest.js +0 -0
  109. /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → zSHLdEn5cpkqivbJZq0Qq}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- dbdfc4ddfea7fdce
1
+ b9c084c2ccf7748d
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,146 @@
1
+ function now() {
2
+ return Date.now();
3
+ }
4
+ export class AutoOrchestrator {
5
+ status = {
6
+ phase: "idle",
7
+ transitionCount: 0,
8
+ };
9
+ deps;
10
+ lastAdvanceKey = null;
11
+ constructor(deps) {
12
+ this.deps = deps;
13
+ }
14
+ async start(_sessionContext) {
15
+ this.lastAdvanceKey = null;
16
+ this.status.phase = "running";
17
+ this.bumpTransition();
18
+ await this.deps.runtime.journalTransition({ name: "start" });
19
+ await this.deps.notifications.notifyLifecycle({ name: "start" });
20
+ return this.advance();
21
+ }
22
+ async advance() {
23
+ try {
24
+ await this.deps.runtime.ensureLockOwnership();
25
+ const gate = await this.deps.health.preAdvanceGate();
26
+ if (!gate.allow) {
27
+ const blocked = { kind: "blocked", reason: gate.reason ?? "health gate blocked" };
28
+ await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
29
+ await this.deps.health.postAdvanceRecord(blocked);
30
+ return blocked;
31
+ }
32
+ const decision = await this.deps.dispatch.decideNextUnit();
33
+ if (!decision) {
34
+ const stopped = { kind: "stopped", reason: "no remaining units" };
35
+ this.status.phase = "stopped";
36
+ this.status.activeUnit = undefined;
37
+ this.lastAdvanceKey = null;
38
+ this.bumpTransition();
39
+ await this.deps.runtime.journalTransition({ name: "advance-stopped", reason: stopped.reason });
40
+ await this.deps.health.postAdvanceRecord(stopped);
41
+ return stopped;
42
+ }
43
+ const nextKey = `${decision.unitType}:${decision.unitId}`;
44
+ if (this.lastAdvanceKey === nextKey) {
45
+ const blocked = { kind: "blocked", reason: "idempotent advance: unit already active" };
46
+ await this.deps.runtime.journalTransition({
47
+ name: "advance-blocked",
48
+ reason: blocked.reason,
49
+ unitType: decision.unitType,
50
+ unitId: decision.unitId,
51
+ });
52
+ await this.deps.health.postAdvanceRecord(blocked);
53
+ return blocked;
54
+ }
55
+ this.status.activeUnit = { unitType: decision.unitType, unitId: decision.unitId };
56
+ this.status.phase = "running";
57
+ this.lastAdvanceKey = nextKey;
58
+ this.bumpTransition();
59
+ await this.deps.runtime.journalTransition({
60
+ name: "advance",
61
+ reason: decision.reason,
62
+ unitType: decision.unitType,
63
+ unitId: decision.unitId,
64
+ });
65
+ await this.deps.worktree.prepareForUnit(decision.unitType, decision.unitId);
66
+ await this.deps.worktree.syncAfterUnit(decision.unitType, decision.unitId);
67
+ const advanced = { kind: "advanced" };
68
+ await this.deps.health.postAdvanceRecord(advanced);
69
+ return advanced;
70
+ }
71
+ catch (error) {
72
+ const recovery = await this.deps.recovery.classifyAndRecover({
73
+ error,
74
+ unitType: this.status.activeUnit?.unitType,
75
+ unitId: this.status.activeUnit?.unitId,
76
+ });
77
+ const result = recovery.action === "retry"
78
+ ? { kind: "paused", reason: recovery.reason }
79
+ : recovery.action === "escalate"
80
+ ? { kind: "error", reason: recovery.reason }
81
+ : { kind: "stopped", reason: recovery.reason };
82
+ if (result.kind === "paused") {
83
+ this.status.phase = "paused";
84
+ }
85
+ else if (result.kind === "stopped") {
86
+ this.status.phase = "stopped";
87
+ }
88
+ else {
89
+ this.status.phase = "error";
90
+ }
91
+ if (result.kind === "stopped") {
92
+ this.lastAdvanceKey = null;
93
+ this.status.activeUnit = undefined;
94
+ }
95
+ this.bumpTransition();
96
+ const journalName = result.kind === "paused"
97
+ ? "advance-paused"
98
+ : result.kind === "stopped"
99
+ ? "advance-stopped"
100
+ : "advance-error";
101
+ await this.deps.runtime.journalTransition({ name: journalName, reason: recovery.reason });
102
+ if (result.kind === "paused") {
103
+ await this.deps.notifications.notifyLifecycle({ name: "pause", detail: recovery.reason });
104
+ }
105
+ else if (result.kind === "stopped") {
106
+ await this.deps.notifications.notifyLifecycle({ name: "stopped", detail: recovery.reason });
107
+ }
108
+ else if (result.kind === "error") {
109
+ await this.deps.notifications.notifyLifecycle({ name: "error", detail: recovery.reason });
110
+ }
111
+ await this.deps.health.postAdvanceRecord(result);
112
+ return result;
113
+ }
114
+ }
115
+ async resume() {
116
+ this.lastAdvanceKey = null;
117
+ this.status.phase = "running";
118
+ this.bumpTransition();
119
+ await this.deps.runtime.journalTransition({ name: "resume" });
120
+ await this.deps.notifications.notifyLifecycle({ name: "resume" });
121
+ return this.advance();
122
+ }
123
+ async stop(reason) {
124
+ if (this.status.phase === "stopped") {
125
+ return { kind: "stopped", reason };
126
+ }
127
+ await this.deps.worktree.cleanupOnStop(reason);
128
+ this.status.phase = "stopped";
129
+ this.status.activeUnit = undefined;
130
+ this.lastAdvanceKey = null;
131
+ this.bumpTransition();
132
+ await this.deps.runtime.journalTransition({ name: "stop", reason });
133
+ await this.deps.notifications.notifyLifecycle({ name: "stop", detail: reason });
134
+ return { kind: "stopped", reason };
135
+ }
136
+ getStatus() {
137
+ return { ...this.status, activeUnit: this.status.activeUnit ? { ...this.status.activeUnit } : undefined };
138
+ }
139
+ bumpTransition() {
140
+ this.status.transitionCount += 1;
141
+ this.status.lastTransitionAt = now();
142
+ }
143
+ }
144
+ export function createAutoOrchestrator(deps) {
145
+ return new AutoOrchestrator(deps);
146
+ }
@@ -28,7 +28,7 @@ import { writeUnitRuntimeRecord } from "../unit-runtime.js";
28
28
  import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
29
29
  import { getEligibleSlices } from "../slice-parallel-eligibility.js";
30
30
  import { startSliceParallel } from "../slice-parallel-orchestrator.js";
31
- import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
31
+ import { isDbAvailable, getMilestoneSlices, refreshOpenDatabaseFromDisk } from "../gsd-db.js";
32
32
  import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
33
33
  import { resolveUokFlags } from "../uok/flags.js";
34
34
  import { UokGateRunner } from "../uok/gate-runner.js";
@@ -42,6 +42,13 @@ import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit,
42
42
  function isSamePathLocal(a, b) {
43
43
  return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
44
44
  }
45
+ function refreshPlanSliceRecoveryDbIfNeeded(unitType) {
46
+ if (unitType !== "plan-slice")
47
+ return true;
48
+ if (!isDbAvailable())
49
+ return true;
50
+ return refreshOpenDatabaseFromDisk();
51
+ }
45
52
  // ─── Session timeout auto-resume state ────────────────────────────────────────
46
53
  let consecutiveSessionTimeouts = 0;
47
54
  const MAX_SESSION_TIMEOUT_AUTO_RESUMES = 3;
@@ -145,6 +152,22 @@ async function emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, errorCon
145
152
  causedBy: { flowId: ic.flowId, seq: unitStartSeq },
146
153
  });
147
154
  }
155
+ export function _buildCancelledUnitStopReason(unitType, unitId, errorContext) {
156
+ const cancellationMessage = errorContext?.message ?? "unknown";
157
+ const isSessionCreationFailure = errorContext?.category === "session-failed";
158
+ if (isSessionCreationFailure) {
159
+ return {
160
+ notifyMessage: `Session creation failed for ${unitType} ${unitId}: ${cancellationMessage}. Stopping auto-mode.`,
161
+ stopReason: `Session creation failed: ${cancellationMessage}`,
162
+ loopReason: "session-failed",
163
+ };
164
+ }
165
+ return {
166
+ notifyMessage: `Unit ${unitType} ${unitId} aborted after dispatch: ${cancellationMessage}. Stopping auto-mode.`,
167
+ stopReason: `Unit aborted: ${cancellationMessage}`,
168
+ loopReason: "unit-aborted",
169
+ };
170
+ }
148
171
  async function failClosedOnFinalizeTimeout(ic, iterData, loopState, stage, startedAt) {
149
172
  const { ctx, pi, s, deps } = ic;
150
173
  const now = Date.now();
@@ -710,7 +733,10 @@ export async function runDispatch(ic, preData, loopState) {
710
733
  // See: https://github.com/gsd-build/gsd-2/issues/2474
711
734
  if (dispatchResult.level === "warning") {
712
735
  ctx.ui.notify(dispatchResult.reason, "warning");
713
- await deps.pauseAuto(ctx, pi);
736
+ await deps.pauseAuto(ctx, pi, {
737
+ message: dispatchResult.reason,
738
+ category: "unknown",
739
+ });
714
740
  }
715
741
  else {
716
742
  await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
@@ -770,7 +796,13 @@ export async function runDispatch(ic, preData, loopState) {
770
796
  action: "artifact-found",
771
797
  });
772
798
  ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
799
+ if (!refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
800
+ ctx.ui.notify(`Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed. Keeping stuck state for retry.`, "warning");
801
+ return { action: "continue" };
802
+ }
773
803
  deps.invalidateAllCaches();
804
+ loopState.recentUnits.length = 0;
805
+ loopState.stuckRecoveryAttempts = 0;
774
806
  return { action: "continue" };
775
807
  }
776
808
  ctx.ui.notify(`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`, "warning");
@@ -778,6 +810,22 @@ export async function runDispatch(ic, preData, loopState) {
778
810
  }
779
811
  else {
780
812
  // Level 2: hard stop — genuinely stuck
813
+ deps.invalidateAllCaches();
814
+ const artifactExists = verifyExpectedArtifact(unitType, unitId, s.basePath);
815
+ if (artifactExists && unitType !== "complete-milestone") {
816
+ debugLog("autoLoop", {
817
+ phase: "stuck-recovery",
818
+ level: 2,
819
+ action: "artifact-found",
820
+ });
821
+ ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`, "info");
822
+ if (refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
823
+ loopState.recentUnits.length = 0;
824
+ loopState.stuckRecoveryAttempts = 0;
825
+ return { action: "continue" };
826
+ }
827
+ ctx.ui.notify(`Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed. Stopping for manual recovery.`, "warning");
828
+ }
781
829
  debugLog("autoLoop", {
782
830
  phase: "stuck-detected",
783
831
  unitType,
@@ -1089,7 +1137,12 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1089
1137
  s.lastGitActionFailure = null;
1090
1138
  s.lastGitActionStatus = null;
1091
1139
  s.lastUnitAgentEndMessages = null;
1092
- setCurrentPhase(unitType);
1140
+ setCurrentPhase(unitType, {
1141
+ basePath: s.basePath,
1142
+ traceId: ic.flowId,
1143
+ turnId: `iter-${ic.iteration}`,
1144
+ causedBy: "unit-start",
1145
+ });
1093
1146
  s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
1094
1147
  const unitStartSeq = ic.nextSeq();
1095
1148
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
@@ -1379,10 +1432,11 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1379
1432
  }
1380
1433
  await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
1381
1434
  await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
1382
- ctx.ui.notify(`Session creation failed for ${unitType} ${unitId}: ${unitResult.errorContext?.message ?? "unknown"}. Stopping auto-mode.`, "warning");
1383
- await deps.stopAuto(ctx, pi, `Session creation failed: ${unitResult.errorContext?.message ?? "unknown"}`);
1384
- debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
1385
- return { action: "break", reason: "session-failed" };
1435
+ const cancelledStop = _buildCancelledUnitStopReason(unitType, unitId, unitResult.errorContext);
1436
+ ctx.ui.notify(cancelledStop.notifyMessage, "warning");
1437
+ await deps.stopAuto(ctx, pi, cancelledStop.stopReason);
1438
+ debugLog("autoLoop", { phase: "exit", reason: cancelledStop.loopReason });
1439
+ return { action: "break", reason: cancelledStop.loopReason };
1386
1440
  }
1387
1441
  // ── Immediate unit closeout (metrics, activity log, memory) ────────
1388
1442
  // Run right after runUnit() returns so telemetry is never lost to a
@@ -148,6 +148,8 @@ export class AutoSession {
148
148
  // ── Remote command polling ───────────────────────────────────────────────
149
149
  /** Cleanup function returned by startCommandPolling(); null when not running. */
150
150
  commandPollingCleanup = null;
151
+ // ── Orchestration seam ───────────────────────────────────────────────────
152
+ orchestration = null;
151
153
  // ── Loop promise state ──────────────────────────────────────────────────
152
154
  // Per-unit resolve function and session-switch guard live at module level
153
155
  // in auto-loop.ts (_currentResolve, _sessionSwitchInFlight).
@@ -268,9 +270,12 @@ export class AutoSession {
268
270
  this.sigtermHandler = null;
269
271
  // Remote command polling — cleanup must be called before reset (auto.ts stopAuto)
270
272
  this.commandPollingCleanup = null;
273
+ // Orchestration seam
274
+ this.orchestration = null;
271
275
  // Loop promise state lives in auto-loop.ts module scope
272
276
  }
273
277
  toJSON() {
278
+ const orchestrationStatus = this.orchestration?.getStatus();
274
279
  return {
275
280
  active: this.active,
276
281
  paused: this.paused,
@@ -280,6 +285,9 @@ export class AutoSession {
280
285
  activeRunDir: this.activeRunDir,
281
286
  currentMilestoneId: this.currentMilestoneId,
282
287
  currentUnit: this.currentUnit,
288
+ orchestrationPhase: orchestrationStatus?.phase,
289
+ orchestrationTransitionCount: orchestrationStatus?.transitionCount,
290
+ orchestrationLastTransitionAt: orchestrationStatus?.lastTransitionAt,
283
291
  unitDispatchCount: Object.fromEntries(this.unitDispatchCount),
284
292
  };
285
293
  }
@@ -128,9 +128,9 @@ export function diagnoseExpectedArtifact(unitType, unitId, base) {
128
128
  }
129
129
  return `${relSliceFile(base, mid, sid, "RESEARCH")} (slice research)`;
130
130
  case "plan-slice":
131
- return `${relSliceFile(base, mid, sid, "PLAN")} (slice plan)`;
131
+ return `${relSliceFile(base, mid, sid, "PLAN")} plus tasks/T##-PLAN.md files (slice plan and task plans)`;
132
132
  case "refine-slice":
133
- return `${relSliceFile(base, mid, sid, "PLAN")} (refined slice plan from sketch)`;
133
+ return `${relSliceFile(base, mid, sid, "PLAN")} plus tasks/T##-PLAN.md files (refined slice plan and task plans)`;
134
134
  case "execute-task": {
135
135
  return `Task ${tid} marked [x] in ${relSliceFile(base, mid, sid, "PLAN")} + summary written`;
136
136
  }
@@ -559,6 +559,8 @@ export const DISPATCH_RULES = [
559
559
  const hasContext = !!(contextFile && (await loadFile(contextFile)));
560
560
  if (hasContext)
561
561
  return null; // fall through to next rule
562
+ if (prefs?.planning_depth === "deep")
563
+ return null;
562
564
  // H6 fix (#4973): keep the non-deep auto-mode bypass, but do not
563
565
  // pre-verify deep planning's user-facing milestone approval gate.
564
566
  if (shouldBypassMilestoneDepthGateInAuto(prefs)) {
@@ -12,7 +12,7 @@ import { appendEvent } from "./workflow-events.js";
12
12
  import { atomicWriteSync } from "./atomic-write.js";
13
13
  import { clearParseCache } from "./files.js";
14
14
  import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
15
- import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus, insertSlice, getMilestone } from "./gsd-db.js";
15
+ import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus, insertSlice, getMilestone, refreshOpenDatabaseFromDisk } from "./gsd-db.js";
16
16
  import { isValidationTerminal } from "./state.js";
17
17
  import { getErrorMessage } from "./error-utils.js";
18
18
  import { logWarning, logError } from "./workflow-logger.js";
@@ -257,7 +257,7 @@ function scanGsdTaggedCommits(basePath, milestoneId, gitArgs) {
257
257
  if (!commitMessageHasGsdTrailer(message))
258
258
  continue;
259
259
  const commitFiles = getChangedFilesForCommit(basePath, hash);
260
- if (!commitMatchesMilestone(message, milestoneId, commitFiles))
260
+ if (!commitMatchesMilestone(basePath, message, milestoneId, commitFiles))
261
261
  continue;
262
262
  matched = true;
263
263
  for (const file of commitFiles) {
@@ -278,22 +278,37 @@ function getChangedFilesForCommit(basePath, hash) {
278
278
  function commitMessageHasGsdTrailer(message) {
279
279
  return /^GSD-(?:Task|Unit):\s*\S+/m.test(message);
280
280
  }
281
- function commitMatchesMilestone(message, milestoneId, files) {
281
+ function commitMatchesMilestone(basePath, message, milestoneId, files) {
282
282
  if (commitTrailerStartsWithMilestone(message, milestoneId))
283
283
  return true;
284
284
  // Meaningful execute-task commits currently store task scope as Sxx/Tyy
285
285
  // rather than Mxx/Sxx/Tyy. Bind those commits back to the milestone when
286
286
  // either the commit touched this milestone's artifacts, or — for projects
287
287
  // where .gsd/ is gitignored/external (#5033) — the message explicitly
288
- // names the milestone.
288
+ // names the milestone or local GSD state proves the task belongs here.
289
289
  if (/^GSD-Task:\s*S[^/\s]+\/T\S+/m.test(message)) {
290
290
  if (files.some((file) => isMilestoneArtifactPath(file, milestoneId)))
291
291
  return true;
292
292
  if (commitMessageMentionsMilestone(message, milestoneId))
293
293
  return true;
294
+ if (commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId))
295
+ return true;
294
296
  }
295
297
  return false;
296
298
  }
299
+ function commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId) {
300
+ const match = message.match(/^GSD-Task:\s*(S[^/\s]+)\/(T[^\s]+)/m);
301
+ if (!match)
302
+ return false;
303
+ const [, sliceId, taskId] = match;
304
+ if (getTask(milestoneId, sliceId, taskId))
305
+ return true;
306
+ const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId);
307
+ if (!tasksDir)
308
+ return false;
309
+ return existsSync(join(tasksDir, `${taskId}-PLAN.md`))
310
+ || existsSync(join(tasksDir, `${taskId}-SUMMARY.md`));
311
+ }
297
312
  function commitMessageMentionsMilestone(message, milestoneId) {
298
313
  if (!MILESTONE_ID_RE.test(milestoneId))
299
314
  return false;
@@ -495,66 +510,32 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
495
510
  return false;
496
511
  }
497
512
  }
498
- // plan-slice must produce a plan with actual task entries, not just a scaffold.
499
- // The plan file may exist from a prior discussion/context step with only headings
500
- // but no tasks. Without this check the artifact is considered "complete" and the
501
- // unit gets skipped — but deriveState still returns phase:"planning" because the
502
- // plan has no tasks, creating an infinite skip loop (#699).
503
- if (unitType === "plan-slice") {
504
- const planContent = readFileSync(absPath, "utf-8");
505
- // Accept checkbox-style (- [x] **T01: ...) or heading-style (### T01 -- / ### T01: / ### T01 —)
506
- const hasCheckboxTask = /^- \[[xX ]\] \*\*T\d+:/m.test(planContent);
507
- const hasHeadingTask = /^#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
508
- if (!hasCheckboxTask && !hasHeadingTask) {
509
- logWarning("recovery", `verify-fail ${unitType} ${unitId}: plan has no task checkbox/heading (len=${planContent.length}) at ${absPath}`);
510
- return false;
511
- }
512
- }
513
- // execute-task: DB status is authoritative. Fall back to checked-checkbox
514
- // detection when the DB is unavailable (unmigrated projects), or when the
515
- // disk artifacts already reflect completion but the DB replay is one beat
516
- // behind the completion write.
517
- if (unitType === "execute-task") {
518
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
519
- if (mid && sid && tid) {
520
- const dbTask = getTask(mid, sid, tid);
521
- if (dbTask) {
522
- if (dbTask.status !== "complete" && dbTask.status !== "done" && !hasCheckedTaskCompletionOnDisk(base, mid, sid, tid)) {
523
- return false;
524
- }
525
- }
526
- else if (!isDbAvailable()) {
527
- // LEGACY: Pre-migration fallback for projects without DB.
528
- // Require a CHECKED checkbox — a bare heading or unchecked checkbox
529
- // does not prove gsd_complete_task ran. Summary file on disk alone
530
- // is not sufficient evidence (could be a rogue write) (#3607).
531
- if (!hasCheckedTaskCompletionOnDisk(base, mid, sid, tid))
532
- return false;
533
- }
534
- else {
535
- // DB available but task row not found — completion tool never ran (#3607)
536
- return false;
537
- }
538
- }
539
- }
540
- // plan-slice must also produce individual task plan files for every task listed
541
- // in the slice plan. Without this check, a plan-slice that wrote S{sid}-PLAN.md
542
- // but omitted T{tid}-PLAN.md files would be marked complete, causing execute-task
543
- // to dispatch with a missing task plan (see issue #739).
513
+ // plan-slice verification is DB-primary. The slice plan is a projection, so
514
+ // DB task rows prove the slice was planned even if the rendered markdown no
515
+ // longer uses legacy checkbox/heading syntax.
544
516
  if (unitType === "plan-slice") {
545
517
  const { milestone: mid, slice: sid } = parseUnitId(unitId);
546
518
  if (mid && sid) {
547
519
  try {
548
- // DB primary path — get task IDs to verify task plan files exist
549
520
  let taskIds = null;
550
521
  if (isDbAvailable()) {
551
- const tasks = getSliceTasks(mid, sid);
552
- if (tasks.length > 0)
553
- taskIds = tasks.map(t => t.id);
522
+ const refreshed = refreshOpenDatabaseFromDisk();
523
+ if (refreshed) {
524
+ const tasks = getSliceTasks(mid, sid);
525
+ if (tasks.length > 0)
526
+ taskIds = tasks.map(t => t.id);
527
+ }
554
528
  }
555
529
  if (!taskIds) {
556
- // LEGACY: DB unavailable or no tasks in DB parse plan file for task IDs
530
+ // LEGACY: DB unavailable or no tasks in DB. Require actual task
531
+ // entries so an empty scaffold cannot advance the pipeline (#699).
557
532
  const planContent = readFileSync(absPath, "utf-8");
533
+ const hasCheckboxTask = /^\s*- \[[xX ]\] \*\*T\d+:/m.test(planContent);
534
+ const hasHeadingTask = /^\s*#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
535
+ if (!hasCheckboxTask && !hasHeadingTask) {
536
+ logWarning("recovery", `verify-fail ${unitType} ${unitId}: plan has no task checkbox/heading (len=${planContent.length}) at ${absPath}`);
537
+ return false;
538
+ }
558
539
  const plan = parseLegacyPlan(planContent);
559
540
  if (plan.tasks.length > 0)
560
541
  taskIds = plan.tasks.map((t) => t.id);
@@ -580,6 +561,33 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
580
561
  }
581
562
  }
582
563
  }
564
+ // execute-task: DB status is authoritative. Fall back to checked-checkbox
565
+ // detection when the DB is unavailable (unmigrated projects), or when the
566
+ // disk artifacts already reflect completion but the DB replay is one beat
567
+ // behind the completion write.
568
+ if (unitType === "execute-task") {
569
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
570
+ if (mid && sid && tid) {
571
+ const dbTask = getTask(mid, sid, tid);
572
+ if (dbTask) {
573
+ if (dbTask.status !== "complete" && dbTask.status !== "done" && !hasCheckedTaskCompletionOnDisk(base, mid, sid, tid)) {
574
+ return false;
575
+ }
576
+ }
577
+ else if (!isDbAvailable()) {
578
+ // LEGACY: Pre-migration fallback for projects without DB.
579
+ // Require a CHECKED checkbox — a bare heading or unchecked checkbox
580
+ // does not prove gsd_complete_task ran. Summary file on disk alone
581
+ // is not sufficient evidence (could be a rogue write) (#3607).
582
+ if (!hasCheckedTaskCompletionOnDisk(base, mid, sid, tid))
583
+ return false;
584
+ }
585
+ else {
586
+ // DB available but task row not found — completion tool never ran (#3607)
587
+ return false;
588
+ }
589
+ }
590
+ }
583
591
  // complete-slice: DB status is authoritative for whether the slice is done.
584
592
  // Fall back to file-based check (roadmap [x]) when DB is unavailable.
585
593
  if (unitType === "complete-slice") {
@@ -3,11 +3,15 @@ import { AutoSession } from "./auto/session.js";
3
3
  import { isDeterministicPolicyError, isQueuedUserMessageSkip, isToolInvocationError, markToolEnd as markTrackedToolEnd, markToolStart as markTrackedToolStart, } from "./auto-tool-tracking.js";
4
4
  export const autoSession = new AutoSession();
5
5
  export function getAutoRuntimeSnapshot() {
6
+ const orchestrationStatus = autoSession.orchestration?.getStatus();
6
7
  return {
7
8
  active: autoSession.active,
8
9
  paused: autoSession.paused,
9
10
  currentUnit: autoSession.currentUnit ? { ...autoSession.currentUnit } : null,
10
11
  basePath: autoSession.basePath,
12
+ orchestrationPhase: orchestrationStatus?.phase,
13
+ orchestrationTransitionCount: orchestrationStatus?.transitionCount,
14
+ orchestrationLastTransitionAt: orchestrationStatus?.lastTransitionAt,
11
15
  };
12
16
  }
13
17
  export function isAutoActive() {
@@ -486,8 +486,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
486
486
  // Clear survivor flag — finalization is done
487
487
  hasSurvivorBranch = false;
488
488
  }
489
+ const effectivePrefs = loadEffectiveGSDPreferences(base)?.preferences;
489
490
  const deepProjectStagePending = !hasSurvivorBranch
490
- ? (await import("./auto-dispatch.js")).hasPendingDeepStage(loadEffectiveGSDPreferences(base)?.preferences, base)
491
+ ? (await import("./auto-dispatch.js")).hasPendingDeepStage(effectivePrefs, base)
491
492
  : false;
492
493
  if (deepProjectStagePending) {
493
494
  // Deep project-level setup runs before the first milestone exists. Let
@@ -526,7 +527,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
526
527
  const mid = state.activeMilestone.id;
527
528
  const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
528
529
  const hasContext = !!(contextFile && (await loadFile(contextFile)));
529
- if (!hasContext) {
530
+ if (!hasContext && effectivePrefs?.planning_depth !== "deep") {
530
531
  const { showSmartEntry } = await import("./guided-flow.js");
531
532
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
532
533
  // showSmartEntry dispatches via pi.sendMessage() which is fire-and-forget: