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
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
291e7683e17ec7a9
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
function now() {
|
|
4
4
|
return Date.now();
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Size of the dispatch-decision ring buffer used by the Auto Orchestration
|
|
8
|
+
* module's stuck-loop detector. When the same `${unitType}:${unitId}` key
|
|
9
|
+
* fills the window, advance() blocks with `action: "stop"`.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the legacy `STUCK_WINDOW_SIZE` in auto/phases.ts so behaviour is
|
|
12
|
+
* preserved across the eventual cutover (issue #5791).
|
|
13
|
+
*/
|
|
14
|
+
export const STUCK_WINDOW_SIZE = 6;
|
|
6
15
|
export class AutoOrchestrator {
|
|
7
16
|
status = {
|
|
8
17
|
phase: "idle",
|
|
@@ -10,11 +19,13 @@ export class AutoOrchestrator {
|
|
|
10
19
|
};
|
|
11
20
|
deps;
|
|
12
21
|
lastAdvanceKey = null;
|
|
22
|
+
dispatchKeyWindow = [];
|
|
13
23
|
constructor(deps) {
|
|
14
24
|
this.deps = deps;
|
|
15
25
|
}
|
|
16
26
|
async start(_sessionContext) {
|
|
17
27
|
this.lastAdvanceKey = null;
|
|
28
|
+
this.dispatchKeyWindow = [];
|
|
18
29
|
this.status.phase = "running";
|
|
19
30
|
this.bumpTransition();
|
|
20
31
|
await this.deps.runtime.journalTransition({ name: "start" });
|
|
@@ -24,18 +35,70 @@ export class AutoOrchestrator {
|
|
|
24
35
|
async advance() {
|
|
25
36
|
try {
|
|
26
37
|
await this.deps.runtime.ensureLockOwnership();
|
|
38
|
+
const staleMsg = this.deps.health.checkResourcesStale();
|
|
39
|
+
if (staleMsg) {
|
|
40
|
+
await this.deps.uokGate.emit({
|
|
41
|
+
gateId: "resource-version-guard",
|
|
42
|
+
gateType: "policy",
|
|
43
|
+
outcome: "fail",
|
|
44
|
+
failureClass: "policy",
|
|
45
|
+
rationale: "resource version guard blocked dispatch",
|
|
46
|
+
findings: staleMsg,
|
|
47
|
+
});
|
|
48
|
+
const blocked = { kind: "blocked", reason: staleMsg, action: "stop" };
|
|
49
|
+
await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
50
|
+
await this.deps.health.postAdvanceRecord(blocked);
|
|
51
|
+
return blocked;
|
|
52
|
+
}
|
|
53
|
+
await this.deps.uokGate.emit({
|
|
54
|
+
gateId: "resource-version-guard",
|
|
55
|
+
gateType: "policy",
|
|
56
|
+
outcome: "pass",
|
|
57
|
+
failureClass: "none",
|
|
58
|
+
rationale: "resource version guard passed",
|
|
59
|
+
});
|
|
27
60
|
const gate = await this.deps.health.preAdvanceGate();
|
|
28
|
-
if (
|
|
29
|
-
|
|
61
|
+
if (gate.kind === "fail") {
|
|
62
|
+
await this.deps.uokGate.emit({
|
|
63
|
+
gateId: "pre-dispatch-health-gate",
|
|
64
|
+
gateType: "execution",
|
|
65
|
+
outcome: "manual-attention",
|
|
66
|
+
failureClass: "manual-attention",
|
|
67
|
+
rationale: "pre-dispatch health gate blocked dispatch",
|
|
68
|
+
findings: gate.reason,
|
|
69
|
+
});
|
|
70
|
+
const blocked = { kind: "blocked", reason: gate.reason, action: "pause" };
|
|
30
71
|
await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
31
72
|
await this.deps.health.postAdvanceRecord(blocked);
|
|
32
73
|
return blocked;
|
|
33
74
|
}
|
|
75
|
+
if (gate.kind === "threw") {
|
|
76
|
+
await this.deps.uokGate.emit({
|
|
77
|
+
gateId: "pre-dispatch-health-gate",
|
|
78
|
+
gateType: "execution",
|
|
79
|
+
outcome: "manual-attention",
|
|
80
|
+
failureClass: "manual-attention",
|
|
81
|
+
rationale: "pre-dispatch health gate threw unexpectedly",
|
|
82
|
+
findings: String(gate.error),
|
|
83
|
+
});
|
|
84
|
+
// intentional fall-through: matches runPreDispatch behaviour
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
await this.deps.uokGate.emit({
|
|
88
|
+
gateId: "pre-dispatch-health-gate",
|
|
89
|
+
gateType: "execution",
|
|
90
|
+
outcome: "pass",
|
|
91
|
+
failureClass: "none",
|
|
92
|
+
rationale: "pre-dispatch health gate passed",
|
|
93
|
+
findings: gate.fixesApplied?.join(", ") ?? "",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
34
96
|
const reconciliation = await this.deps.stateReconciliation.reconcileBeforeDispatch();
|
|
35
97
|
if (!reconciliation.ok || !reconciliation.stateSnapshot) {
|
|
36
98
|
const blocked = {
|
|
37
99
|
kind: "blocked",
|
|
38
|
-
reason: reconciliation.reason,
|
|
100
|
+
reason: reconciliation.reason ?? "state reconciliation produced no snapshot",
|
|
101
|
+
action: "pause",
|
|
39
102
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
40
103
|
};
|
|
41
104
|
await this.deps.runtime.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
@@ -48,14 +111,49 @@ export class AutoOrchestrator {
|
|
|
48
111
|
this.status.phase = "stopped";
|
|
49
112
|
this.status.activeUnit = undefined;
|
|
50
113
|
this.lastAdvanceKey = null;
|
|
114
|
+
this.dispatchKeyWindow = [];
|
|
51
115
|
this.bumpTransition();
|
|
52
116
|
await this.deps.runtime.journalTransition({ name: "advance-stopped", reason: stopped.reason });
|
|
53
117
|
await this.deps.health.postAdvanceRecord(stopped);
|
|
54
118
|
return stopped;
|
|
55
119
|
}
|
|
56
120
|
const nextKey = `${decision.unitType}:${decision.unitId}`;
|
|
57
|
-
|
|
58
|
-
|
|
121
|
+
// Record every dispatch decision in the ring buffer before pre-flight
|
|
122
|
+
// checks so the stuck-loop detector observes the full decision history
|
|
123
|
+
// (including decisions that idempotency would otherwise short-circuit).
|
|
124
|
+
// The ring is capped at STUCK_WINDOW_SIZE and evicts oldest-first.
|
|
125
|
+
this.dispatchKeyWindow.push(nextKey);
|
|
126
|
+
if (this.dispatchKeyWindow.length > STUCK_WINDOW_SIZE) {
|
|
127
|
+
this.dispatchKeyWindow.shift();
|
|
128
|
+
}
|
|
129
|
+
// Idempotency: same key as immediately previous successful advance.
|
|
130
|
+
// This is the soft, fast-path block kept from #5786. It only fires when
|
|
131
|
+
// the ring is NOT yet saturated for this key — once the ring is full of
|
|
132
|
+
// `nextKey`, the stuck-loop verdict takes precedence (see below). Both
|
|
133
|
+
// checks coexist: idempotency for the common immediate-repeat case,
|
|
134
|
+
// stuck-loop for the saturated-window case.
|
|
135
|
+
const matchingCount = this.dispatchKeyWindow.filter((k) => k === nextKey).length;
|
|
136
|
+
if (this.lastAdvanceKey === nextKey && matchingCount < STUCK_WINDOW_SIZE) {
|
|
137
|
+
const blocked = { kind: "blocked", reason: "idempotent advance: unit already active", action: "stop" };
|
|
138
|
+
await this.deps.runtime.journalTransition({
|
|
139
|
+
name: "advance-blocked",
|
|
140
|
+
reason: blocked.reason,
|
|
141
|
+
unitType: decision.unitType,
|
|
142
|
+
unitId: decision.unitId,
|
|
143
|
+
});
|
|
144
|
+
await this.deps.health.postAdvanceRecord(blocked);
|
|
145
|
+
return blocked;
|
|
146
|
+
}
|
|
147
|
+
// Stuck-loop detection: when the ring is saturated with copies of
|
|
148
|
+
// `nextKey` (count >= STUCK_WINDOW_SIZE), the orchestrator has been
|
|
149
|
+
// picking the same unit across the whole window and must hard-stop with
|
|
150
|
+
// a diagnosable reason.
|
|
151
|
+
if (matchingCount >= STUCK_WINDOW_SIZE) {
|
|
152
|
+
const blocked = {
|
|
153
|
+
kind: "blocked",
|
|
154
|
+
reason: `stuck-loop: ${nextKey} picked ${matchingCount} times`,
|
|
155
|
+
action: "stop",
|
|
156
|
+
};
|
|
59
157
|
await this.deps.runtime.journalTransition({
|
|
60
158
|
name: "advance-blocked",
|
|
61
159
|
reason: blocked.reason,
|
|
@@ -70,6 +168,7 @@ export class AutoOrchestrator {
|
|
|
70
168
|
const blocked = {
|
|
71
169
|
kind: "blocked",
|
|
72
170
|
reason: contract.reason,
|
|
171
|
+
action: "pause",
|
|
73
172
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
74
173
|
};
|
|
75
174
|
await this.deps.runtime.journalTransition({
|
|
@@ -86,6 +185,7 @@ export class AutoOrchestrator {
|
|
|
86
185
|
const blocked = {
|
|
87
186
|
kind: "blocked",
|
|
88
187
|
reason: worktree.reason,
|
|
188
|
+
action: "pause",
|
|
89
189
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
90
190
|
};
|
|
91
191
|
await this.deps.runtime.journalTransition({
|
|
@@ -108,7 +208,11 @@ export class AutoOrchestrator {
|
|
|
108
208
|
unitId: decision.unitId,
|
|
109
209
|
});
|
|
110
210
|
await this.deps.worktree.syncAfterUnit(decision.unitType, decision.unitId);
|
|
111
|
-
const advanced = {
|
|
211
|
+
const advanced = {
|
|
212
|
+
kind: "advanced",
|
|
213
|
+
unit: { unitType: decision.unitType, unitId: decision.unitId },
|
|
214
|
+
stateSnapshot: reconciliation.stateSnapshot,
|
|
215
|
+
};
|
|
112
216
|
await this.deps.health.postAdvanceRecord(advanced);
|
|
113
217
|
return advanced;
|
|
114
218
|
}
|
|
@@ -134,6 +238,7 @@ export class AutoOrchestrator {
|
|
|
134
238
|
}
|
|
135
239
|
if (result.kind === "stopped") {
|
|
136
240
|
this.lastAdvanceKey = null;
|
|
241
|
+
this.dispatchKeyWindow = [];
|
|
137
242
|
this.status.activeUnit = undefined;
|
|
138
243
|
}
|
|
139
244
|
this.bumpTransition();
|
|
@@ -158,6 +263,7 @@ export class AutoOrchestrator {
|
|
|
158
263
|
}
|
|
159
264
|
async resume() {
|
|
160
265
|
this.lastAdvanceKey = null;
|
|
266
|
+
this.dispatchKeyWindow = [];
|
|
161
267
|
this.status.phase = "running";
|
|
162
268
|
this.bumpTransition();
|
|
163
269
|
await this.deps.runtime.journalTransition({ name: "resume" });
|
|
@@ -172,6 +278,7 @@ export class AutoOrchestrator {
|
|
|
172
278
|
this.status.phase = "stopped";
|
|
173
279
|
this.status.activeUnit = undefined;
|
|
174
280
|
this.lastAdvanceKey = null;
|
|
281
|
+
this.dispatchKeyWindow = [];
|
|
175
282
|
this.bumpTransition();
|
|
176
283
|
await this.deps.runtime.journalTransition({ name: "stop", reason });
|
|
177
284
|
await this.deps.notifications.notifyLifecycle({ name: "stop", detail: reason });
|
|
@@ -92,6 +92,7 @@ import { compileUnitToolContract } from "./tool-contract.js";
|
|
|
92
92
|
import { createWorktreeSafetyModule } from "./worktree-safety.js";
|
|
93
93
|
import { resolveManifest } from "./unit-context-manifest.js";
|
|
94
94
|
import { classifyFailure } from "./recovery-classification.js";
|
|
95
|
+
import { supportsStructuredQuestions } from "./workflow-mcp.js";
|
|
95
96
|
import { WorktreeLifecycle, } from "./worktree-lifecycle.js";
|
|
96
97
|
import { WorktreeStateProjection } from "./worktree-state-projection.js";
|
|
97
98
|
import { reorderForCaching } from "./prompt-ordering.js";
|
|
@@ -711,13 +712,11 @@ export async function cleanupAfterLoopExit(ctx) {
|
|
|
711
712
|
}
|
|
712
713
|
initHealthWidget(ctx);
|
|
713
714
|
}
|
|
714
|
-
// ADR-016 phase 3 (#5693): the stop-path basePath restore routes
|
|
715
|
-
// `Lifecycle.restoreToProjectRoot()`, the sole owner of
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
// The chdir stays here because `restoreToProjectRoot` is a pure
|
|
720
|
-
// session-state mutation.
|
|
715
|
+
// ADR-016 phase 3 (#5693): the stop-path basePath restore + chdir routes
|
|
716
|
+
// through `Lifecycle.restoreToProjectRoot()`, the sole owner of both
|
|
717
|
+
// `s.basePath` mutation and the paired `process.chdir` for auto-loop
|
|
718
|
+
// transitions. The verb assigns `s.basePath` before any throwable work, so
|
|
719
|
+
// a thrown error still leaves basePath restored.
|
|
721
720
|
if (s.originalBasePath) {
|
|
722
721
|
try {
|
|
723
722
|
buildLifecycle().restoreToProjectRoot();
|
|
@@ -725,12 +724,6 @@ export async function cleanupAfterLoopExit(ctx) {
|
|
|
725
724
|
catch (err) {
|
|
726
725
|
logWarning("engine", `restore project root failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
727
726
|
}
|
|
728
|
-
try {
|
|
729
|
-
process.chdir(s.originalBasePath);
|
|
730
|
-
}
|
|
731
|
-
catch (err) {
|
|
732
|
-
logWarning("engine", `basePath restore/chdir failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
733
|
-
}
|
|
734
727
|
}
|
|
735
728
|
if (s.originalBasePath && s.cmdCtx) {
|
|
736
729
|
const result = await rerootCommandSession(s.cmdCtx, s.originalBasePath);
|
|
@@ -1001,8 +994,8 @@ export async function stopAuto(ctx, pi, reason, options = {}) {
|
|
|
1001
994
|
}
|
|
1002
995
|
}
|
|
1003
996
|
// ── Step 7: Restore basePath and chdir (ADR-016 phase 3, #5693) ──
|
|
1004
|
-
// `restoreToProjectRoot`
|
|
1005
|
-
// no
|
|
997
|
+
// `restoreToProjectRoot` owns both s.basePath restore and process.chdir;
|
|
998
|
+
// no paired chdir is needed at the call site.
|
|
1006
999
|
if (s.originalBasePath) {
|
|
1007
1000
|
try {
|
|
1008
1001
|
buildLifecycle().restoreToProjectRoot();
|
|
@@ -1010,13 +1003,6 @@ export async function stopAuto(ctx, pi, reason, options = {}) {
|
|
|
1010
1003
|
catch (e) {
|
|
1011
1004
|
debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
|
|
1012
1005
|
}
|
|
1013
|
-
try {
|
|
1014
|
-
process.chdir(s.basePath);
|
|
1015
|
-
}
|
|
1016
|
-
catch (err) {
|
|
1017
|
-
/* best-effort */
|
|
1018
|
-
logWarning("engine", `chdir failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1019
|
-
}
|
|
1020
1006
|
}
|
|
1021
1007
|
// Re-root the active command session/tool runtime after worktree teardown.
|
|
1022
1008
|
// mergeAndExit restores process.cwd(), but AgentSession has already captured
|
|
@@ -1385,6 +1371,66 @@ export function buildWorktreeLifecycleDeps() {
|
|
|
1385
1371
|
function buildLifecycle() {
|
|
1386
1372
|
return new WorktreeLifecycle(s, buildWorktreeLifecycleDeps());
|
|
1387
1373
|
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Build the production `DispatchAdapter` used by `createWiredAutoOrchestrationModule`.
|
|
1376
|
+
*
|
|
1377
|
+
* Exported so tests can verify parity with `runDispatch`'s `resolveDispatch` call —
|
|
1378
|
+
* the wired adapter must derive `structuredQuestionsAvailable`, `sessionContextWindow`,
|
|
1379
|
+
* `sessionProvider`, and `modelRegistry` the same way phases.ts:runDispatch does.
|
|
1380
|
+
*/
|
|
1381
|
+
export function createWiredDispatchAdapter(ctx, pi, dispatchBasePath) {
|
|
1382
|
+
return {
|
|
1383
|
+
async decideNextUnit(input) {
|
|
1384
|
+
const state = input.stateSnapshot;
|
|
1385
|
+
const active = state.activeMilestone;
|
|
1386
|
+
if (!active)
|
|
1387
|
+
return null;
|
|
1388
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1389
|
+
// Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
|
|
1390
|
+
// (#5789). Prefer caller-supplied values when present so test harnesses and
|
|
1391
|
+
// alternative wirings can inject deterministic snapshots; otherwise pull from
|
|
1392
|
+
// the captured pi/ctx references.
|
|
1393
|
+
const sessionProvider = input.sessionProvider ?? ctx.model?.provider;
|
|
1394
|
+
const sessionContextWindow = input.sessionContextWindow ?? ctx.model?.contextWindow;
|
|
1395
|
+
const modelRegistry = input.modelRegistry ?? ctx.modelRegistry;
|
|
1396
|
+
const authMode = sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
|
|
1397
|
+
? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
|
|
1398
|
+
: undefined;
|
|
1399
|
+
const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
|
|
1400
|
+
// Mirrors runDispatch: deep-planning keeps approval gates in plain chat
|
|
1401
|
+
// because structured questions can be cancelled outside the chat turn on
|
|
1402
|
+
// some transports.
|
|
1403
|
+
const structuredQuestionsAvailable = input.structuredQuestionsAvailable ??
|
|
1404
|
+
(prefs?.planning_depth === "deep"
|
|
1405
|
+
? "false"
|
|
1406
|
+
: supportsStructuredQuestions(activeTools, {
|
|
1407
|
+
authMode,
|
|
1408
|
+
baseUrl: ctx.model?.baseUrl,
|
|
1409
|
+
})
|
|
1410
|
+
? "true"
|
|
1411
|
+
: "false");
|
|
1412
|
+
const action = await resolveDispatch({
|
|
1413
|
+
basePath: dispatchBasePath,
|
|
1414
|
+
mid: active.id,
|
|
1415
|
+
midTitle: active.title,
|
|
1416
|
+
state,
|
|
1417
|
+
prefs,
|
|
1418
|
+
structuredQuestionsAvailable,
|
|
1419
|
+
sessionContextWindow,
|
|
1420
|
+
sessionProvider,
|
|
1421
|
+
modelRegistry,
|
|
1422
|
+
});
|
|
1423
|
+
if (action.action !== "dispatch")
|
|
1424
|
+
return null;
|
|
1425
|
+
return {
|
|
1426
|
+
unitType: action.unitType,
|
|
1427
|
+
unitId: action.unitId,
|
|
1428
|
+
reason: action.matchedRule ?? "dispatch",
|
|
1429
|
+
preconditions: [],
|
|
1430
|
+
};
|
|
1431
|
+
},
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1388
1434
|
/**
|
|
1389
1435
|
* Thin entry glue for the new Auto Orchestration module.
|
|
1390
1436
|
*
|
|
@@ -1392,7 +1438,7 @@ function buildLifecycle() {
|
|
|
1392
1438
|
* no behavior changes to the existing auto loop. It provides a concrete seam
|
|
1393
1439
|
* the next refactor steps can adopt incrementally.
|
|
1394
1440
|
*/
|
|
1395
|
-
export function createWiredAutoOrchestrationModule(ctx,
|
|
1441
|
+
export function createWiredAutoOrchestrationModule(ctx, pi, dispatchBasePath, runtimeBasePath = resolveProjectRoot(dispatchBasePath)) {
|
|
1396
1442
|
const flowId = `auto-orchestrator-${Date.now()}`;
|
|
1397
1443
|
let seq = 0;
|
|
1398
1444
|
const deps = {
|
|
@@ -1416,30 +1462,7 @@ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, r
|
|
|
1416
1462
|
};
|
|
1417
1463
|
},
|
|
1418
1464
|
},
|
|
1419
|
-
dispatch:
|
|
1420
|
-
async decideNextUnit(input) {
|
|
1421
|
-
const state = input.stateSnapshot;
|
|
1422
|
-
const active = state.activeMilestone;
|
|
1423
|
-
if (!active)
|
|
1424
|
-
return null;
|
|
1425
|
-
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1426
|
-
const action = await resolveDispatch({
|
|
1427
|
-
basePath: dispatchBasePath,
|
|
1428
|
-
mid: active.id,
|
|
1429
|
-
midTitle: active.title,
|
|
1430
|
-
state,
|
|
1431
|
-
prefs,
|
|
1432
|
-
});
|
|
1433
|
-
if (action.action !== "dispatch")
|
|
1434
|
-
return null;
|
|
1435
|
-
return {
|
|
1436
|
-
unitType: action.unitType,
|
|
1437
|
-
unitId: action.unitId,
|
|
1438
|
-
reason: action.matchedRule ?? "dispatch",
|
|
1439
|
-
preconditions: [],
|
|
1440
|
-
};
|
|
1441
|
-
},
|
|
1442
|
-
},
|
|
1465
|
+
dispatch: createWiredDispatchAdapter(ctx, pi, dispatchBasePath),
|
|
1443
1466
|
recovery: {
|
|
1444
1467
|
async classifyAndRecover(input) {
|
|
1445
1468
|
const recovery = classifyFailure(input);
|
|
@@ -1488,12 +1511,26 @@ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, r
|
|
|
1488
1511
|
async cleanupOnStop() { },
|
|
1489
1512
|
},
|
|
1490
1513
|
health: {
|
|
1514
|
+
checkResourcesStale() {
|
|
1515
|
+
return checkResourcesStale(s.resourceVersionOnStart);
|
|
1516
|
+
},
|
|
1491
1517
|
async preAdvanceGate() {
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1518
|
+
try {
|
|
1519
|
+
const gate = await preDispatchHealthGate(dispatchBasePath);
|
|
1520
|
+
if (gate.proceed) {
|
|
1521
|
+
return {
|
|
1522
|
+
kind: "pass",
|
|
1523
|
+
fixesApplied: gate.fixesApplied,
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
return {
|
|
1527
|
+
kind: "fail",
|
|
1528
|
+
reason: gate.reason ?? "Pre-dispatch health check failed — run /gsd doctor for details.",
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
catch (error) {
|
|
1532
|
+
return { kind: "threw", error };
|
|
1533
|
+
}
|
|
1497
1534
|
},
|
|
1498
1535
|
async postAdvanceRecord(result) {
|
|
1499
1536
|
if (result.kind === "error") {
|
|
@@ -1561,6 +1598,45 @@ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, r
|
|
|
1561
1598
|
}
|
|
1562
1599
|
},
|
|
1563
1600
|
},
|
|
1601
|
+
uokGate: {
|
|
1602
|
+
async emit(input) {
|
|
1603
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1604
|
+
const uokFlags = resolveUokFlags(prefs);
|
|
1605
|
+
if (!uokFlags.gates)
|
|
1606
|
+
return;
|
|
1607
|
+
const milestoneId = input.milestoneId ?? s.currentMilestoneId ?? undefined;
|
|
1608
|
+
try {
|
|
1609
|
+
const { UokGateRunner } = await import("./uok/gate-runner.js");
|
|
1610
|
+
const runner = new UokGateRunner();
|
|
1611
|
+
runner.register({
|
|
1612
|
+
id: input.gateId,
|
|
1613
|
+
type: input.gateType,
|
|
1614
|
+
execute: async () => ({
|
|
1615
|
+
outcome: input.outcome,
|
|
1616
|
+
failureClass: input.failureClass,
|
|
1617
|
+
rationale: input.rationale,
|
|
1618
|
+
findings: input.findings ?? "",
|
|
1619
|
+
}),
|
|
1620
|
+
});
|
|
1621
|
+
await runner.run(input.gateId, {
|
|
1622
|
+
basePath: dispatchBasePath,
|
|
1623
|
+
traceId: `pre-dispatch:${flowId}`,
|
|
1624
|
+
turnId: `orch-${seq}`,
|
|
1625
|
+
milestoneId,
|
|
1626
|
+
unitType: "pre-dispatch",
|
|
1627
|
+
unitId: `orch-${seq}`,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
catch (err) {
|
|
1631
|
+
logWarning("engine", `uok gate emit failed: ${getErrorMessage(err)}`, {
|
|
1632
|
+
file: "auto.ts",
|
|
1633
|
+
gateId: input.gateId,
|
|
1634
|
+
gateType: input.gateType,
|
|
1635
|
+
...(milestoneId ? { milestoneId } : {}),
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
},
|
|
1639
|
+
},
|
|
1564
1640
|
};
|
|
1565
1641
|
return createAutoOrchestrator(deps);
|
|
1566
1642
|
}
|
|
@@ -256,7 +256,7 @@ function importRequirements(gsdDir) {
|
|
|
256
256
|
}
|
|
257
257
|
// ─── Hierarchy Artifact Walker ─────────────────────────────────────────────
|
|
258
258
|
/** Artifact suffixes to look for at each hierarchy level */
|
|
259
|
-
const MILESTONE_SUFFIXES = ['ROADMAP', 'CONTEXT', 'RESEARCH', 'ASSESSMENT'];
|
|
259
|
+
const MILESTONE_SUFFIXES = ['ROADMAP', 'CONTEXT', 'RESEARCH', 'ASSESSMENT', 'SUMMARY', 'VALIDATION'];
|
|
260
260
|
const SLICE_SUFFIXES = ['PLAN', 'SUMMARY', 'RESEARCH', 'CONTEXT', 'ASSESSMENT', 'UAT'];
|
|
261
261
|
const TASK_SUFFIXES = ['PLAN', 'SUMMARY', 'CONTINUE', 'CONTEXT', 'RESEARCH'];
|
|
262
262
|
/**
|
|
@@ -21,6 +21,9 @@ import { migrateFromMarkdown } from "../md-importer.js";
|
|
|
21
21
|
import { invalidateStateCache } from "../state.js";
|
|
22
22
|
function assertMigrationImportMatchesPreview(imported, preview) {
|
|
23
23
|
const mismatches = [];
|
|
24
|
+
if (imported.decisions !== preview.decisions.total) {
|
|
25
|
+
mismatches.push(`decisions ${imported.decisions}/${preview.decisions.total}`);
|
|
26
|
+
}
|
|
24
27
|
if (imported.hierarchy.milestones !== preview.milestoneCount) {
|
|
25
28
|
mismatches.push(`milestones ${imported.hierarchy.milestones}/${preview.milestoneCount}`);
|
|
26
29
|
}
|
|
@@ -55,6 +58,7 @@ export async function importWrittenMigrationToDb(basePath, preview) {
|
|
|
55
58
|
/** Format preview stats for embedding in the review prompt. */
|
|
56
59
|
function formatPreviewStats(preview) {
|
|
57
60
|
const lines = [
|
|
61
|
+
`- Decisions: ${preview.decisions.total}`,
|
|
58
62
|
`- Milestones: ${preview.milestoneCount}`,
|
|
59
63
|
`- Slices: ${preview.totalSlices} (${preview.doneSlices} done — ${preview.sliceCompletionPct}%)`,
|
|
60
64
|
`- Tasks: ${preview.totalTasks} (${preview.doneTasks} done — ${preview.taskCompletionPct}%)`,
|
|
@@ -124,6 +128,7 @@ export async function handleMigrate(args, ctx, pi) {
|
|
|
124
128
|
const preview = generatePreview(project);
|
|
125
129
|
// ── Build preview text ─────────────────────────────────────────────────────
|
|
126
130
|
const lines = [
|
|
131
|
+
`Decisions: ${preview.decisions.total}`,
|
|
127
132
|
`Milestones: ${preview.milestoneCount}`,
|
|
128
133
|
`Slices: ${preview.totalSlices} (${preview.doneSlices} done — ${preview.sliceCompletionPct}%)`,
|
|
129
134
|
`Tasks: ${preview.totalTasks} (${preview.doneTasks} done — ${preview.taskCompletionPct}%)`,
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// GSD Migration Preview — Pre-write statistics
|
|
2
2
|
// Pure function, no I/O. Computes counts from a GSDProject.
|
|
3
|
+
function countCanonicalDecisionRows(content) {
|
|
4
|
+
return content
|
|
5
|
+
.split('\n')
|
|
6
|
+
.filter((line) => /^\|\s*D\d+\s*\|/.test(line.trim()))
|
|
7
|
+
.length;
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
10
|
* Compute pre-write statistics from a GSDProject without performing I/O.
|
|
5
11
|
* Used to show the user what a migration will produce before writing anything.
|
|
@@ -35,6 +41,9 @@ export function generatePreview(project) {
|
|
|
35
41
|
reqCounts.total++;
|
|
36
42
|
}
|
|
37
43
|
return {
|
|
44
|
+
decisions: {
|
|
45
|
+
total: countCanonicalDecisionRows(project.decisionsContent),
|
|
46
|
+
},
|
|
38
47
|
milestoneCount: project.milestones.length,
|
|
39
48
|
totalSlices,
|
|
40
49
|
totalTasks,
|
|
@@ -187,16 +187,48 @@ function normalizeStatus(status) {
|
|
|
187
187
|
return 'validated';
|
|
188
188
|
return 'active';
|
|
189
189
|
}
|
|
190
|
+
function normalizeRequirementId(id) {
|
|
191
|
+
const match = id.trim().match(/^R(\d+)$/i);
|
|
192
|
+
if (!match)
|
|
193
|
+
return null;
|
|
194
|
+
return `R${match[1].padStart(3, '0')}`;
|
|
195
|
+
}
|
|
190
196
|
function mapRequirements(reqs) {
|
|
191
197
|
let autoId = 0;
|
|
198
|
+
const reservedIds = new Set(reqs
|
|
199
|
+
.map((req) => normalizeRequirementId(req.id))
|
|
200
|
+
.filter((id) => id !== null));
|
|
201
|
+
const usedIds = new Set();
|
|
202
|
+
function nextRequirementId() {
|
|
203
|
+
let id = '';
|
|
204
|
+
do {
|
|
205
|
+
autoId++;
|
|
206
|
+
id = padId('R', autoId, 3);
|
|
207
|
+
} while (usedIds.has(id) || reservedIds.has(id));
|
|
208
|
+
usedIds.add(id);
|
|
209
|
+
return id;
|
|
210
|
+
}
|
|
192
211
|
return reqs.map((req) => {
|
|
193
|
-
|
|
212
|
+
const originalId = req.id.trim();
|
|
213
|
+
const canonicalId = normalizeRequirementId(originalId);
|
|
214
|
+
let id;
|
|
215
|
+
let description = req.description;
|
|
216
|
+
if (canonicalId && !usedIds.has(canonicalId)) {
|
|
217
|
+
id = canonicalId;
|
|
218
|
+
usedIds.add(id);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
id = nextRequirementId();
|
|
222
|
+
if (originalId) {
|
|
223
|
+
description = `Legacy ID: ${originalId}\n\n${description}`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
194
226
|
return {
|
|
195
|
-
id
|
|
227
|
+
id,
|
|
196
228
|
title: req.title,
|
|
197
229
|
class: 'core-capability',
|
|
198
230
|
status: normalizeStatus(req.status),
|
|
199
|
-
description
|
|
231
|
+
description,
|
|
200
232
|
source: 'inferred',
|
|
201
233
|
primarySlice: 'none yet',
|
|
202
234
|
};
|
|
@@ -233,7 +265,22 @@ function deriveDecisions(parsed) {
|
|
|
233
265
|
}
|
|
234
266
|
if (decisions.length === 0)
|
|
235
267
|
return '';
|
|
236
|
-
|
|
268
|
+
const lines = [
|
|
269
|
+
'# Decisions Register',
|
|
270
|
+
'',
|
|
271
|
+
'<!-- Append-only. Never edit or remove existing rows.',
|
|
272
|
+
' To reverse a decision, add a new row that supersedes it.',
|
|
273
|
+
' Read this file at the start of any planning or research phase. -->',
|
|
274
|
+
'',
|
|
275
|
+
'| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |',
|
|
276
|
+
'|---|------|-------|----------|--------|-----------|------------|---------|',
|
|
277
|
+
];
|
|
278
|
+
decisions.forEach((decision, index) => {
|
|
279
|
+
const id = padId('D', index + 1, 3);
|
|
280
|
+
const escaped = decision.replace(/\|/g, '\\|');
|
|
281
|
+
lines.push(`| ${id} | migration | migrated-summary | ${escaped} | ${escaped} | Migrated from legacy summary key-decisions | Yes | agent |`);
|
|
282
|
+
});
|
|
283
|
+
return lines.join('\n') + '\n';
|
|
237
284
|
}
|
|
238
285
|
// ─── Main Entry Point ──────────────────────────────────────────────────────
|
|
239
286
|
export function transformToGSD(parsed) {
|
|
@@ -296,7 +296,17 @@ export function formatProject(content) {
|
|
|
296
296
|
*/
|
|
297
297
|
export function formatDecisions(content) {
|
|
298
298
|
if (!content || !content.trim()) {
|
|
299
|
-
return
|
|
299
|
+
return [
|
|
300
|
+
'# Decisions Register',
|
|
301
|
+
'',
|
|
302
|
+
'<!-- Append-only. Never edit or remove existing rows.',
|
|
303
|
+
' To reverse a decision, add a new row that supersedes it.',
|
|
304
|
+
' Read this file at the start of any planning or research phase. -->',
|
|
305
|
+
'',
|
|
306
|
+
'| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |',
|
|
307
|
+
'|---|------|-------|----------|--------|-----------|------------|---------|',
|
|
308
|
+
'',
|
|
309
|
+
].join('\n');
|
|
300
310
|
}
|
|
301
311
|
return content.endsWith('\n') ? content : content + '\n';
|
|
302
312
|
}
|