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.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/contracts.js +1 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +146 -0
- package/dist/resources/extensions/gsd/auto/phases.js +61 -7
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +63 -55
- package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
- package/dist/resources/extensions/gsd/auto-start.js +3 -2
- package/dist/resources/extensions/gsd/auto.js +159 -2
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -8
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
- package/dist/resources/extensions/gsd/gsd-db.js +34 -1
- package/dist/resources/extensions/gsd/guided-flow.js +40 -0
- package/dist/resources/extensions/gsd/paths.js +5 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +45 -4
- package/dist/resources/extensions/gsd/uok/audit.js +23 -9
- package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
- package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
- package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
- package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
- package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
- package/dist/resources/extensions/shared/interview-ui.js +15 -4
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +13 -2
- package/src/resources/extensions/gsd/auto/contracts.ts +87 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -3
- package/src/resources/extensions/gsd/auto/orchestrator.ts +161 -0
- package/src/resources/extensions/gsd/auto/phases.ts +88 -9
- package/src/resources/extensions/gsd/auto/session.ts +11 -0
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +59 -53
- package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
- package/src/resources/extensions/gsd/auto-start.ts +3 -2
- package/src/resources/extensions/gsd/auto.ts +167 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +14 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +11 -8
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
- package/src/resources/extensions/gsd/gsd-db.ts +35 -1
- package/src/resources/extensions/gsd/guided-flow.ts +47 -0
- package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
- package/src/resources/extensions/gsd/paths.ts +6 -1
- package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
- package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
- package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +36 -7
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +47 -4
- package/src/resources/extensions/gsd/uok/audit.ts +25 -9
- package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
- package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
- package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
- package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
- package/src/resources/extensions/shared/interview-ui.ts +18 -5
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
- package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
- /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → zSHLdEn5cpkqivbJZq0Qq}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → zSHLdEn5cpkqivbJZq0Qq}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
|
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
|
|
499
|
-
//
|
|
500
|
-
//
|
|
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
|
|
552
|
-
if (
|
|
553
|
-
|
|
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
|
|
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(
|
|
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:
|