gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216
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/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve bundled raw resource files from the package root.
|
|
3
|
+
*
|
|
4
|
+
* Both `src/*.ts` and compiled `dist/*.js` entry points need to load the same
|
|
5
|
+
* raw `.ts` resource modules via jiti. Those modules are shipped under
|
|
6
|
+
* `src/resources/**`, not next to the compiled entry point.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveBundledSourceResource(importUrl: string, ...segments: string[]): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve bundled raw resource files from the package root.
|
|
5
|
+
*
|
|
6
|
+
* Both `src/*.ts` and compiled `dist/*.js` entry points need to load the same
|
|
7
|
+
* raw `.ts` resource modules via jiti. Those modules are shipped under
|
|
8
|
+
* `src/resources/**`, not next to the compiled entry point.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveBundledSourceResource(importUrl, ...segments) {
|
|
11
|
+
const moduleDir = dirname(fileURLToPath(importUrl));
|
|
12
|
+
const packageRoot = resolve(moduleDir, "..");
|
|
13
|
+
return join(packageRoot, "src", "resources", ...segments);
|
|
14
|
+
}
|
package/dist/headless-query.js
CHANGED
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { createJiti } from '@mariozechner/jiti';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
-
import {
|
|
19
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
import { resolveBundledSourceResource } from './bundled-resource-path.js';
|
|
20
19
|
const jiti = createJiti(fileURLToPath(import.meta.url), { interopDefault: true, debug: false });
|
|
20
|
+
const gsdExtensionPath = (...segments) => resolveBundledSourceResource(import.meta.url, 'extensions', 'gsd', ...segments);
|
|
21
21
|
async function loadExtensionModules() {
|
|
22
|
-
const stateModule = await jiti.import(
|
|
23
|
-
const dispatchModule = await jiti.import(
|
|
24
|
-
const sessionModule = await jiti.import(
|
|
25
|
-
const prefsModule = await jiti.import(
|
|
22
|
+
const stateModule = await jiti.import(gsdExtensionPath('state.ts'), {});
|
|
23
|
+
const dispatchModule = await jiti.import(gsdExtensionPath('auto-dispatch.ts'), {});
|
|
24
|
+
const sessionModule = await jiti.import(gsdExtensionPath('session-status-io.ts'), {});
|
|
25
|
+
const prefsModule = await jiti.import(gsdExtensionPath('preferences.ts'), {});
|
|
26
26
|
return {
|
|
27
27
|
deriveState: stateModule.deriveState,
|
|
28
28
|
resolveDispatch: dispatchModule.resolveDispatch,
|
|
@@ -19,17 +19,12 @@
|
|
|
19
19
|
export const MAX_UNIT_DISPATCHES = 3;
|
|
20
20
|
export const STUB_RECOVERY_THRESHOLD = 2;
|
|
21
21
|
export const MAX_LIFETIME_DISPATCHES = 6;
|
|
22
|
-
export const MAX_CONSECUTIVE_SKIPS = 3;
|
|
23
|
-
export const DISPATCH_GAP_TIMEOUT_MS = 5_000;
|
|
24
|
-
export const MAX_SKIP_DEPTH = 20;
|
|
25
22
|
export const NEW_SESSION_TIMEOUT_MS = 30_000;
|
|
26
|
-
export const DISPATCH_HANG_TIMEOUT_MS = 60_000;
|
|
27
23
|
// ─── AutoSession ─────────────────────────────────────────────────────────────
|
|
28
24
|
export class AutoSession {
|
|
29
25
|
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
30
26
|
active = false;
|
|
31
27
|
paused = false;
|
|
32
|
-
pausedForSecrets = false;
|
|
33
28
|
stepMode = false;
|
|
34
29
|
verbose = false;
|
|
35
30
|
cmdCtx = null;
|
|
@@ -41,14 +36,11 @@ export class AutoSession {
|
|
|
41
36
|
unitDispatchCount = new Map();
|
|
42
37
|
unitLifetimeDispatches = new Map();
|
|
43
38
|
unitRecoveryCount = new Map();
|
|
44
|
-
unitConsecutiveSkips = new Map();
|
|
45
|
-
completedKeySet = new Set();
|
|
46
39
|
// ── Timers ───────────────────────────────────────────────────────────────
|
|
47
40
|
unitTimeoutHandle = null;
|
|
48
41
|
wrapupWarningHandle = null;
|
|
49
42
|
idleWatchdogHandle = null;
|
|
50
43
|
continueHereHandle = null;
|
|
51
|
-
dispatchGapHandle = null;
|
|
52
44
|
// ── Current unit ─────────────────────────────────────────────────────────
|
|
53
45
|
currentUnit = null;
|
|
54
46
|
currentUnitRouting = null;
|
|
@@ -66,12 +58,8 @@ export class AutoSession {
|
|
|
66
58
|
pausedSessionFile = null;
|
|
67
59
|
resourceVersionOnStart = null;
|
|
68
60
|
lastStateRebuildAt = 0;
|
|
69
|
-
// ──
|
|
70
|
-
|
|
71
|
-
pendingAgentEndRetry = false;
|
|
72
|
-
dispatching = false;
|
|
73
|
-
skipDepth = 0;
|
|
74
|
-
recentlyEvictedKeys = new Set();
|
|
61
|
+
// ── Sidecar queue ─────────────────────────────────────────────────────
|
|
62
|
+
sidecarQueue = [];
|
|
75
63
|
// ── Metrics ──────────────────────────────────────────────────────────────
|
|
76
64
|
autoStartTime = 0;
|
|
77
65
|
lastPromptCharCount;
|
|
@@ -79,6 +67,26 @@ export class AutoSession {
|
|
|
79
67
|
pendingQuickTasks = [];
|
|
80
68
|
// ── Signal handler ───────────────────────────────────────────────────────
|
|
81
69
|
sigtermHandler = null;
|
|
70
|
+
// ── Loop promise state ──────────────────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* True only while runUnit is rotating into a fresh session. agent_end events
|
|
73
|
+
* emitted from the previous session's abort during this window must be
|
|
74
|
+
* ignored; they do not belong to the new unit.
|
|
75
|
+
*/
|
|
76
|
+
sessionSwitchInFlight = false;
|
|
77
|
+
/**
|
|
78
|
+
* One-shot resolver for the current unit's agent_end promise.
|
|
79
|
+
* Non-null only while a unit is in-flight (between sendMessage and agent_end).
|
|
80
|
+
* Scoped to the session to prevent concurrent session corruption.
|
|
81
|
+
*/
|
|
82
|
+
pendingResolve = null;
|
|
83
|
+
/**
|
|
84
|
+
* Queue for agent_end events that arrive when no pendingResolve exists.
|
|
85
|
+
* This happens when error-recovery sendMessage retries produce agent_end
|
|
86
|
+
* events between loop iterations. The next runUnit drains this queue
|
|
87
|
+
* instead of waiting for a new event.
|
|
88
|
+
*/
|
|
89
|
+
pendingAgentEndQueue = [];
|
|
82
90
|
// ── Methods ──────────────────────────────────────────────────────────────
|
|
83
91
|
clearTimers() {
|
|
84
92
|
if (this.unitTimeoutHandle) {
|
|
@@ -97,15 +105,10 @@ export class AutoSession {
|
|
|
97
105
|
clearInterval(this.continueHereHandle);
|
|
98
106
|
this.continueHereHandle = null;
|
|
99
107
|
}
|
|
100
|
-
if (this.dispatchGapHandle) {
|
|
101
|
-
clearTimeout(this.dispatchGapHandle);
|
|
102
|
-
this.dispatchGapHandle = null;
|
|
103
|
-
}
|
|
104
108
|
}
|
|
105
109
|
resetDispatchCounters() {
|
|
106
110
|
this.unitDispatchCount.clear();
|
|
107
111
|
this.unitLifetimeDispatches.clear();
|
|
108
|
-
this.unitConsecutiveSkips.clear();
|
|
109
112
|
}
|
|
110
113
|
get lockBasePath() {
|
|
111
114
|
return this.originalBasePath || this.basePath;
|
|
@@ -123,7 +126,6 @@ export class AutoSession {
|
|
|
123
126
|
// Lifecycle
|
|
124
127
|
this.active = false;
|
|
125
128
|
this.paused = false;
|
|
126
|
-
this.pausedForSecrets = false;
|
|
127
129
|
this.stepMode = false;
|
|
128
130
|
this.verbose = false;
|
|
129
131
|
this.cmdCtx = null;
|
|
@@ -135,9 +137,6 @@ export class AutoSession {
|
|
|
135
137
|
this.unitDispatchCount.clear();
|
|
136
138
|
this.unitLifetimeDispatches.clear();
|
|
137
139
|
this.unitRecoveryCount.clear();
|
|
138
|
-
this.unitConsecutiveSkips.clear();
|
|
139
|
-
// Note: completedKeySet is intentionally NOT cleared — it persists
|
|
140
|
-
// across restarts to prevent re-dispatching completed units.
|
|
141
140
|
// Unit
|
|
142
141
|
this.currentUnit = null;
|
|
143
142
|
this.currentUnitRouting = null;
|
|
@@ -155,19 +154,18 @@ export class AutoSession {
|
|
|
155
154
|
this.pausedSessionFile = null;
|
|
156
155
|
this.resourceVersionOnStart = null;
|
|
157
156
|
this.lastStateRebuildAt = 0;
|
|
158
|
-
// Guards
|
|
159
|
-
this.handlingAgentEnd = false;
|
|
160
|
-
this.pendingAgentEndRetry = false;
|
|
161
|
-
this.dispatching = false;
|
|
162
|
-
this.skipDepth = 0;
|
|
163
|
-
this.recentlyEvictedKeys.clear();
|
|
164
157
|
// Metrics
|
|
165
158
|
this.autoStartTime = 0;
|
|
166
159
|
this.lastPromptCharCount = undefined;
|
|
167
160
|
this.lastBaselineCharCount = undefined;
|
|
168
161
|
this.pendingQuickTasks = [];
|
|
162
|
+
this.sidecarQueue = [];
|
|
169
163
|
// Signal handler
|
|
170
164
|
this.sigtermHandler = null;
|
|
165
|
+
// Loop promise state
|
|
166
|
+
this.sessionSwitchInFlight = false;
|
|
167
|
+
this.pendingResolve = null;
|
|
168
|
+
this.pendingAgentEndQueue = [];
|
|
171
169
|
}
|
|
172
170
|
toJSON() {
|
|
173
171
|
return {
|
|
@@ -178,10 +176,7 @@ export class AutoSession {
|
|
|
178
176
|
currentMilestoneId: this.currentMilestoneId,
|
|
179
177
|
currentUnit: this.currentUnit,
|
|
180
178
|
completedUnits: this.completedUnits.length,
|
|
181
|
-
completedKeySet: this.completedKeySet.size,
|
|
182
179
|
unitDispatchCount: Object.fromEntries(this.unitDispatchCount),
|
|
183
|
-
dispatching: this.dispatching,
|
|
184
|
-
skipDepth: this.skipDepth,
|
|
185
180
|
};
|
|
186
181
|
}
|
|
187
182
|
}
|
|
@@ -7,42 +7,47 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { getCurrentBranch } from "./worktree.js";
|
|
9
9
|
import { getActiveHook } from "./post-unit-hooks.js";
|
|
10
|
-
import { getLedger, getProjectTotals,
|
|
11
|
-
import { getHealthTrend, getConsecutiveErrorUnits } from "./doctor-proactive.js";
|
|
10
|
+
import { getLedger, getProjectTotals, formatTierSavings } from "./metrics.js";
|
|
12
11
|
import { resolveMilestoneFile, resolveSliceFile, } from "./paths.js";
|
|
13
12
|
import { parseRoadmap, parsePlan } from "./files.js";
|
|
14
13
|
import { readFileSync, existsSync } from "node:fs";
|
|
15
14
|
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
16
15
|
import { makeUI, GLYPH, INDENT } from "../shared/mod.js";
|
|
17
|
-
import { parseUnitId } from "./unit-id.js";
|
|
18
16
|
// ─── Unit Description Helpers ─────────────────────────────────────────────────
|
|
19
|
-
/** Canonical verb and phase label for each known unit type. */
|
|
20
|
-
const UNIT_TYPE_INFO = {
|
|
21
|
-
"research-milestone": { verb: "researching", phaseLabel: "RESEARCH" },
|
|
22
|
-
"research-slice": { verb: "researching", phaseLabel: "RESEARCH" },
|
|
23
|
-
"plan-milestone": { verb: "planning", phaseLabel: "PLAN" },
|
|
24
|
-
"plan-slice": { verb: "planning", phaseLabel: "PLAN" },
|
|
25
|
-
"execute-task": { verb: "executing", phaseLabel: "EXECUTE" },
|
|
26
|
-
"complete-slice": { verb: "completing", phaseLabel: "COMPLETE" },
|
|
27
|
-
"replan-slice": { verb: "replanning", phaseLabel: "REPLAN" },
|
|
28
|
-
"rewrite-docs": { verb: "rewriting", phaseLabel: "REWRITE" },
|
|
29
|
-
"reassess-roadmap": { verb: "reassessing", phaseLabel: "REASSESS" },
|
|
30
|
-
"run-uat": { verb: "running UAT", phaseLabel: "UAT" },
|
|
31
|
-
};
|
|
32
17
|
export function unitVerb(unitType) {
|
|
33
18
|
if (unitType.startsWith("hook/"))
|
|
34
19
|
return `hook: ${unitType.slice(5)}`;
|
|
35
|
-
|
|
20
|
+
switch (unitType) {
|
|
21
|
+
case "research-milestone":
|
|
22
|
+
case "research-slice": return "researching";
|
|
23
|
+
case "plan-milestone":
|
|
24
|
+
case "plan-slice": return "planning";
|
|
25
|
+
case "execute-task": return "executing";
|
|
26
|
+
case "complete-slice": return "completing";
|
|
27
|
+
case "replan-slice": return "replanning";
|
|
28
|
+
case "rewrite-docs": return "rewriting";
|
|
29
|
+
case "reassess-roadmap": return "reassessing";
|
|
30
|
+
case "run-uat": return "running UAT";
|
|
31
|
+
default: return unitType;
|
|
32
|
+
}
|
|
36
33
|
}
|
|
37
34
|
export function unitPhaseLabel(unitType) {
|
|
38
35
|
if (unitType.startsWith("hook/"))
|
|
39
36
|
return "HOOK";
|
|
40
|
-
|
|
37
|
+
switch (unitType) {
|
|
38
|
+
case "research-milestone": return "RESEARCH";
|
|
39
|
+
case "research-slice": return "RESEARCH";
|
|
40
|
+
case "plan-milestone": return "PLAN";
|
|
41
|
+
case "plan-slice": return "PLAN";
|
|
42
|
+
case "execute-task": return "EXECUTE";
|
|
43
|
+
case "complete-slice": return "COMPLETE";
|
|
44
|
+
case "replan-slice": return "REPLAN";
|
|
45
|
+
case "rewrite-docs": return "REWRITE";
|
|
46
|
+
case "reassess-roadmap": return "REASSESS";
|
|
47
|
+
case "run-uat": return "UAT";
|
|
48
|
+
default: return unitType.toUpperCase();
|
|
49
|
+
}
|
|
41
50
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Describe the expected next step after the current unit completes.
|
|
44
|
-
* Unit types here mirror the keys in UNIT_TYPE_INFO above.
|
|
45
|
-
*/
|
|
46
51
|
function peekNext(unitType, state) {
|
|
47
52
|
// Show active hook info in progress display
|
|
48
53
|
const activeHookState = getActiveHook();
|
|
@@ -237,12 +242,6 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
|
|
|
237
242
|
}
|
|
238
243
|
if (cachedBranch)
|
|
239
244
|
widgetPwd = `${widgetPwd} (${cachedBranch})`;
|
|
240
|
-
// Set a string-array fallback first — this is the only version RPC mode will
|
|
241
|
-
// see, since the factory widget set below is not supported in RPC mode.
|
|
242
|
-
const progressText = buildProgressTextLines(verb, phaseLabel, unitId, mid, slice, task, next, accessors, tierBadge, widgetPwd);
|
|
243
|
-
ctx.ui.setWidget("gsd-progress", progressText);
|
|
244
|
-
// Set the factory-based widget — in TUI mode this replaces the string-array
|
|
245
|
-
// version with a dynamic, animated widget. In RPC mode this call is a no-op.
|
|
246
245
|
ctx.ui.setWidget("gsd-progress", (tui, theme) => {
|
|
247
246
|
let pulseBright = true;
|
|
248
247
|
let cachedLines;
|
|
@@ -288,11 +287,7 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
|
|
|
288
287
|
lines.push(truncateToWidth(`${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`, width));
|
|
289
288
|
}
|
|
290
289
|
lines.push("");
|
|
291
|
-
const
|
|
292
|
-
const hookParsed = isHook ? parseUnitId(unitId) : undefined;
|
|
293
|
-
const target = isHook
|
|
294
|
-
? (hookParsed.task ?? hookParsed.slice ?? unitId)
|
|
295
|
-
: (task ? `${task.id}: ${task.title}` : unitId);
|
|
290
|
+
const target = task ? `${task.id}: ${task.title}` : unitId;
|
|
296
291
|
const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
|
|
297
292
|
const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
|
|
298
293
|
const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
|
|
@@ -309,10 +304,7 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
|
|
|
309
304
|
+ theme.fg("dim", "░".repeat(barWidth - filled));
|
|
310
305
|
let meta = theme.fg("dim", `${done}/${total} slices`);
|
|
311
306
|
if (activeSliceTasks && activeSliceTasks.total > 0) {
|
|
312
|
-
|
|
313
|
-
const taskNum = isHook
|
|
314
|
-
? Math.max(activeSliceTasks.done, 1)
|
|
315
|
-
: Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
|
|
307
|
+
const taskNum = Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
|
|
316
308
|
meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
|
|
317
309
|
}
|
|
318
310
|
// ETA estimate
|
|
@@ -374,8 +366,6 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
|
|
|
374
366
|
}
|
|
375
367
|
if (cumulativeCost)
|
|
376
368
|
sp.push(`$${cumulativeCost.toFixed(3)}`);
|
|
377
|
-
else if (autoTotals?.apiRequests)
|
|
378
|
-
sp.push(`${autoTotals.apiRequests} reqs`);
|
|
379
369
|
const cxDisplay = cxPct === "?"
|
|
380
370
|
? `?/${formatWidgetTokens(cxWindow)}`
|
|
381
371
|
: `${cxPct}%/${formatWidgetTokens(cxWindow)}`;
|
|
@@ -429,76 +419,6 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
|
|
|
429
419
|
};
|
|
430
420
|
});
|
|
431
421
|
}
|
|
432
|
-
// ─── Text Fallback for RPC Mode ───────────────────────────────────────────
|
|
433
|
-
/**
|
|
434
|
-
* Build a compact string-array representation of the progress widget.
|
|
435
|
-
* Used as a fallback when the factory-based widget cannot render (RPC mode).
|
|
436
|
-
*/
|
|
437
|
-
// ─── Model Health Indicator ───────────────────────────────────────────────────
|
|
438
|
-
/**
|
|
439
|
-
* Compute a traffic-light health indicator from observable signals.
|
|
440
|
-
* 🟢 progressing well — no errors, trend stable/improving
|
|
441
|
-
* 🟡 struggling — some errors or degrading trend
|
|
442
|
-
* 🔴 stuck — consecutive errors, likely needs attention
|
|
443
|
-
*/
|
|
444
|
-
export function getModelHealthIndicator() {
|
|
445
|
-
const trend = getHealthTrend();
|
|
446
|
-
const consecutiveErrors = getConsecutiveErrorUnits();
|
|
447
|
-
if (consecutiveErrors >= 3) {
|
|
448
|
-
return { emoji: "🔴", label: "stuck" };
|
|
449
|
-
}
|
|
450
|
-
if (consecutiveErrors >= 1 || trend === "degrading") {
|
|
451
|
-
return { emoji: "🟡", label: "struggling" };
|
|
452
|
-
}
|
|
453
|
-
if (trend === "improving") {
|
|
454
|
-
return { emoji: "🟢", label: "progressing well" };
|
|
455
|
-
}
|
|
456
|
-
// stable or unknown
|
|
457
|
-
return { emoji: "🟢", label: "progressing" };
|
|
458
|
-
}
|
|
459
|
-
function buildProgressTextLines(verb, phaseLabel, unitId, mid, slice, task, next, accessors, tierBadge, widgetPwd) {
|
|
460
|
-
const mode = accessors.isStepMode() ? "step" : "auto";
|
|
461
|
-
const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
|
|
462
|
-
const tierStr = tierBadge ? ` [${tierBadge}]` : "";
|
|
463
|
-
const lines = [];
|
|
464
|
-
lines.push(`[GSD ${mode}] ${verb} ${unitId}${tierStr}${elapsed ? ` — ${elapsed}` : ""}`);
|
|
465
|
-
if (mid)
|
|
466
|
-
lines.push(` Milestone: ${mid.id} — ${mid.title}`);
|
|
467
|
-
if (slice)
|
|
468
|
-
lines.push(` Slice: ${slice.id} — ${slice.title}`);
|
|
469
|
-
if (task)
|
|
470
|
-
lines.push(` Task: ${task.id} — ${task.title}`);
|
|
471
|
-
// Progress bar
|
|
472
|
-
const sp = cachedSliceProgress;
|
|
473
|
-
if (sp && sp.total > 0) {
|
|
474
|
-
const pct = Math.round((sp.done / sp.total) * 100);
|
|
475
|
-
const taskInfo = sp.activeSliceTasks
|
|
476
|
-
? ` (tasks: ${sp.activeSliceTasks.done}/${sp.activeSliceTasks.total})`
|
|
477
|
-
: "";
|
|
478
|
-
lines.push(` Progress: ${sp.done}/${sp.total} slices (${pct}%)${taskInfo}`);
|
|
479
|
-
}
|
|
480
|
-
// Cost / tokens
|
|
481
|
-
const ledger = getLedger();
|
|
482
|
-
const totals = ledger ? getProjectTotals(ledger.units) : null;
|
|
483
|
-
if (totals) {
|
|
484
|
-
const parts = [];
|
|
485
|
-
if (totals.tokens.input || totals.tokens.output) {
|
|
486
|
-
parts.push(`tokens: ${formatWidgetTokens(totals.tokens.input)}↑ ${formatWidgetTokens(totals.tokens.output)}↓`);
|
|
487
|
-
}
|
|
488
|
-
if (totals.cost > 0) {
|
|
489
|
-
parts.push(`cost: ${formatCost(totals.cost)}`);
|
|
490
|
-
}
|
|
491
|
-
if (parts.length > 0)
|
|
492
|
-
lines.push(` ${parts.join(" — ")}`);
|
|
493
|
-
}
|
|
494
|
-
if (next)
|
|
495
|
-
lines.push(` Next: ${next}`);
|
|
496
|
-
// Model health indicator
|
|
497
|
-
const health = getModelHealthIndicator();
|
|
498
|
-
lines.push(` Health: ${health.emoji} ${health.label}`);
|
|
499
|
-
lines.push(` ${widgetPwd}`);
|
|
500
|
-
return lines;
|
|
501
|
-
}
|
|
502
422
|
// ─── Right-align Helper ───────────────────────────────────────────────────────
|
|
503
423
|
/** Right-align helper: build a line with left content and right content. */
|
|
504
424
|
function rightAlign(left, right, width) {
|
|
@@ -147,10 +147,15 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
|
|
|
147
147
|
ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
|
+
const uatContent = await loadFile(uatFile);
|
|
151
|
+
if (!uatContent) {
|
|
152
|
+
ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
150
155
|
const uatPath = relSliceFile(base, mid, sid, "UAT");
|
|
151
156
|
unitType = "run-uat";
|
|
152
157
|
unitId = `${mid}/${sid}`;
|
|
153
|
-
prompt = await buildRunUatPrompt(mid, sid, uatPath, base);
|
|
158
|
+
prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
|
|
154
159
|
break;
|
|
155
160
|
}
|
|
156
161
|
case "replan":
|
|
@@ -8,36 +8,24 @@
|
|
|
8
8
|
* data structure that is inspectable, testable per-rule, and extensible
|
|
9
9
|
* without modifying orchestration code.
|
|
10
10
|
*/
|
|
11
|
-
import { loadFile, loadActiveOverrides
|
|
11
|
+
import { loadFile, loadActiveOverrides } from "./files.js";
|
|
12
12
|
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile, relSliceFile, buildMilestoneFileName, } from "./paths.js";
|
|
13
13
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { buildResearchMilestonePrompt, buildPlanMilestonePrompt, buildResearchSlicePrompt, buildPlanSlicePrompt, buildExecuteTaskPrompt, buildCompleteSlicePrompt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt, buildReplanSlicePrompt, buildRunUatPrompt, buildReassessRoadmapPrompt, buildRewriteDocsPrompt, checkNeedsReassessment, checkNeedsRunUat, } from "./auto-prompts.js";
|
|
16
|
+
function missingSliceStop(mid, phase) {
|
|
17
|
+
return {
|
|
18
|
+
action: "stop",
|
|
19
|
+
reason: `${mid}: phase "${phase}" has no active slice — run /gsd doctor.`,
|
|
20
|
+
level: "error",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
16
23
|
// ─── Rewrite Circuit Breaker ──────────────────────────────────────────────
|
|
17
24
|
const MAX_REWRITE_ATTEMPTS = 3;
|
|
18
25
|
let rewriteAttemptCount = 0;
|
|
19
26
|
export function resetRewriteCircuitBreaker() {
|
|
20
27
|
rewriteAttemptCount = 0;
|
|
21
28
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Guard for accessing activeSlice/activeTask in dispatch rules.
|
|
24
|
-
* Returns a stop action if the expected ref is null (corrupt state).
|
|
25
|
-
*/
|
|
26
|
-
function requireSlice(state) {
|
|
27
|
-
if (!state.activeSlice) {
|
|
28
|
-
return { action: "stop", reason: `Phase "${state.phase}" but no active slice — run /gsd doctor.`, level: "error" };
|
|
29
|
-
}
|
|
30
|
-
return { sid: state.activeSlice.id, sTitle: state.activeSlice.title };
|
|
31
|
-
}
|
|
32
|
-
function requireTask(state) {
|
|
33
|
-
if (!state.activeSlice || !state.activeTask) {
|
|
34
|
-
return { action: "stop", reason: `Phase "${state.phase}" but no active slice/task — run /gsd doctor.`, level: "error" };
|
|
35
|
-
}
|
|
36
|
-
return { sid: state.activeSlice.id, sTitle: state.activeSlice.title, tid: state.activeTask.id, tTitle: state.activeTask.title };
|
|
37
|
-
}
|
|
38
|
-
function isStopAction(v) {
|
|
39
|
-
return typeof v === "object" && v !== null && "action" in v;
|
|
40
|
-
}
|
|
41
29
|
// ─── Rules ────────────────────────────────────────────────────────────────
|
|
42
30
|
const DISPATCH_RULES = [
|
|
43
31
|
{
|
|
@@ -67,10 +55,10 @@ const DISPATCH_RULES = [
|
|
|
67
55
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
68
56
|
if (state.phase !== "summarizing")
|
|
69
57
|
return null;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
58
|
+
if (!state.activeSlice)
|
|
59
|
+
return missingSliceStop(mid, state.phase);
|
|
60
|
+
const sid = state.activeSlice.id;
|
|
61
|
+
const sTitle = state.activeSlice.title;
|
|
74
62
|
return {
|
|
75
63
|
action: "dispatch",
|
|
76
64
|
unitType: "complete-slice",
|
|
@@ -79,57 +67,28 @@ const DISPATCH_RULES = [
|
|
|
79
67
|
};
|
|
80
68
|
},
|
|
81
69
|
},
|
|
82
|
-
{
|
|
83
|
-
name: "uat-verdict-gate (non-PASS blocks progression)",
|
|
84
|
-
match: async ({ mid, basePath, prefs }) => {
|
|
85
|
-
// Only applies when UAT dispatch is enabled
|
|
86
|
-
if (!prefs?.uat_dispatch)
|
|
87
|
-
return null;
|
|
88
|
-
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
89
|
-
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
90
|
-
if (!roadmapContent)
|
|
91
|
-
return null;
|
|
92
|
-
const roadmap = parseRoadmap(roadmapContent);
|
|
93
|
-
for (const slice of roadmap.slices.filter(s => s.done)) {
|
|
94
|
-
const resultFile = resolveSliceFile(basePath, mid, slice.id, "UAT-RESULT");
|
|
95
|
-
if (!resultFile)
|
|
96
|
-
continue;
|
|
97
|
-
const content = await loadFile(resultFile);
|
|
98
|
-
if (!content)
|
|
99
|
-
continue;
|
|
100
|
-
const verdictMatch = content.match(/verdict:\s*([\w-]+)/i);
|
|
101
|
-
const verdict = verdictMatch?.[1]?.toLowerCase();
|
|
102
|
-
if (verdict && verdict !== "pass" && verdict !== "passed") {
|
|
103
|
-
return {
|
|
104
|
-
action: "stop",
|
|
105
|
-
reason: `UAT verdict for ${slice.id} is "${verdict}" — blocking progression until resolved.\nReview the UAT result and update the verdict to PASS, or re-run /gsd auto after fixing.`,
|
|
106
|
-
level: "warning",
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return null;
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
70
|
{
|
|
114
71
|
name: "run-uat (post-completion)",
|
|
115
72
|
match: async ({ state, mid, basePath, prefs }) => {
|
|
116
73
|
const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
|
|
117
74
|
if (!needsRunUat)
|
|
118
75
|
return null;
|
|
119
|
-
const { sliceId } = needsRunUat;
|
|
76
|
+
const { sliceId, uatType } = needsRunUat;
|
|
77
|
+
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT");
|
|
78
|
+
const uatContent = await loadFile(uatFile);
|
|
120
79
|
return {
|
|
121
80
|
action: "dispatch",
|
|
122
81
|
unitType: "run-uat",
|
|
123
82
|
unitId: `${mid}/${sliceId}`,
|
|
124
|
-
prompt: await buildRunUatPrompt(mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), basePath),
|
|
83
|
+
prompt: await buildRunUatPrompt(mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath),
|
|
84
|
+
pauseAfterDispatch: uatType !== "artifact-driven",
|
|
125
85
|
};
|
|
126
86
|
},
|
|
127
87
|
},
|
|
128
88
|
{
|
|
129
89
|
name: "reassess-roadmap (post-completion)",
|
|
130
90
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
131
|
-
|
|
132
|
-
if (!prefs?.phases?.reassess_after_slice)
|
|
91
|
+
if (prefs?.phases?.skip_reassess || !prefs?.phases?.reassess_after_slice)
|
|
133
92
|
return null;
|
|
134
93
|
const needsReassess = await checkNeedsReassessment(basePath, mid, state);
|
|
135
94
|
if (!needsReassess)
|
|
@@ -160,7 +119,7 @@ const DISPATCH_RULES = [
|
|
|
160
119
|
if (state.phase !== "pre-planning")
|
|
161
120
|
return null;
|
|
162
121
|
const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
|
|
163
|
-
const hasContext = !!(contextFile && await loadFile(contextFile));
|
|
122
|
+
const hasContext = !!(contextFile && (await loadFile(contextFile)));
|
|
164
123
|
if (hasContext)
|
|
165
124
|
return null; // fall through to next rule
|
|
166
125
|
return {
|
|
@@ -210,10 +169,10 @@ const DISPATCH_RULES = [
|
|
|
210
169
|
// Phase skip: skip research when preference or profile says so
|
|
211
170
|
if (prefs?.phases?.skip_research || prefs?.phases?.skip_slice_research)
|
|
212
171
|
return null;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
172
|
+
if (!state.activeSlice)
|
|
173
|
+
return missingSliceStop(mid, state.phase);
|
|
174
|
+
const sid = state.activeSlice.id;
|
|
175
|
+
const sTitle = state.activeSlice.title;
|
|
217
176
|
const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
|
|
218
177
|
if (researchFile)
|
|
219
178
|
return null; // has research, fall through
|
|
@@ -235,10 +194,10 @@ const DISPATCH_RULES = [
|
|
|
235
194
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
236
195
|
if (state.phase !== "planning")
|
|
237
196
|
return null;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
197
|
+
if (!state.activeSlice)
|
|
198
|
+
return missingSliceStop(mid, state.phase);
|
|
199
|
+
const sid = state.activeSlice.id;
|
|
200
|
+
const sTitle = state.activeSlice.title;
|
|
242
201
|
return {
|
|
243
202
|
action: "dispatch",
|
|
244
203
|
unitType: "plan-slice",
|
|
@@ -252,10 +211,10 @@ const DISPATCH_RULES = [
|
|
|
252
211
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
253
212
|
if (state.phase !== "replanning-slice")
|
|
254
213
|
return null;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
214
|
+
if (!state.activeSlice)
|
|
215
|
+
return missingSliceStop(mid, state.phase);
|
|
216
|
+
const sid = state.activeSlice.id;
|
|
217
|
+
const sTitle = state.activeSlice.title;
|
|
259
218
|
return {
|
|
260
219
|
action: "dispatch",
|
|
261
220
|
unitType: "replan-slice",
|
|
@@ -269,10 +228,10 @@ const DISPATCH_RULES = [
|
|
|
269
228
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
270
229
|
if (state.phase !== "executing" || !state.activeTask)
|
|
271
230
|
return null;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
231
|
+
if (!state.activeSlice)
|
|
232
|
+
return missingSliceStop(mid, state.phase);
|
|
233
|
+
const sid = state.activeSlice.id;
|
|
234
|
+
const sTitle = state.activeSlice.title;
|
|
276
235
|
const tid = state.activeTask.id;
|
|
277
236
|
// Guard: if the slice plan exists but the individual task plan files are
|
|
278
237
|
// missing, the planner created S##-PLAN.md with task entries but never
|
|
@@ -296,10 +255,10 @@ const DISPATCH_RULES = [
|
|
|
296
255
|
match: async ({ state, mid, basePath }) => {
|
|
297
256
|
if (state.phase !== "executing" || !state.activeTask)
|
|
298
257
|
return null;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
258
|
+
if (!state.activeSlice)
|
|
259
|
+
return missingSliceStop(mid, state.phase);
|
|
260
|
+
const sid = state.activeSlice.id;
|
|
261
|
+
const sTitle = state.activeSlice.title;
|
|
303
262
|
const tid = state.activeTask.id;
|
|
304
263
|
const tTitle = state.activeTask.title;
|
|
305
264
|
return {
|
|
@@ -357,6 +316,18 @@ const DISPATCH_RULES = [
|
|
|
357
316
|
};
|
|
358
317
|
},
|
|
359
318
|
},
|
|
319
|
+
{
|
|
320
|
+
name: "complete → stop",
|
|
321
|
+
match: async ({ state }) => {
|
|
322
|
+
if (state.phase !== "complete")
|
|
323
|
+
return null;
|
|
324
|
+
return {
|
|
325
|
+
action: "stop",
|
|
326
|
+
reason: "All milestones complete.",
|
|
327
|
+
level: "info",
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
},
|
|
360
331
|
];
|
|
361
332
|
// ─── Resolver ─────────────────────────────────────────────────────────────
|
|
362
333
|
/**
|
|
@@ -378,5 +349,5 @@ export async function resolveDispatch(ctx) {
|
|
|
378
349
|
}
|
|
379
350
|
/** Exposed for testing — returns the rule names in evaluation order. */
|
|
380
351
|
export function getDispatchRuleNames() {
|
|
381
|
-
return DISPATCH_RULES.map(r => r.name);
|
|
352
|
+
return DISPATCH_RULES.map((r) => r.name);
|
|
382
353
|
}
|