gsd-pi 2.82.0-dev.725028083 → 2.82.0-dev.c22380fc3
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/orchestrator.js +113 -6
- package/dist/resources/extensions/gsd/auto.js +128 -52
- package/dist/resources/extensions/gsd/md-importer.js +1 -1
- package/dist/resources/extensions/gsd/migrate/command.js +5 -0
- package/dist/resources/extensions/gsd/migrate/preview.js +9 -0
- package/dist/resources/extensions/gsd/migrate/transformer.js +51 -4
- package/dist/resources/extensions/gsd/migrate/writer.js +11 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +119 -0
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +21 -2
- 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/contracts/dist/rpc.test.js +7 -0
- package/packages/contracts/dist/rpc.test.js.map +1 -1
- package/packages/contracts/dist/workflow.d.ts +21 -0
- package/packages/contracts/dist/workflow.d.ts.map +1 -1
- package/packages/contracts/dist/workflow.js +24 -0
- package/packages/contracts/dist/workflow.js.map +1 -1
- package/packages/contracts/src/rpc.test.ts +8 -0
- package/packages/contracts/src/workflow.ts +24 -0
- package/packages/mcp-server/README.md +13 -4
- package/packages/mcp-server/dist/workflow-tools.d.ts +0 -3
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +80 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +22 -0
- package/packages/mcp-server/src/workflow-tools.ts +168 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +5 -0
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +6 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/contracts.ts +46 -11
- package/src/resources/extensions/gsd/auto/orchestrator.ts +118 -6
- package/src/resources/extensions/gsd/auto.ts +136 -51
- package/src/resources/extensions/gsd/md-importer.ts +1 -1
- package/src/resources/extensions/gsd/migrate/command.ts +5 -0
- package/src/resources/extensions/gsd/migrate/preview.ts +10 -0
- package/src/resources/extensions/gsd/migrate/transformer.ts +58 -4
- package/src/resources/extensions/gsd/migrate/writer.ts +14 -1
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +408 -4
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -5
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/migrate-command.test.ts +48 -3
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +25 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +135 -0
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +20 -2
- /package/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → Wop3A7KRGyR06H3rla_1-}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → Wop3A7KRGyR06H3rla_1-}/_ssgManifest.js +0 -0
|
@@ -7,6 +7,16 @@ function now(): number {
|
|
|
7
7
|
return Date.now();
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Size of the dispatch-decision ring buffer used by the Auto Orchestration
|
|
12
|
+
* module's stuck-loop detector. When the same `${unitType}:${unitId}` key
|
|
13
|
+
* fills the window, advance() blocks with `action: "stop"`.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the legacy `STUCK_WINDOW_SIZE` in auto/phases.ts so behaviour is
|
|
16
|
+
* preserved across the eventual cutover (issue #5791).
|
|
17
|
+
*/
|
|
18
|
+
export const STUCK_WINDOW_SIZE = 6;
|
|
19
|
+
|
|
10
20
|
export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
11
21
|
private status: AutoStatus = {
|
|
12
22
|
phase: "idle",
|
|
@@ -14,6 +24,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
14
24
|
};
|
|
15
25
|
private readonly deps: AutoOrchestratorDeps;
|
|
16
26
|
private lastAdvanceKey: string | null = null;
|
|
27
|
+
private dispatchKeyWindow: string[] = [];
|
|
17
28
|
|
|
18
29
|
public constructor(deps: AutoOrchestratorDeps) {
|
|
19
30
|
this.deps = deps;
|
|
@@ -21,6 +32,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
21
32
|
|
|
22
33
|
public async start(_sessionContext: AutoSessionContext): Promise<AutoAdvanceResult> {
|
|
23
34
|
this.lastAdvanceKey = null;
|
|
35
|
+
this.dispatchKeyWindow = [];
|
|
24
36
|
this.status.phase = "running";
|
|
25
37
|
this.bumpTransition();
|
|
26
38
|
await this.deps.runtime.journalTransition({ name: "start" });
|
|
@@ -31,19 +43,72 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
31
43
|
public async advance(): Promise<AutoAdvanceResult> {
|
|
32
44
|
try {
|
|
33
45
|
await this.deps.runtime.ensureLockOwnership();
|
|
46
|
+
|
|
47
|
+
const staleMsg = this.deps.health.checkResourcesStale();
|
|
48
|
+
if (staleMsg) {
|
|
49
|
+
await this.deps.uokGate.emit({
|
|
50
|
+
gateId: "resource-version-guard",
|
|
51
|
+
gateType: "policy",
|
|
52
|
+
outcome: "fail",
|
|
53
|
+
failureClass: "policy",
|
|
54
|
+
rationale: "resource version guard blocked dispatch",
|
|
55
|
+
findings: staleMsg,
|
|
56
|
+
});
|
|
57
|
+
const blocked: AutoAdvanceResult = { kind: "blocked", reason: staleMsg, action: "stop" };
|
|
58
|
+
await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
59
|
+
await this.deps.health.postAdvanceRecord(blocked);
|
|
60
|
+
return blocked;
|
|
61
|
+
}
|
|
62
|
+
await this.deps.uokGate.emit({
|
|
63
|
+
gateId: "resource-version-guard",
|
|
64
|
+
gateType: "policy",
|
|
65
|
+
outcome: "pass",
|
|
66
|
+
failureClass: "none",
|
|
67
|
+
rationale: "resource version guard passed",
|
|
68
|
+
});
|
|
69
|
+
|
|
34
70
|
const gate = await this.deps.health.preAdvanceGate();
|
|
35
|
-
if (
|
|
36
|
-
|
|
71
|
+
if (gate.kind === "fail") {
|
|
72
|
+
await this.deps.uokGate.emit({
|
|
73
|
+
gateId: "pre-dispatch-health-gate",
|
|
74
|
+
gateType: "execution",
|
|
75
|
+
outcome: "manual-attention",
|
|
76
|
+
failureClass: "manual-attention",
|
|
77
|
+
rationale: "pre-dispatch health gate blocked dispatch",
|
|
78
|
+
findings: gate.reason,
|
|
79
|
+
});
|
|
80
|
+
const blocked: AutoAdvanceResult = { kind: "blocked", reason: gate.reason, action: "pause" };
|
|
37
81
|
await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
38
82
|
await this.deps.health.postAdvanceRecord(blocked);
|
|
39
83
|
return blocked;
|
|
40
84
|
}
|
|
85
|
+
if (gate.kind === "threw") {
|
|
86
|
+
await this.deps.uokGate.emit({
|
|
87
|
+
gateId: "pre-dispatch-health-gate",
|
|
88
|
+
gateType: "execution",
|
|
89
|
+
outcome: "manual-attention",
|
|
90
|
+
failureClass: "manual-attention",
|
|
91
|
+
rationale: "pre-dispatch health gate threw unexpectedly",
|
|
92
|
+
findings: String(gate.error),
|
|
93
|
+
});
|
|
94
|
+
// intentional fall-through: matches runPreDispatch behaviour
|
|
95
|
+
} else {
|
|
96
|
+
await this.deps.uokGate.emit({
|
|
97
|
+
gateId: "pre-dispatch-health-gate",
|
|
98
|
+
gateType: "execution",
|
|
99
|
+
outcome: "pass",
|
|
100
|
+
failureClass: "none",
|
|
101
|
+
rationale: "pre-dispatch health gate passed",
|
|
102
|
+
findings: gate.fixesApplied?.join(", ") ?? "",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
41
105
|
|
|
42
106
|
const reconciliation = await this.deps.stateReconciliation.reconcileBeforeDispatch();
|
|
43
107
|
if (!reconciliation.ok || !reconciliation.stateSnapshot) {
|
|
44
108
|
const blocked: AutoAdvanceResult = {
|
|
45
109
|
kind: "blocked",
|
|
46
|
-
reason: reconciliation.reason,
|
|
110
|
+
reason: reconciliation.reason ?? "state reconciliation produced no snapshot",
|
|
111
|
+
action: "pause",
|
|
47
112
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
48
113
|
};
|
|
49
114
|
await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
@@ -57,6 +122,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
57
122
|
this.status.phase = "stopped";
|
|
58
123
|
this.status.activeUnit = undefined;
|
|
59
124
|
this.lastAdvanceKey = null;
|
|
125
|
+
this.dispatchKeyWindow = [];
|
|
60
126
|
this.bumpTransition();
|
|
61
127
|
await this.deps.runtime.journalTransition({ name: "advance-stopped", reason: stopped.reason });
|
|
62
128
|
await this.deps.health.postAdvanceRecord(stopped);
|
|
@@ -64,8 +130,45 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
64
130
|
}
|
|
65
131
|
|
|
66
132
|
const nextKey = `${decision.unitType}:${decision.unitId}`;
|
|
67
|
-
|
|
68
|
-
|
|
133
|
+
|
|
134
|
+
// Record every dispatch decision in the ring buffer before pre-flight
|
|
135
|
+
// checks so the stuck-loop detector observes the full decision history
|
|
136
|
+
// (including decisions that idempotency would otherwise short-circuit).
|
|
137
|
+
// The ring is capped at STUCK_WINDOW_SIZE and evicts oldest-first.
|
|
138
|
+
this.dispatchKeyWindow.push(nextKey);
|
|
139
|
+
if (this.dispatchKeyWindow.length > STUCK_WINDOW_SIZE) {
|
|
140
|
+
this.dispatchKeyWindow.shift();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Idempotency: same key as immediately previous successful advance.
|
|
144
|
+
// This is the soft, fast-path block kept from #5786. It only fires when
|
|
145
|
+
// the ring is NOT yet saturated for this key — once the ring is full of
|
|
146
|
+
// `nextKey`, the stuck-loop verdict takes precedence (see below). Both
|
|
147
|
+
// checks coexist: idempotency for the common immediate-repeat case,
|
|
148
|
+
// stuck-loop for the saturated-window case.
|
|
149
|
+
const matchingCount = this.dispatchKeyWindow.filter((k) => k === nextKey).length;
|
|
150
|
+
if (this.lastAdvanceKey === nextKey && matchingCount < STUCK_WINDOW_SIZE) {
|
|
151
|
+
const blocked: AutoAdvanceResult = { kind: "blocked", reason: "idempotent advance: unit already active", action: "stop" };
|
|
152
|
+
await this.deps.runtime.journalTransition({
|
|
153
|
+
name: "advance-blocked",
|
|
154
|
+
reason: blocked.reason,
|
|
155
|
+
unitType: decision.unitType,
|
|
156
|
+
unitId: decision.unitId,
|
|
157
|
+
});
|
|
158
|
+
await this.deps.health.postAdvanceRecord(blocked);
|
|
159
|
+
return blocked;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Stuck-loop detection: when the ring is saturated with copies of
|
|
163
|
+
// `nextKey` (count >= STUCK_WINDOW_SIZE), the orchestrator has been
|
|
164
|
+
// picking the same unit across the whole window and must hard-stop with
|
|
165
|
+
// a diagnosable reason.
|
|
166
|
+
if (matchingCount >= STUCK_WINDOW_SIZE) {
|
|
167
|
+
const blocked: AutoAdvanceResult = {
|
|
168
|
+
kind: "blocked",
|
|
169
|
+
reason: `stuck-loop: ${nextKey} picked ${matchingCount} times`,
|
|
170
|
+
action: "stop",
|
|
171
|
+
};
|
|
69
172
|
await this.deps.runtime.journalTransition({
|
|
70
173
|
name: "advance-blocked",
|
|
71
174
|
reason: blocked.reason,
|
|
@@ -81,6 +184,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
81
184
|
const blocked: AutoAdvanceResult = {
|
|
82
185
|
kind: "blocked",
|
|
83
186
|
reason: contract.reason,
|
|
187
|
+
action: "pause",
|
|
84
188
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
85
189
|
};
|
|
86
190
|
await this.deps.runtime.journalTransition({
|
|
@@ -98,6 +202,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
98
202
|
const blocked: AutoAdvanceResult = {
|
|
99
203
|
kind: "blocked",
|
|
100
204
|
reason: worktree.reason,
|
|
205
|
+
action: "pause",
|
|
101
206
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
102
207
|
};
|
|
103
208
|
await this.deps.runtime.journalTransition({
|
|
@@ -123,7 +228,11 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
123
228
|
});
|
|
124
229
|
await this.deps.worktree.syncAfterUnit(decision.unitType, decision.unitId);
|
|
125
230
|
|
|
126
|
-
const advanced: AutoAdvanceResult = {
|
|
231
|
+
const advanced: AutoAdvanceResult = {
|
|
232
|
+
kind: "advanced",
|
|
233
|
+
unit: { unitType: decision.unitType, unitId: decision.unitId },
|
|
234
|
+
stateSnapshot: reconciliation.stateSnapshot,
|
|
235
|
+
};
|
|
127
236
|
await this.deps.health.postAdvanceRecord(advanced);
|
|
128
237
|
return advanced;
|
|
129
238
|
} catch (error) {
|
|
@@ -148,6 +257,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
148
257
|
|
|
149
258
|
if (result.kind === "stopped") {
|
|
150
259
|
this.lastAdvanceKey = null;
|
|
260
|
+
this.dispatchKeyWindow = [];
|
|
151
261
|
this.status.activeUnit = undefined;
|
|
152
262
|
}
|
|
153
263
|
this.bumpTransition();
|
|
@@ -173,6 +283,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
173
283
|
|
|
174
284
|
public async resume(): Promise<AutoAdvanceResult> {
|
|
175
285
|
this.lastAdvanceKey = null;
|
|
286
|
+
this.dispatchKeyWindow = [];
|
|
176
287
|
this.status.phase = "running";
|
|
177
288
|
this.bumpTransition();
|
|
178
289
|
await this.deps.runtime.journalTransition({ name: "resume" });
|
|
@@ -188,6 +299,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
188
299
|
this.status.phase = "stopped";
|
|
189
300
|
this.status.activeUnit = undefined;
|
|
190
301
|
this.lastAdvanceKey = null;
|
|
302
|
+
this.dispatchKeyWindow = [];
|
|
191
303
|
this.bumpTransition();
|
|
192
304
|
await this.deps.runtime.journalTransition({ name: "stop", reason });
|
|
193
305
|
await this.deps.notifications.notifyLifecycle({ name: "stop", detail: reason });
|
|
@@ -240,12 +240,14 @@ import { runAutoLoopWithUok } from "./uok/kernel.js";
|
|
|
240
240
|
import { resolveUokFlags } from "./uok/flags.js";
|
|
241
241
|
import { validateDirectory } from "./validate-directory.js";
|
|
242
242
|
import { createAutoOrchestrator } from "./auto/orchestrator.js";
|
|
243
|
-
import type { AutoOrchestrationModule, AutoOrchestratorDeps } from "./auto/contracts.js";
|
|
243
|
+
import type { AutoOrchestrationModule, AutoOrchestratorDeps, DispatchAdapter } from "./auto/contracts.js";
|
|
244
244
|
import { reconcileBeforeDispatch } from "./state-reconciliation.js";
|
|
245
245
|
import { compileUnitToolContract } from "./tool-contract.js";
|
|
246
246
|
import { createWorktreeSafetyModule } from "./worktree-safety.js";
|
|
247
247
|
import { resolveManifest } from "./unit-context-manifest.js";
|
|
248
248
|
import { classifyFailure } from "./recovery-classification.js";
|
|
249
|
+
import { supportsStructuredQuestions } from "./workflow-mcp.js";
|
|
250
|
+
import type { MinimalModelRegistry } from "./context-budget.js";
|
|
249
251
|
// Slice-level parallelism (#2340)
|
|
250
252
|
import { getEligibleSlices } from "./slice-parallel-eligibility.js";
|
|
251
253
|
import { startSliceParallel } from "./slice-parallel-orchestrator.js";
|
|
@@ -1048,13 +1050,11 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
|
|
|
1048
1050
|
initHealthWidget(ctx);
|
|
1049
1051
|
}
|
|
1050
1052
|
|
|
1051
|
-
// ADR-016 phase 3 (#5693): the stop-path basePath restore routes
|
|
1052
|
-
// `Lifecycle.restoreToProjectRoot()`, the sole owner of
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
-
//
|
|
1056
|
-
// The chdir stays here because `restoreToProjectRoot` is a pure
|
|
1057
|
-
// session-state mutation.
|
|
1053
|
+
// ADR-016 phase 3 (#5693): the stop-path basePath restore + chdir routes
|
|
1054
|
+
// through `Lifecycle.restoreToProjectRoot()`, the sole owner of both
|
|
1055
|
+
// `s.basePath` mutation and the paired `process.chdir` for auto-loop
|
|
1056
|
+
// transitions. The verb assigns `s.basePath` before any throwable work, so
|
|
1057
|
+
// a thrown error still leaves basePath restored.
|
|
1058
1058
|
if (s.originalBasePath) {
|
|
1059
1059
|
try {
|
|
1060
1060
|
buildLifecycle().restoreToProjectRoot();
|
|
@@ -1065,11 +1065,6 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
|
|
|
1065
1065
|
{ file: "auto.ts" },
|
|
1066
1066
|
);
|
|
1067
1067
|
}
|
|
1068
|
-
try {
|
|
1069
|
-
process.chdir(s.originalBasePath);
|
|
1070
|
-
} catch (err) {
|
|
1071
|
-
logWarning("engine", `basePath restore/chdir failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1072
|
-
}
|
|
1073
1068
|
}
|
|
1074
1069
|
|
|
1075
1070
|
if (s.originalBasePath && s.cmdCtx) {
|
|
@@ -1384,20 +1379,14 @@ export async function stopAuto(
|
|
|
1384
1379
|
}
|
|
1385
1380
|
|
|
1386
1381
|
// ── Step 7: Restore basePath and chdir (ADR-016 phase 3, #5693) ──
|
|
1387
|
-
// `restoreToProjectRoot`
|
|
1388
|
-
// no
|
|
1382
|
+
// `restoreToProjectRoot` owns both s.basePath restore and process.chdir;
|
|
1383
|
+
// no paired chdir is needed at the call site.
|
|
1389
1384
|
if (s.originalBasePath) {
|
|
1390
1385
|
try {
|
|
1391
1386
|
buildLifecycle().restoreToProjectRoot();
|
|
1392
1387
|
} catch (e) {
|
|
1393
1388
|
debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
|
|
1394
1389
|
}
|
|
1395
|
-
try {
|
|
1396
|
-
process.chdir(s.basePath);
|
|
1397
|
-
} catch (err) {
|
|
1398
|
-
/* best-effort */
|
|
1399
|
-
logWarning("engine", `chdir failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1400
|
-
}
|
|
1401
1390
|
}
|
|
1402
1391
|
|
|
1403
1392
|
// Re-root the active command session/tool runtime after worktree teardown.
|
|
@@ -1792,6 +1781,75 @@ function buildLifecycle(): WorktreeLifecycle {
|
|
|
1792
1781
|
return new WorktreeLifecycle(s, buildWorktreeLifecycleDeps());
|
|
1793
1782
|
}
|
|
1794
1783
|
|
|
1784
|
+
/**
|
|
1785
|
+
* Build the production `DispatchAdapter` used by `createWiredAutoOrchestrationModule`.
|
|
1786
|
+
*
|
|
1787
|
+
* Exported so tests can verify parity with `runDispatch`'s `resolveDispatch` call —
|
|
1788
|
+
* the wired adapter must derive `structuredQuestionsAvailable`, `sessionContextWindow`,
|
|
1789
|
+
* `sessionProvider`, and `modelRegistry` the same way phases.ts:runDispatch does.
|
|
1790
|
+
*/
|
|
1791
|
+
export function createWiredDispatchAdapter(
|
|
1792
|
+
ctx: ExtensionContext,
|
|
1793
|
+
pi: ExtensionAPI,
|
|
1794
|
+
dispatchBasePath: string,
|
|
1795
|
+
): DispatchAdapter {
|
|
1796
|
+
return {
|
|
1797
|
+
async decideNextUnit(input) {
|
|
1798
|
+
const state = input.stateSnapshot;
|
|
1799
|
+
const active = state.activeMilestone;
|
|
1800
|
+
if (!active) return null;
|
|
1801
|
+
|
|
1802
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1803
|
+
|
|
1804
|
+
// Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
|
|
1805
|
+
// (#5789). Prefer caller-supplied values when present so test harnesses and
|
|
1806
|
+
// alternative wirings can inject deterministic snapshots; otherwise pull from
|
|
1807
|
+
// the captured pi/ctx references.
|
|
1808
|
+
const sessionProvider = input.sessionProvider ?? ctx.model?.provider;
|
|
1809
|
+
const sessionContextWindow = input.sessionContextWindow ?? ctx.model?.contextWindow;
|
|
1810
|
+
const modelRegistry = input.modelRegistry ?? (ctx.modelRegistry as MinimalModelRegistry | undefined);
|
|
1811
|
+
const authMode =
|
|
1812
|
+
sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
|
|
1813
|
+
? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
|
|
1814
|
+
: undefined;
|
|
1815
|
+
const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
|
|
1816
|
+
// Mirrors runDispatch: deep-planning keeps approval gates in plain chat
|
|
1817
|
+
// because structured questions can be cancelled outside the chat turn on
|
|
1818
|
+
// some transports.
|
|
1819
|
+
const structuredQuestionsAvailable =
|
|
1820
|
+
input.structuredQuestionsAvailable ??
|
|
1821
|
+
(prefs?.planning_depth === "deep"
|
|
1822
|
+
? "false"
|
|
1823
|
+
: supportsStructuredQuestions(activeTools, {
|
|
1824
|
+
authMode,
|
|
1825
|
+
baseUrl: ctx.model?.baseUrl,
|
|
1826
|
+
})
|
|
1827
|
+
? "true"
|
|
1828
|
+
: "false");
|
|
1829
|
+
|
|
1830
|
+
const action = await resolveDispatch({
|
|
1831
|
+
basePath: dispatchBasePath,
|
|
1832
|
+
mid: active.id,
|
|
1833
|
+
midTitle: active.title,
|
|
1834
|
+
state,
|
|
1835
|
+
prefs,
|
|
1836
|
+
structuredQuestionsAvailable,
|
|
1837
|
+
sessionContextWindow,
|
|
1838
|
+
sessionProvider,
|
|
1839
|
+
modelRegistry,
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
if (action.action !== "dispatch") return null;
|
|
1843
|
+
return {
|
|
1844
|
+
unitType: action.unitType,
|
|
1845
|
+
unitId: action.unitId,
|
|
1846
|
+
reason: action.matchedRule ?? "dispatch",
|
|
1847
|
+
preconditions: [],
|
|
1848
|
+
};
|
|
1849
|
+
},
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1795
1853
|
/**
|
|
1796
1854
|
* Thin entry glue for the new Auto Orchestration module.
|
|
1797
1855
|
*
|
|
@@ -1801,7 +1859,7 @@ function buildLifecycle(): WorktreeLifecycle {
|
|
|
1801
1859
|
*/
|
|
1802
1860
|
export function createWiredAutoOrchestrationModule(
|
|
1803
1861
|
ctx: ExtensionContext,
|
|
1804
|
-
|
|
1862
|
+
pi: ExtensionAPI,
|
|
1805
1863
|
dispatchBasePath: string,
|
|
1806
1864
|
runtimeBasePath = resolveProjectRoot(dispatchBasePath),
|
|
1807
1865
|
): AutoOrchestrationModule {
|
|
@@ -1830,30 +1888,7 @@ export function createWiredAutoOrchestrationModule(
|
|
|
1830
1888
|
};
|
|
1831
1889
|
},
|
|
1832
1890
|
},
|
|
1833
|
-
dispatch:
|
|
1834
|
-
async decideNextUnit(input) {
|
|
1835
|
-
const state = input.stateSnapshot;
|
|
1836
|
-
const active = state.activeMilestone;
|
|
1837
|
-
if (!active) return null;
|
|
1838
|
-
|
|
1839
|
-
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1840
|
-
const action = await resolveDispatch({
|
|
1841
|
-
basePath: dispatchBasePath,
|
|
1842
|
-
mid: active.id,
|
|
1843
|
-
midTitle: active.title,
|
|
1844
|
-
state,
|
|
1845
|
-
prefs,
|
|
1846
|
-
});
|
|
1847
|
-
|
|
1848
|
-
if (action.action !== "dispatch") return null;
|
|
1849
|
-
return {
|
|
1850
|
-
unitType: action.unitType,
|
|
1851
|
-
unitId: action.unitId,
|
|
1852
|
-
reason: action.matchedRule ?? "dispatch",
|
|
1853
|
-
preconditions: [],
|
|
1854
|
-
};
|
|
1855
|
-
},
|
|
1856
|
-
},
|
|
1891
|
+
dispatch: createWiredDispatchAdapter(ctx, pi, dispatchBasePath),
|
|
1857
1892
|
recovery: {
|
|
1858
1893
|
async classifyAndRecover(input) {
|
|
1859
1894
|
const recovery = classifyFailure(input);
|
|
@@ -1902,12 +1937,25 @@ export function createWiredAutoOrchestrationModule(
|
|
|
1902
1937
|
async cleanupOnStop() {},
|
|
1903
1938
|
},
|
|
1904
1939
|
health: {
|
|
1940
|
+
checkResourcesStale() {
|
|
1941
|
+
return checkResourcesStale(s.resourceVersionOnStart);
|
|
1942
|
+
},
|
|
1905
1943
|
async preAdvanceGate() {
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1944
|
+
try {
|
|
1945
|
+
const gate = await preDispatchHealthGate(dispatchBasePath);
|
|
1946
|
+
if (gate.proceed) {
|
|
1947
|
+
return {
|
|
1948
|
+
kind: "pass",
|
|
1949
|
+
fixesApplied: gate.fixesApplied,
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
return {
|
|
1953
|
+
kind: "fail",
|
|
1954
|
+
reason: gate.reason ?? "Pre-dispatch health check failed — run /gsd doctor for details.",
|
|
1955
|
+
};
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
return { kind: "threw", error };
|
|
1958
|
+
}
|
|
1911
1959
|
},
|
|
1912
1960
|
async postAdvanceRecord(result) {
|
|
1913
1961
|
if (result.kind === "error") {
|
|
@@ -1975,6 +2023,43 @@ export function createWiredAutoOrchestrationModule(
|
|
|
1975
2023
|
}
|
|
1976
2024
|
},
|
|
1977
2025
|
},
|
|
2026
|
+
uokGate: {
|
|
2027
|
+
async emit(input) {
|
|
2028
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
2029
|
+
const uokFlags = resolveUokFlags(prefs);
|
|
2030
|
+
if (!uokFlags.gates) return;
|
|
2031
|
+
const milestoneId = input.milestoneId ?? s.currentMilestoneId ?? undefined;
|
|
2032
|
+
try {
|
|
2033
|
+
const { UokGateRunner } = await import("./uok/gate-runner.js");
|
|
2034
|
+
const runner = new UokGateRunner();
|
|
2035
|
+
runner.register({
|
|
2036
|
+
id: input.gateId,
|
|
2037
|
+
type: input.gateType,
|
|
2038
|
+
execute: async () => ({
|
|
2039
|
+
outcome: input.outcome,
|
|
2040
|
+
failureClass: input.failureClass,
|
|
2041
|
+
rationale: input.rationale,
|
|
2042
|
+
findings: input.findings ?? "",
|
|
2043
|
+
}),
|
|
2044
|
+
});
|
|
2045
|
+
await runner.run(input.gateId, {
|
|
2046
|
+
basePath: dispatchBasePath,
|
|
2047
|
+
traceId: `pre-dispatch:${flowId}`,
|
|
2048
|
+
turnId: `orch-${seq}`,
|
|
2049
|
+
milestoneId,
|
|
2050
|
+
unitType: "pre-dispatch",
|
|
2051
|
+
unitId: `orch-${seq}`,
|
|
2052
|
+
});
|
|
2053
|
+
} catch (err) {
|
|
2054
|
+
logWarning("engine", `uok gate emit failed: ${getErrorMessage(err)}`, {
|
|
2055
|
+
file: "auto.ts",
|
|
2056
|
+
gateId: input.gateId,
|
|
2057
|
+
gateType: input.gateType,
|
|
2058
|
+
...(milestoneId ? { milestoneId } : {}),
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
},
|
|
2062
|
+
},
|
|
1978
2063
|
};
|
|
1979
2064
|
|
|
1980
2065
|
return createAutoOrchestrator(deps);
|
|
@@ -306,7 +306,7 @@ function importRequirements(gsdDir: string): number {
|
|
|
306
306
|
// ─── Hierarchy Artifact Walker ─────────────────────────────────────────────
|
|
307
307
|
|
|
308
308
|
/** Artifact suffixes to look for at each hierarchy level */
|
|
309
|
-
const MILESTONE_SUFFIXES = ['ROADMAP', 'CONTEXT', 'RESEARCH', 'ASSESSMENT'];
|
|
309
|
+
const MILESTONE_SUFFIXES = ['ROADMAP', 'CONTEXT', 'RESEARCH', 'ASSESSMENT', 'SUMMARY', 'VALIDATION'];
|
|
310
310
|
const SLICE_SUFFIXES = ['PLAN', 'SUMMARY', 'RESEARCH', 'CONTEXT', 'ASSESSMENT', 'UAT'];
|
|
311
311
|
const TASK_SUFFIXES = ['PLAN', 'SUMMARY', 'CONTINUE', 'CONTEXT', 'RESEARCH'];
|
|
312
312
|
|
|
@@ -34,6 +34,9 @@ export type MigrationImportCounts = ReturnType<typeof migrateFromMarkdown>;
|
|
|
34
34
|
|
|
35
35
|
function assertMigrationImportMatchesPreview(imported: MigrationImportCounts, preview: MigrationPreview): void {
|
|
36
36
|
const mismatches: string[] = [];
|
|
37
|
+
if (imported.decisions !== preview.decisions.total) {
|
|
38
|
+
mismatches.push(`decisions ${imported.decisions}/${preview.decisions.total}`);
|
|
39
|
+
}
|
|
37
40
|
if (imported.hierarchy.milestones !== preview.milestoneCount) {
|
|
38
41
|
mismatches.push(`milestones ${imported.hierarchy.milestones}/${preview.milestoneCount}`);
|
|
39
42
|
}
|
|
@@ -73,6 +76,7 @@ export async function importWrittenMigrationToDb(
|
|
|
73
76
|
/** Format preview stats for embedding in the review prompt. */
|
|
74
77
|
function formatPreviewStats(preview: MigrationPreview): string {
|
|
75
78
|
const lines = [
|
|
79
|
+
`- Decisions: ${preview.decisions.total}`,
|
|
76
80
|
`- Milestones: ${preview.milestoneCount}`,
|
|
77
81
|
`- Slices: ${preview.totalSlices} (${preview.doneSlices} done — ${preview.sliceCompletionPct}%)`,
|
|
78
82
|
`- Tasks: ${preview.totalTasks} (${preview.doneTasks} done — ${preview.taskCompletionPct}%)`,
|
|
@@ -179,6 +183,7 @@ export async function handleMigrate(
|
|
|
179
183
|
|
|
180
184
|
// ── Build preview text ─────────────────────────────────────────────────────
|
|
181
185
|
const lines: string[] = [
|
|
186
|
+
`Decisions: ${preview.decisions.total}`,
|
|
182
187
|
`Milestones: ${preview.milestoneCount}`,
|
|
183
188
|
`Slices: ${preview.totalSlices} (${preview.doneSlices} done — ${preview.sliceCompletionPct}%)`,
|
|
184
189
|
`Tasks: ${preview.totalTasks} (${preview.doneTasks} done — ${preview.taskCompletionPct}%)`,
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
import type { GSDProject } from './types.js';
|
|
5
5
|
import type { MigrationPreview } from './writer.js';
|
|
6
6
|
|
|
7
|
+
function countCanonicalDecisionRows(content: string): number {
|
|
8
|
+
return content
|
|
9
|
+
.split('\n')
|
|
10
|
+
.filter((line) => /^\|\s*D\d+\s*\|/.test(line.trim()))
|
|
11
|
+
.length;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Compute pre-write statistics from a GSDProject without performing I/O.
|
|
9
16
|
* Used to show the user what a migration will produce before writing anything.
|
|
@@ -36,6 +43,9 @@ export function generatePreview(project: GSDProject): MigrationPreview {
|
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
return {
|
|
46
|
+
decisions: {
|
|
47
|
+
total: countCanonicalDecisionRows(project.decisionsContent),
|
|
48
|
+
},
|
|
39
49
|
milestoneCount: project.milestones.length,
|
|
40
50
|
totalSlices,
|
|
41
51
|
totalTasks,
|
|
@@ -238,16 +238,53 @@ function normalizeStatus(status: string): 'active' | 'validated' | 'deferred' {
|
|
|
238
238
|
return 'active';
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
function normalizeRequirementId(id: string): string | null {
|
|
242
|
+
const match = id.trim().match(/^R(\d+)$/i);
|
|
243
|
+
if (!match) return null;
|
|
244
|
+
return `R${match[1].padStart(3, '0')}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
241
247
|
function mapRequirements(reqs: PlanningRequirement[]): GSDRequirement[] {
|
|
242
248
|
let autoId = 0;
|
|
249
|
+
const reservedIds = new Set(
|
|
250
|
+
reqs
|
|
251
|
+
.map((req) => normalizeRequirementId(req.id))
|
|
252
|
+
.filter((id): id is string => id !== null),
|
|
253
|
+
);
|
|
254
|
+
const usedIds = new Set<string>();
|
|
255
|
+
|
|
256
|
+
function nextRequirementId(): string {
|
|
257
|
+
let id = '';
|
|
258
|
+
do {
|
|
259
|
+
autoId++;
|
|
260
|
+
id = padId('R', autoId, 3);
|
|
261
|
+
} while (usedIds.has(id) || reservedIds.has(id));
|
|
262
|
+
usedIds.add(id);
|
|
263
|
+
return id;
|
|
264
|
+
}
|
|
265
|
+
|
|
243
266
|
return reqs.map((req) => {
|
|
244
|
-
|
|
267
|
+
const originalId = req.id.trim();
|
|
268
|
+
const canonicalId = normalizeRequirementId(originalId);
|
|
269
|
+
let id: string;
|
|
270
|
+
let description = req.description;
|
|
271
|
+
|
|
272
|
+
if (canonicalId && !usedIds.has(canonicalId)) {
|
|
273
|
+
id = canonicalId;
|
|
274
|
+
usedIds.add(id);
|
|
275
|
+
} else {
|
|
276
|
+
id = nextRequirementId();
|
|
277
|
+
if (originalId) {
|
|
278
|
+
description = `Legacy ID: ${originalId}\n\n${description}`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
245
282
|
return {
|
|
246
|
-
id
|
|
283
|
+
id,
|
|
247
284
|
title: req.title,
|
|
248
285
|
class: 'core-capability',
|
|
249
286
|
status: normalizeStatus(req.status),
|
|
250
|
-
description
|
|
287
|
+
description,
|
|
251
288
|
source: 'inferred',
|
|
252
289
|
primarySlice: 'none yet',
|
|
253
290
|
};
|
|
@@ -286,7 +323,24 @@ function deriveDecisions(parsed: PlanningProject): string {
|
|
|
286
323
|
}
|
|
287
324
|
}
|
|
288
325
|
if (decisions.length === 0) return '';
|
|
289
|
-
|
|
326
|
+
const lines = [
|
|
327
|
+
'# Decisions Register',
|
|
328
|
+
'',
|
|
329
|
+
'<!-- Append-only. Never edit or remove existing rows.',
|
|
330
|
+
' To reverse a decision, add a new row that supersedes it.',
|
|
331
|
+
' Read this file at the start of any planning or research phase. -->',
|
|
332
|
+
'',
|
|
333
|
+
'| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |',
|
|
334
|
+
'|---|------|-------|----------|--------|-----------|------------|---------|',
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
decisions.forEach((decision, index) => {
|
|
338
|
+
const id = padId('D', index + 1, 3);
|
|
339
|
+
const escaped = decision.replace(/\|/g, '\\|');
|
|
340
|
+
lines.push(`| ${id} | migration | migrated-summary | ${escaped} | ${escaped} | Migrated from legacy summary key-decisions | Yes | agent |`);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return lines.join('\n') + '\n';
|
|
290
344
|
}
|
|
291
345
|
|
|
292
346
|
// ─── Main Entry Point ──────────────────────────────────────────────────────
|
|
@@ -37,6 +37,9 @@ export interface WrittenFiles {
|
|
|
37
37
|
|
|
38
38
|
/** Pre-write statistics computed from a GSDProject without I/O. */
|
|
39
39
|
export interface MigrationPreview {
|
|
40
|
+
decisions: {
|
|
41
|
+
total: number;
|
|
42
|
+
};
|
|
40
43
|
milestoneCount: number;
|
|
41
44
|
totalSlices: number;
|
|
42
45
|
totalTasks: number;
|
|
@@ -374,7 +377,17 @@ export function formatProject(content: string): string {
|
|
|
374
377
|
*/
|
|
375
378
|
export function formatDecisions(content: string): string {
|
|
376
379
|
if (!content || !content.trim()) {
|
|
377
|
-
return
|
|
380
|
+
return [
|
|
381
|
+
'# Decisions Register',
|
|
382
|
+
'',
|
|
383
|
+
'<!-- Append-only. Never edit or remove existing rows.',
|
|
384
|
+
' To reverse a decision, add a new row that supersedes it.',
|
|
385
|
+
' Read this file at the start of any planning or research phase. -->',
|
|
386
|
+
'',
|
|
387
|
+
'| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |',
|
|
388
|
+
'|---|------|-------|----------|--------|-----------|------------|---------|',
|
|
389
|
+
'',
|
|
390
|
+
].join('\n');
|
|
378
391
|
}
|
|
379
392
|
return content.endsWith('\n') ? content : content + '\n';
|
|
380
393
|
}
|