gsd-pi 2.79.0-dev.ece5fd8ba → 2.80.0

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 (193) hide show
  1. package/dist/loader.js +0 -0
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/contracts.js +1 -0
  4. package/dist/resources/extensions/gsd/auto/orchestrator.js +146 -0
  5. package/dist/resources/extensions/gsd/auto/phases.js +55 -6
  6. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  7. package/dist/resources/extensions/gsd/auto-recovery.js +45 -52
  8. package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
  9. package/dist/resources/extensions/gsd/auto.js +159 -2
  10. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
  11. package/dist/resources/extensions/gsd/gsd-db.js +34 -1
  12. package/dist/resources/extensions/gsd/post-execution-checks.js +25 -6
  13. package/dist/resources/extensions/shared/interview-ui.js +15 -4
  14. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  15. package/dist/web/standalone/.next/BUILD_ID +1 -1
  16. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  17. package/dist/web/standalone/.next/build-manifest.json +3 -3
  18. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  19. package/dist/web/standalone/.next/required-server-files.json +3 -3
  20. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  21. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  31. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  41. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  42. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  43. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  44. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  45. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  47. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  59. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  79. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  89. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  95. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  111. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
  115. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/index.html +1 -1
  125. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  126. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  127. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  128. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  129. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  130. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  131. package/dist/web/standalone/.next/server/app/page.js +2 -2
  132. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  134. package/dist/web/standalone/.next/server/chunks/63.js +3 -3
  135. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  136. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/middleware.js +2 -2
  138. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  140. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  141. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  142. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  143. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-f2a7482d42a5614b.js → page-2f24283c162b6ab3.js} +1 -1
  144. package/dist/web/standalone/.next/static/chunks/app/{layout-a16c7a7ecdf0c2cf.js → layout-9ecfd95f343793f0.js} +1 -1
  145. package/dist/web/standalone/.next/static/chunks/app/page-ff639266d978f2a0.js +1 -0
  146. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +1 -0
  147. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +1 -0
  148. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  149. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  150. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  151. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  152. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  153. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  154. package/dist/web/standalone/server.js +1 -1
  155. package/package.json +1 -1
  156. package/packages/daemon/package.json +2 -2
  157. package/packages/mcp-server/package.json +2 -2
  158. package/packages/native/package.json +1 -1
  159. package/packages/pi-agent-core/package.json +1 -1
  160. package/packages/pi-ai/package.json +1 -1
  161. package/packages/pi-coding-agent/package.json +1 -1
  162. package/packages/pi-tui/package.json +1 -1
  163. package/packages/rpc-client/package.json +1 -1
  164. package/pkg/package.json +1 -1
  165. package/src/resources/extensions/gsd/auto/contracts.ts +87 -0
  166. package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -3
  167. package/src/resources/extensions/gsd/auto/orchestrator.ts +161 -0
  168. package/src/resources/extensions/gsd/auto/phases.ts +82 -8
  169. package/src/resources/extensions/gsd/auto/session.ts +11 -0
  170. package/src/resources/extensions/gsd/auto-recovery.ts +42 -50
  171. package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
  172. package/src/resources/extensions/gsd/auto.ts +167 -1
  173. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +14 -1
  174. package/src/resources/extensions/gsd/gsd-db.ts +35 -1
  175. package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
  176. package/src/resources/extensions/gsd/post-execution-checks.ts +31 -6
  177. package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +32 -0
  178. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +353 -0
  179. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +39 -0
  180. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +3 -0
  181. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
  182. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +79 -0
  183. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +134 -0
  184. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +2 -0
  185. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +27 -0
  186. package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +46 -0
  187. package/src/resources/extensions/shared/interview-ui.ts +18 -5
  188. package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
  189. package/dist/web/standalone/.next/static/chunks/app/page-fab3ebb85b006001.js +0 -1
  190. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +0 -1
  191. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +0 -1
  192. /package/dist/web/standalone/.next/static/{TzEVJ1Lh8vbez4n4Q9TqQ → V-3Ehy4B24f9FCGiLPWIM}/_buildManifest.js +0 -0
  193. /package/dist/web/standalone/.next/static/{TzEVJ1Lh8vbez4n4Q9TqQ → V-3Ehy4B24f9FCGiLPWIM}/_ssgManifest.js +0 -0
@@ -0,0 +1,87 @@
1
+ import type { GSDState } from "../types.js";
2
+
3
+ export interface AutoSessionContext {
4
+ basePath: string;
5
+ trigger: "guided-flow" | "resume" | "auto-loop" | "manual";
6
+ }
7
+
8
+ export interface AutoStatus {
9
+ phase: "idle" | "running" | "paused" | "stopped" | "error";
10
+ activeUnit?: {
11
+ unitType: string;
12
+ unitId: string;
13
+ };
14
+ lastTransitionAt?: number;
15
+ transitionCount: number;
16
+ }
17
+
18
+ export interface AutoAdvanceResult {
19
+ kind: "advanced" | "blocked" | "paused" | "stopped" | "error";
20
+ reason?: string;
21
+ stateSnapshot?: GSDState;
22
+ }
23
+
24
+ export interface AutoOrchestrationModule {
25
+ start(sessionContext: AutoSessionContext): Promise<AutoAdvanceResult>;
26
+ advance(): Promise<AutoAdvanceResult>;
27
+ resume(): Promise<AutoAdvanceResult>;
28
+ stop(reason: string): Promise<AutoAdvanceResult>;
29
+ getStatus(): AutoStatus;
30
+ }
31
+
32
+ export interface DispatchAdapter {
33
+ decideNextUnit(): Promise<{
34
+ unitType: string;
35
+ unitId: string;
36
+ reason: string;
37
+ preconditions: string[];
38
+ } | null>;
39
+ }
40
+
41
+ export interface RecoveryAdapter {
42
+ classifyAndRecover(input: {
43
+ error: unknown;
44
+ unitType?: string;
45
+ unitId?: string;
46
+ }): Promise<{
47
+ action: "retry" | "escalate" | "stop";
48
+ reason: string;
49
+ }>;
50
+ }
51
+
52
+ export interface WorktreeAdapter {
53
+ prepareForUnit(unitType: string, unitId: string): Promise<void>;
54
+ syncAfterUnit(unitType: string, unitId: string): Promise<void>;
55
+ cleanupOnStop(reason: string): Promise<void>;
56
+ }
57
+
58
+ export interface HealthAdapter {
59
+ preAdvanceGate(): Promise<{ allow: boolean; reason?: string }>;
60
+ postAdvanceRecord(result: AutoAdvanceResult): Promise<void>;
61
+ }
62
+
63
+ export interface RuntimePersistenceAdapter {
64
+ ensureLockOwnership(): Promise<void>;
65
+ journalTransition(event: {
66
+ name: string;
67
+ reason?: string;
68
+ unitType?: string;
69
+ unitId?: string;
70
+ }): Promise<void>;
71
+ }
72
+
73
+ export interface NotificationAdapter {
74
+ notifyLifecycle(event: {
75
+ name: string;
76
+ detail?: string;
77
+ }): Promise<void>;
78
+ }
79
+
80
+ export interface AutoOrchestratorDeps {
81
+ dispatch: DispatchAdapter;
82
+ recovery: RecoveryAdapter;
83
+ worktree: WorktreeAdapter;
84
+ health: HealthAdapter;
85
+ runtime: RuntimePersistenceAdapter;
86
+ notifications: NotificationAdapter;
87
+ }
@@ -7,6 +7,7 @@
7
7
  import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
8
8
 
9
9
  import type { AutoSession } from "./session.js";
10
+ import type { ErrorContext } from "./types.js";
10
11
  import type { GSDPreferences } from "../preferences.js";
11
12
  import type { GSDState } from "../types.js";
12
13
  import type { SessionLockStatus } from "../session-lock.js";
@@ -24,6 +25,12 @@ import type { MergeReconcileResult } from "../auto-recovery.js";
24
25
  import type { UokTurnObserver } from "../uok/contracts.js";
25
26
  import type { PreflightResult } from "../clean-root-preflight.js";
26
27
 
28
+ type PauseAutoFn = (
29
+ ctx?: ExtensionContext,
30
+ pi?: ExtensionAPI,
31
+ errorContext?: ErrorContext,
32
+ ) => Promise<void>;
33
+
27
34
  /**
28
35
  * Dependencies injected by the caller (auto.ts startAuto) so autoLoop
29
36
  * can access private functions from auto.ts without exporting them.
@@ -39,7 +46,7 @@ export interface LoopDeps {
39
46
  pi?: ExtensionAPI,
40
47
  reason?: string,
41
48
  ) => Promise<void>;
42
- pauseAuto: (ctx?: ExtensionContext, pi?: ExtensionAPI) => Promise<void>;
49
+ pauseAuto: PauseAutoFn;
43
50
  clearUnitTimeout: () => void;
44
51
  updateProgressWidget: (
45
52
  ctx: ExtensionContext,
@@ -245,7 +252,7 @@ export interface LoopDeps {
245
252
  prefs: GSDPreferences | undefined;
246
253
  buildSnapshotOpts: () => CloseoutOptions & Record<string, unknown>;
247
254
  buildRecoveryContext: () => unknown;
248
- pauseAuto: (ctx?: ExtensionContext, pi?: ExtensionAPI) => Promise<void>;
255
+ pauseAuto: PauseAutoFn;
249
256
  }) => void;
250
257
 
251
258
  // Prompt helpers
@@ -271,7 +278,7 @@ export interface LoopDeps {
271
278
  ) => Promise<"dispatched" | "continue" | "retry">;
272
279
  runPostUnitVerification: (
273
280
  vctx: VerificationContext,
274
- pauseAuto: (ctx?: ExtensionContext, pi?: ExtensionAPI) => Promise<void>,
281
+ pauseAuto: PauseAutoFn,
275
282
  ) => Promise<VerificationResult>;
276
283
  postUnitPostVerification: (
277
284
  pctx: PostUnitContext,
@@ -0,0 +1,161 @@
1
+ import type { AutoAdvanceResult, AutoOrchestrationModule, AutoOrchestratorDeps, AutoSessionContext, AutoStatus } from "./contracts.js";
2
+
3
+ function now(): number {
4
+ return Date.now();
5
+ }
6
+
7
+ export class AutoOrchestrator implements AutoOrchestrationModule {
8
+ private status: AutoStatus = {
9
+ phase: "idle",
10
+ transitionCount: 0,
11
+ };
12
+ private readonly deps: AutoOrchestratorDeps;
13
+ private lastAdvanceKey: string | null = null;
14
+
15
+ public constructor(deps: AutoOrchestratorDeps) {
16
+ this.deps = deps;
17
+ }
18
+
19
+ public async start(_sessionContext: AutoSessionContext): Promise<AutoAdvanceResult> {
20
+ this.lastAdvanceKey = null;
21
+ this.status.phase = "running";
22
+ this.bumpTransition();
23
+ await this.deps.runtime.journalTransition({ name: "start" });
24
+ await this.deps.notifications.notifyLifecycle({ name: "start" });
25
+ return this.advance();
26
+ }
27
+
28
+ public async advance(): Promise<AutoAdvanceResult> {
29
+ try {
30
+ await this.deps.runtime.ensureLockOwnership();
31
+ const gate = await this.deps.health.preAdvanceGate();
32
+ if (!gate.allow) {
33
+ const blocked: AutoAdvanceResult = { kind: "blocked", reason: gate.reason ?? "health gate blocked" };
34
+ await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
35
+ await this.deps.health.postAdvanceRecord(blocked);
36
+ return blocked;
37
+ }
38
+
39
+ const decision = await this.deps.dispatch.decideNextUnit();
40
+ if (!decision) {
41
+ const stopped: AutoAdvanceResult = { kind: "stopped", reason: "no remaining units" };
42
+ this.status.phase = "stopped";
43
+ this.status.activeUnit = undefined;
44
+ this.lastAdvanceKey = null;
45
+ this.bumpTransition();
46
+ await this.deps.runtime.journalTransition({ name: "advance-stopped", reason: stopped.reason });
47
+ await this.deps.health.postAdvanceRecord(stopped);
48
+ return stopped;
49
+ }
50
+
51
+ const nextKey = `${decision.unitType}:${decision.unitId}`;
52
+ if (this.lastAdvanceKey === nextKey) {
53
+ const blocked: AutoAdvanceResult = { kind: "blocked", reason: "idempotent advance: unit already active" };
54
+ await this.deps.runtime.journalTransition({
55
+ name: "advance-blocked",
56
+ reason: blocked.reason,
57
+ unitType: decision.unitType,
58
+ unitId: decision.unitId,
59
+ });
60
+ await this.deps.health.postAdvanceRecord(blocked);
61
+ return blocked;
62
+ }
63
+
64
+ this.status.activeUnit = { unitType: decision.unitType, unitId: decision.unitId };
65
+ this.status.phase = "running";
66
+ this.lastAdvanceKey = nextKey;
67
+ this.bumpTransition();
68
+
69
+ await this.deps.runtime.journalTransition({
70
+ name: "advance",
71
+ reason: decision.reason,
72
+ unitType: decision.unitType,
73
+ unitId: decision.unitId,
74
+ });
75
+ await this.deps.worktree.prepareForUnit(decision.unitType, decision.unitId);
76
+ await this.deps.worktree.syncAfterUnit(decision.unitType, decision.unitId);
77
+
78
+ const advanced: AutoAdvanceResult = { kind: "advanced" };
79
+ await this.deps.health.postAdvanceRecord(advanced);
80
+ return advanced;
81
+ } catch (error) {
82
+ const recovery = await this.deps.recovery.classifyAndRecover({
83
+ error,
84
+ unitType: this.status.activeUnit?.unitType,
85
+ unitId: this.status.activeUnit?.unitId,
86
+ });
87
+ const result: AutoAdvanceResult = recovery.action === "retry"
88
+ ? { kind: "paused", reason: recovery.reason }
89
+ : recovery.action === "escalate"
90
+ ? { kind: "error", reason: recovery.reason }
91
+ : { kind: "stopped", reason: recovery.reason };
92
+
93
+ if (result.kind === "paused") {
94
+ this.status.phase = "paused";
95
+ } else if (result.kind === "stopped") {
96
+ this.status.phase = "stopped";
97
+ } else {
98
+ this.status.phase = "error";
99
+ }
100
+
101
+ if (result.kind === "stopped") {
102
+ this.lastAdvanceKey = null;
103
+ this.status.activeUnit = undefined;
104
+ }
105
+ this.bumpTransition();
106
+
107
+ const journalName = result.kind === "paused"
108
+ ? "advance-paused"
109
+ : result.kind === "stopped"
110
+ ? "advance-stopped"
111
+ : "advance-error";
112
+ await this.deps.runtime.journalTransition({ name: journalName, reason: recovery.reason });
113
+
114
+ if (result.kind === "paused") {
115
+ await this.deps.notifications.notifyLifecycle({ name: "pause", detail: recovery.reason });
116
+ } else if (result.kind === "stopped") {
117
+ await this.deps.notifications.notifyLifecycle({ name: "stopped", detail: recovery.reason });
118
+ } else if (result.kind === "error") {
119
+ await this.deps.notifications.notifyLifecycle({ name: "error", detail: recovery.reason });
120
+ }
121
+ await this.deps.health.postAdvanceRecord(result);
122
+ return result;
123
+ }
124
+ }
125
+
126
+ public async resume(): Promise<AutoAdvanceResult> {
127
+ this.lastAdvanceKey = null;
128
+ this.status.phase = "running";
129
+ this.bumpTransition();
130
+ await this.deps.runtime.journalTransition({ name: "resume" });
131
+ await this.deps.notifications.notifyLifecycle({ name: "resume" });
132
+ return this.advance();
133
+ }
134
+
135
+ public async stop(reason: string): Promise<AutoAdvanceResult> {
136
+ if (this.status.phase === "stopped") {
137
+ return { kind: "stopped", reason };
138
+ }
139
+ await this.deps.worktree.cleanupOnStop(reason);
140
+ this.status.phase = "stopped";
141
+ this.status.activeUnit = undefined;
142
+ this.lastAdvanceKey = null;
143
+ this.bumpTransition();
144
+ await this.deps.runtime.journalTransition({ name: "stop", reason });
145
+ await this.deps.notifications.notifyLifecycle({ name: "stop", detail: reason });
146
+ return { kind: "stopped", reason };
147
+ }
148
+
149
+ public getStatus(): AutoStatus {
150
+ return { ...this.status, activeUnit: this.status.activeUnit ? { ...this.status.activeUnit } : undefined };
151
+ }
152
+
153
+ private bumpTransition(): void {
154
+ this.status.transitionCount += 1;
155
+ this.status.lastTransitionAt = now();
156
+ }
157
+ }
158
+
159
+ export function createAutoOrchestrator(deps: AutoOrchestratorDeps): AutoOrchestrationModule {
160
+ return new AutoOrchestrator(deps);
161
+ }
@@ -55,7 +55,7 @@ import { writeUnitRuntimeRecord } from "../unit-runtime.js";
55
55
  import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
56
56
  import { getEligibleSlices } from "../slice-parallel-eligibility.js";
57
57
  import { startSliceParallel } from "../slice-parallel-orchestrator.js";
58
- import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
58
+ import { isDbAvailable, getMilestoneSlices, refreshOpenDatabaseFromDisk } from "../gsd-db.js";
59
59
  import type { MinimalModelRegistry } from "../context-budget.js";
60
60
  import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
61
61
  import { resolveUokFlags } from "../uok/flags.js";
@@ -76,6 +76,12 @@ function isSamePathLocal(a: string, b: string): boolean {
76
76
  return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
77
77
  }
78
78
 
79
+ function refreshPlanSliceRecoveryDbIfNeeded(unitType: string): boolean {
80
+ if (unitType !== "plan-slice") return true;
81
+ if (!isDbAvailable()) return true;
82
+ return refreshOpenDatabaseFromDisk();
83
+ }
84
+
79
85
  // ─── Session timeout auto-resume state ────────────────────────────────────────
80
86
 
81
87
  let consecutiveSessionTimeouts = 0;
@@ -232,6 +238,33 @@ async function emitCancelledUnitEnd(
232
238
  });
233
239
  }
234
240
 
241
+ export function _buildCancelledUnitStopReason(
242
+ unitType: string,
243
+ unitId: string,
244
+ errorContext?: { message: string; category: string },
245
+ ): {
246
+ notifyMessage: string;
247
+ stopReason: string;
248
+ loopReason: "session-failed" | "unit-aborted";
249
+ } {
250
+ const cancellationMessage = errorContext?.message ?? "unknown";
251
+ const isSessionCreationFailure = errorContext?.category === "session-failed";
252
+
253
+ if (isSessionCreationFailure) {
254
+ return {
255
+ notifyMessage: `Session creation failed for ${unitType} ${unitId}: ${cancellationMessage}. Stopping auto-mode.`,
256
+ stopReason: `Session creation failed: ${cancellationMessage}`,
257
+ loopReason: "session-failed",
258
+ };
259
+ }
260
+
261
+ return {
262
+ notifyMessage: `Unit ${unitType} ${unitId} aborted after dispatch: ${cancellationMessage}. Stopping auto-mode.`,
263
+ stopReason: `Unit aborted: ${cancellationMessage}`,
264
+ loopReason: "unit-aborted",
265
+ };
266
+ }
267
+
235
268
  async function failClosedOnFinalizeTimeout(
236
269
  ic: IterationContext,
237
270
  iterData: IterationData,
@@ -994,7 +1027,10 @@ export async function runDispatch(
994
1027
  // See: https://github.com/gsd-build/gsd-2/issues/2474
995
1028
  if (dispatchResult.level === "warning") {
996
1029
  ctx.ui.notify(dispatchResult.reason, "warning");
997
- await deps.pauseAuto(ctx, pi);
1030
+ await deps.pauseAuto(ctx, pi, {
1031
+ message: dispatchResult.reason,
1032
+ category: "unknown",
1033
+ });
998
1034
  } else {
999
1035
  await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
1000
1036
  }
@@ -1065,7 +1101,16 @@ export async function runDispatch(
1065
1101
  `Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
1066
1102
  "info",
1067
1103
  );
1104
+ if (!refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
1105
+ ctx.ui.notify(
1106
+ `Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed. Keeping stuck state for retry.`,
1107
+ "warning",
1108
+ );
1109
+ return { action: "continue" };
1110
+ }
1068
1111
  deps.invalidateAllCaches();
1112
+ loopState.recentUnits.length = 0;
1113
+ loopState.stuckRecoveryAttempts = 0;
1069
1114
  return { action: "continue" };
1070
1115
  }
1071
1116
  ctx.ui.notify(
@@ -1075,6 +1120,32 @@ export async function runDispatch(
1075
1120
  deps.invalidateAllCaches();
1076
1121
  } else {
1077
1122
  // Level 2: hard stop — genuinely stuck
1123
+ deps.invalidateAllCaches();
1124
+ const artifactExists = verifyExpectedArtifact(
1125
+ unitType,
1126
+ unitId,
1127
+ s.basePath,
1128
+ );
1129
+ if (artifactExists && unitType !== "complete-milestone") {
1130
+ debugLog("autoLoop", {
1131
+ phase: "stuck-recovery",
1132
+ level: 2,
1133
+ action: "artifact-found",
1134
+ });
1135
+ ctx.ui.notify(
1136
+ `Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`,
1137
+ "info",
1138
+ );
1139
+ if (refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
1140
+ loopState.recentUnits.length = 0;
1141
+ loopState.stuckRecoveryAttempts = 0;
1142
+ return { action: "continue" };
1143
+ }
1144
+ ctx.ui.notify(
1145
+ `Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed. Stopping for manual recovery.`,
1146
+ "warning",
1147
+ );
1148
+ }
1078
1149
  debugLog("autoLoop", {
1079
1150
  phase: "stuck-detected",
1080
1151
  unitType,
@@ -1871,13 +1942,16 @@ export async function runUnitPhase(
1871
1942
  }
1872
1943
  await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
1873
1944
  await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
1874
- ctx.ui.notify(
1875
- `Session creation failed for ${unitType} ${unitId}: ${unitResult.errorContext?.message ?? "unknown"}. Stopping auto-mode.`,
1876
- "warning",
1945
+
1946
+ const cancelledStop = _buildCancelledUnitStopReason(
1947
+ unitType,
1948
+ unitId,
1949
+ unitResult.errorContext,
1877
1950
  );
1878
- await deps.stopAuto(ctx, pi, `Session creation failed: ${unitResult.errorContext?.message ?? "unknown"}`);
1879
- debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
1880
- return { action: "break", reason: "session-failed" };
1951
+ ctx.ui.notify(cancelledStop.notifyMessage, "warning");
1952
+ await deps.stopAuto(ctx, pi, cancelledStop.stopReason);
1953
+ debugLog("autoLoop", { phase: "exit", reason: cancelledStop.loopReason });
1954
+ return { action: "break", reason: cancelledStop.loopReason };
1881
1955
  }
1882
1956
 
1883
1957
  // ── Immediate unit closeout (metrics, activity log, memory) ────────
@@ -21,6 +21,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent
21
21
  import type { GitServiceImpl } from "../git-service.js";
22
22
  import type { CaptureEntry } from "../captures.js";
23
23
  import type { BudgetAlertLevel } from "../auto-budget.js";
24
+ import type { AutoOrchestrationModule } from "./contracts.js";
24
25
  import { resolveWorktreeProjectRoot } from "../worktree-root.js";
25
26
  import { normalizeRealPath } from "../paths.js";
26
27
  import type { MilestoneScope } from "../workspace.js";
@@ -229,6 +230,9 @@ export class AutoSession {
229
230
  /** Cleanup function returned by startCommandPolling(); null when not running. */
230
231
  commandPollingCleanup: (() => void) | null = null;
231
232
 
233
+ // ── Orchestration seam ───────────────────────────────────────────────────
234
+ orchestration: AutoOrchestrationModule | null = null;
235
+
232
236
  // ── Loop promise state ──────────────────────────────────────────────────
233
237
  // Per-unit resolve function and session-switch guard live at module level
234
238
  // in auto-loop.ts (_currentResolve, _sessionSwitchInFlight).
@@ -354,10 +358,14 @@ export class AutoSession {
354
358
  // Remote command polling — cleanup must be called before reset (auto.ts stopAuto)
355
359
  this.commandPollingCleanup = null;
356
360
 
361
+ // Orchestration seam
362
+ this.orchestration = null;
363
+
357
364
  // Loop promise state lives in auto-loop.ts module scope
358
365
  }
359
366
 
360
367
  toJSON(): Record<string, unknown> {
368
+ const orchestrationStatus = this.orchestration?.getStatus();
361
369
  return {
362
370
  active: this.active,
363
371
  paused: this.paused,
@@ -367,6 +375,9 @@ export class AutoSession {
367
375
  activeRunDir: this.activeRunDir,
368
376
  currentMilestoneId: this.currentMilestoneId,
369
377
  currentUnit: this.currentUnit,
378
+ orchestrationPhase: orchestrationStatus?.phase,
379
+ orchestrationTransitionCount: orchestrationStatus?.transitionCount,
380
+ orchestrationLastTransitionAt: orchestrationStatus?.lastTransitionAt,
370
381
  unitDispatchCount: Object.fromEntries(this.unitDispatchCount),
371
382
  };
372
383
  }
@@ -14,7 +14,7 @@ import { appendEvent } from "./workflow-events.js";
14
14
  import { atomicWriteSync } from "./atomic-write.js";
15
15
  import { clearParseCache } from "./files.js";
16
16
  import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
17
- import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus, insertSlice, getMilestone } from "./gsd-db.js";
17
+ import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus, insertSlice, getMilestone, refreshOpenDatabaseFromDisk } from "./gsd-db.js";
18
18
  import { isValidationTerminal } from "./state.js";
19
19
  import { getErrorMessage } from "./error-utils.js";
20
20
  import { logWarning, logError } from "./workflow-logger.js";
@@ -588,65 +588,32 @@ export function verifyExpectedArtifact(
588
588
  }
589
589
  }
590
590
 
591
- // plan-slice must produce a plan with actual task entries, not just a scaffold.
592
- // The plan file may exist from a prior discussion/context step with only headings
593
- // but no tasks. Without this check the artifact is considered "complete" and the
594
- // unit gets skipped — but deriveState still returns phase:"planning" because the
595
- // plan has no tasks, creating an infinite skip loop (#699).
596
- if (unitType === "plan-slice") {
597
- const planContent = readFileSync(absPath, "utf-8");
598
- // Accept checkbox-style (- [x] **T01: ...) or heading-style (### T01 -- / ### T01: / ### T01 —)
599
- const hasCheckboxTask = /^- \[[xX ]\] \*\*T\d+:/m.test(planContent);
600
- const hasHeadingTask = /^#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
601
- if (!hasCheckboxTask && !hasHeadingTask) {
602
- logWarning("recovery", `verify-fail ${unitType} ${unitId}: plan has no task checkbox/heading (len=${planContent.length}) at ${absPath}`);
603
- return false;
604
- }
605
- }
606
-
607
- // execute-task: DB status is authoritative. Fall back to checked-checkbox
608
- // detection when the DB is unavailable (unmigrated projects), or when the
609
- // disk artifacts already reflect completion but the DB replay is one beat
610
- // behind the completion write.
611
- if (unitType === "execute-task") {
612
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
613
- if (mid && sid && tid) {
614
- const dbTask = getTask(mid, sid, tid);
615
- if (dbTask) {
616
- if (dbTask.status !== "complete" && dbTask.status !== "done" && !hasCheckedTaskCompletionOnDisk(base, mid, sid, tid)) {
617
- return false;
618
- }
619
- } else if (!isDbAvailable()) {
620
- // LEGACY: Pre-migration fallback for projects without DB.
621
- // Require a CHECKED checkbox — a bare heading or unchecked checkbox
622
- // does not prove gsd_complete_task ran. Summary file on disk alone
623
- // is not sufficient evidence (could be a rogue write) (#3607).
624
- if (!hasCheckedTaskCompletionOnDisk(base, mid, sid, tid)) return false;
625
- } else {
626
- // DB available but task row not found — completion tool never ran (#3607)
627
- return false;
628
- }
629
- }
630
- }
631
-
632
- // plan-slice must also produce individual task plan files for every task listed
633
- // in the slice plan. Without this check, a plan-slice that wrote S{sid}-PLAN.md
634
- // but omitted T{tid}-PLAN.md files would be marked complete, causing execute-task
635
- // to dispatch with a missing task plan (see issue #739).
591
+ // plan-slice verification is DB-primary. The slice plan is a projection, so
592
+ // DB task rows prove the slice was planned even if the rendered markdown no
593
+ // longer uses legacy checkbox/heading syntax.
636
594
  if (unitType === "plan-slice") {
637
595
  const { milestone: mid, slice: sid } = parseUnitId(unitId);
638
596
  if (mid && sid) {
639
597
  try {
640
- // DB primary path — get task IDs to verify task plan files exist
641
598
  let taskIds: string[] | null = null;
642
599
  if (isDbAvailable()) {
643
- const tasks = getSliceTasks(mid, sid);
644
- if (tasks.length > 0) taskIds = tasks.map(t => t.id);
600
+ const refreshed = refreshOpenDatabaseFromDisk();
601
+ if (refreshed) {
602
+ const tasks = getSliceTasks(mid, sid);
603
+ if (tasks.length > 0) taskIds = tasks.map(t => t.id);
604
+ }
645
605
  }
646
606
 
647
607
  if (!taskIds) {
648
- // LEGACY: DB unavailable or no tasks in DB parse plan file for task IDs
608
+ // LEGACY: DB unavailable or no tasks in DB. Require actual task
609
+ // entries so an empty scaffold cannot advance the pipeline (#699).
649
610
  const planContent = readFileSync(absPath, "utf-8");
611
+ const hasCheckboxTask = /^\s*- \[[xX ]\] \*\*T\d+:/m.test(planContent);
612
+ const hasHeadingTask = /^\s*#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
613
+ if (!hasCheckboxTask && !hasHeadingTask) {
614
+ logWarning("recovery", `verify-fail ${unitType} ${unitId}: plan has no task checkbox/heading (len=${planContent.length}) at ${absPath}`);
615
+ return false;
616
+ }
650
617
  const plan = parseLegacyPlan(planContent);
651
618
  if (plan.tasks.length > 0) taskIds = plan.tasks.map((t: { id: string }) => t.id);
652
619
  }
@@ -672,6 +639,31 @@ export function verifyExpectedArtifact(
672
639
  }
673
640
  }
674
641
 
642
+ // execute-task: DB status is authoritative. Fall back to checked-checkbox
643
+ // detection when the DB is unavailable (unmigrated projects), or when the
644
+ // disk artifacts already reflect completion but the DB replay is one beat
645
+ // behind the completion write.
646
+ if (unitType === "execute-task") {
647
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
648
+ if (mid && sid && tid) {
649
+ const dbTask = getTask(mid, sid, tid);
650
+ if (dbTask) {
651
+ if (dbTask.status !== "complete" && dbTask.status !== "done" && !hasCheckedTaskCompletionOnDisk(base, mid, sid, tid)) {
652
+ return false;
653
+ }
654
+ } else if (!isDbAvailable()) {
655
+ // LEGACY: Pre-migration fallback for projects without DB.
656
+ // Require a CHECKED checkbox — a bare heading or unchecked checkbox
657
+ // does not prove gsd_complete_task ran. Summary file on disk alone
658
+ // is not sufficient evidence (could be a rogue write) (#3607).
659
+ if (!hasCheckedTaskCompletionOnDisk(base, mid, sid, tid)) return false;
660
+ } else {
661
+ // DB available but task row not found — completion tool never ran (#3607)
662
+ return false;
663
+ }
664
+ }
665
+ }
666
+
675
667
  // complete-slice: DB status is authoritative for whether the slice is done.
676
668
  // Fall back to file-based check (roadmap [x]) when DB is unavailable.
677
669
  if (unitType === "complete-slice") {
@@ -16,14 +16,21 @@ export type AutoRuntimeSnapshot = {
16
16
  paused: boolean;
17
17
  currentUnit: CurrentUnit | null;
18
18
  basePath: string;
19
+ orchestrationPhase?: "idle" | "running" | "paused" | "stopped" | "error";
20
+ orchestrationTransitionCount?: number;
21
+ orchestrationLastTransitionAt?: number;
19
22
  };
20
23
 
21
24
  export function getAutoRuntimeSnapshot(): AutoRuntimeSnapshot {
25
+ const orchestrationStatus = autoSession.orchestration?.getStatus();
22
26
  return {
23
27
  active: autoSession.active,
24
28
  paused: autoSession.paused,
25
29
  currentUnit: autoSession.currentUnit ? { ...autoSession.currentUnit } : null,
26
30
  basePath: autoSession.basePath,
31
+ orchestrationPhase: orchestrationStatus?.phase,
32
+ orchestrationTransitionCount: orchestrationStatus?.transitionCount,
33
+ orchestrationLastTransitionAt: orchestrationStatus?.lastTransitionAt,
27
34
  };
28
35
  }
29
36