gsd-pi 2.82.0-dev.725028083 → 2.82.0-dev.ed17d078d
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 +121 -30
- 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/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
- 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 +17 -17
- 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/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/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 +129 -31
- 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-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/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → YEvjuT-fsFfYQhDSWtueS}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → YEvjuT-fsFfYQhDSWtueS}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
497e8db0cb78af90
|
|
@@ -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";
|
|
@@ -1385,6 +1386,66 @@ export function buildWorktreeLifecycleDeps() {
|
|
|
1385
1386
|
function buildLifecycle() {
|
|
1386
1387
|
return new WorktreeLifecycle(s, buildWorktreeLifecycleDeps());
|
|
1387
1388
|
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Build the production `DispatchAdapter` used by `createWiredAutoOrchestrationModule`.
|
|
1391
|
+
*
|
|
1392
|
+
* Exported so tests can verify parity with `runDispatch`'s `resolveDispatch` call —
|
|
1393
|
+
* the wired adapter must derive `structuredQuestionsAvailable`, `sessionContextWindow`,
|
|
1394
|
+
* `sessionProvider`, and `modelRegistry` the same way phases.ts:runDispatch does.
|
|
1395
|
+
*/
|
|
1396
|
+
export function createWiredDispatchAdapter(ctx, pi, dispatchBasePath) {
|
|
1397
|
+
return {
|
|
1398
|
+
async decideNextUnit(input) {
|
|
1399
|
+
const state = input.stateSnapshot;
|
|
1400
|
+
const active = state.activeMilestone;
|
|
1401
|
+
if (!active)
|
|
1402
|
+
return null;
|
|
1403
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1404
|
+
// Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
|
|
1405
|
+
// (#5789). Prefer caller-supplied values when present so test harnesses and
|
|
1406
|
+
// alternative wirings can inject deterministic snapshots; otherwise pull from
|
|
1407
|
+
// the captured pi/ctx references.
|
|
1408
|
+
const sessionProvider = input.sessionProvider ?? ctx.model?.provider;
|
|
1409
|
+
const sessionContextWindow = input.sessionContextWindow ?? ctx.model?.contextWindow;
|
|
1410
|
+
const modelRegistry = input.modelRegistry ?? ctx.modelRegistry;
|
|
1411
|
+
const authMode = sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
|
|
1412
|
+
? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
|
|
1413
|
+
: undefined;
|
|
1414
|
+
const activeTools = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
|
|
1415
|
+
// Mirrors runDispatch: deep-planning keeps approval gates in plain chat
|
|
1416
|
+
// because structured questions can be cancelled outside the chat turn on
|
|
1417
|
+
// some transports.
|
|
1418
|
+
const structuredQuestionsAvailable = input.structuredQuestionsAvailable ??
|
|
1419
|
+
(prefs?.planning_depth === "deep"
|
|
1420
|
+
? "false"
|
|
1421
|
+
: supportsStructuredQuestions(activeTools, {
|
|
1422
|
+
authMode,
|
|
1423
|
+
baseUrl: ctx.model?.baseUrl,
|
|
1424
|
+
})
|
|
1425
|
+
? "true"
|
|
1426
|
+
: "false");
|
|
1427
|
+
const action = await resolveDispatch({
|
|
1428
|
+
basePath: dispatchBasePath,
|
|
1429
|
+
mid: active.id,
|
|
1430
|
+
midTitle: active.title,
|
|
1431
|
+
state,
|
|
1432
|
+
prefs,
|
|
1433
|
+
structuredQuestionsAvailable,
|
|
1434
|
+
sessionContextWindow,
|
|
1435
|
+
sessionProvider,
|
|
1436
|
+
modelRegistry,
|
|
1437
|
+
});
|
|
1438
|
+
if (action.action !== "dispatch")
|
|
1439
|
+
return null;
|
|
1440
|
+
return {
|
|
1441
|
+
unitType: action.unitType,
|
|
1442
|
+
unitId: action.unitId,
|
|
1443
|
+
reason: action.matchedRule ?? "dispatch",
|
|
1444
|
+
preconditions: [],
|
|
1445
|
+
};
|
|
1446
|
+
},
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1388
1449
|
/**
|
|
1389
1450
|
* Thin entry glue for the new Auto Orchestration module.
|
|
1390
1451
|
*
|
|
@@ -1392,7 +1453,7 @@ function buildLifecycle() {
|
|
|
1392
1453
|
* no behavior changes to the existing auto loop. It provides a concrete seam
|
|
1393
1454
|
* the next refactor steps can adopt incrementally.
|
|
1394
1455
|
*/
|
|
1395
|
-
export function createWiredAutoOrchestrationModule(ctx,
|
|
1456
|
+
export function createWiredAutoOrchestrationModule(ctx, pi, dispatchBasePath, runtimeBasePath = resolveProjectRoot(dispatchBasePath)) {
|
|
1396
1457
|
const flowId = `auto-orchestrator-${Date.now()}`;
|
|
1397
1458
|
let seq = 0;
|
|
1398
1459
|
const deps = {
|
|
@@ -1416,30 +1477,7 @@ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, r
|
|
|
1416
1477
|
};
|
|
1417
1478
|
},
|
|
1418
1479
|
},
|
|
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
|
-
},
|
|
1480
|
+
dispatch: createWiredDispatchAdapter(ctx, pi, dispatchBasePath),
|
|
1443
1481
|
recovery: {
|
|
1444
1482
|
async classifyAndRecover(input) {
|
|
1445
1483
|
const recovery = classifyFailure(input);
|
|
@@ -1488,12 +1526,26 @@ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, r
|
|
|
1488
1526
|
async cleanupOnStop() { },
|
|
1489
1527
|
},
|
|
1490
1528
|
health: {
|
|
1529
|
+
checkResourcesStale() {
|
|
1530
|
+
return checkResourcesStale(s.resourceVersionOnStart);
|
|
1531
|
+
},
|
|
1491
1532
|
async preAdvanceGate() {
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1533
|
+
try {
|
|
1534
|
+
const gate = await preDispatchHealthGate(dispatchBasePath);
|
|
1535
|
+
if (gate.proceed) {
|
|
1536
|
+
return {
|
|
1537
|
+
kind: "pass",
|
|
1538
|
+
fixesApplied: gate.fixesApplied,
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
return {
|
|
1542
|
+
kind: "fail",
|
|
1543
|
+
reason: gate.reason ?? "Pre-dispatch health check failed — run /gsd doctor for details.",
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
catch (error) {
|
|
1547
|
+
return { kind: "threw", error };
|
|
1548
|
+
}
|
|
1497
1549
|
},
|
|
1498
1550
|
async postAdvanceRecord(result) {
|
|
1499
1551
|
if (result.kind === "error") {
|
|
@@ -1561,6 +1613,45 @@ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, r
|
|
|
1561
1613
|
}
|
|
1562
1614
|
},
|
|
1563
1615
|
},
|
|
1616
|
+
uokGate: {
|
|
1617
|
+
async emit(input) {
|
|
1618
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1619
|
+
const uokFlags = resolveUokFlags(prefs);
|
|
1620
|
+
if (!uokFlags.gates)
|
|
1621
|
+
return;
|
|
1622
|
+
const milestoneId = input.milestoneId ?? s.currentMilestoneId ?? undefined;
|
|
1623
|
+
try {
|
|
1624
|
+
const { UokGateRunner } = await import("./uok/gate-runner.js");
|
|
1625
|
+
const runner = new UokGateRunner();
|
|
1626
|
+
runner.register({
|
|
1627
|
+
id: input.gateId,
|
|
1628
|
+
type: input.gateType,
|
|
1629
|
+
execute: async () => ({
|
|
1630
|
+
outcome: input.outcome,
|
|
1631
|
+
failureClass: input.failureClass,
|
|
1632
|
+
rationale: input.rationale,
|
|
1633
|
+
findings: input.findings ?? "",
|
|
1634
|
+
}),
|
|
1635
|
+
});
|
|
1636
|
+
await runner.run(input.gateId, {
|
|
1637
|
+
basePath: dispatchBasePath,
|
|
1638
|
+
traceId: `pre-dispatch:${flowId}`,
|
|
1639
|
+
turnId: `orch-${seq}`,
|
|
1640
|
+
milestoneId,
|
|
1641
|
+
unitType: "pre-dispatch",
|
|
1642
|
+
unitId: `orch-${seq}`,
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
catch (err) {
|
|
1646
|
+
logWarning("engine", `uok gate emit failed: ${getErrorMessage(err)}`, {
|
|
1647
|
+
file: "auto.ts",
|
|
1648
|
+
gateId: input.gateId,
|
|
1649
|
+
gateType: input.gateType,
|
|
1650
|
+
...(milestoneId ? { milestoneId } : {}),
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
},
|
|
1564
1655
|
};
|
|
1565
1656
|
return createAutoOrchestrator(deps);
|
|
1566
1657
|
}
|
|
@@ -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
|
}
|