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.
Files changed (87) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/orchestrator.js +113 -6
  3. package/dist/resources/extensions/gsd/auto.js +128 -52
  4. package/dist/resources/extensions/gsd/md-importer.js +1 -1
  5. package/dist/resources/extensions/gsd/migrate/command.js +5 -0
  6. package/dist/resources/extensions/gsd/migrate/preview.js +9 -0
  7. package/dist/resources/extensions/gsd/migrate/transformer.js +51 -4
  8. package/dist/resources/extensions/gsd/migrate/writer.js +11 -1
  9. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +119 -0
  10. package/dist/resources/extensions/gsd/worktree-lifecycle.js +21 -2
  11. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  12. package/dist/web/standalone/.next/BUILD_ID +1 -1
  13. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  14. package/dist/web/standalone/.next/build-manifest.json +2 -2
  15. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  16. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.html +1 -1
  33. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  40. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  42. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  43. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  44. package/package.json +1 -1
  45. package/packages/contracts/dist/rpc.test.js +7 -0
  46. package/packages/contracts/dist/rpc.test.js.map +1 -1
  47. package/packages/contracts/dist/workflow.d.ts +21 -0
  48. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  49. package/packages/contracts/dist/workflow.js +24 -0
  50. package/packages/contracts/dist/workflow.js.map +1 -1
  51. package/packages/contracts/src/rpc.test.ts +8 -0
  52. package/packages/contracts/src/workflow.ts +24 -0
  53. package/packages/mcp-server/README.md +13 -4
  54. package/packages/mcp-server/dist/workflow-tools.d.ts +0 -3
  55. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  56. package/packages/mcp-server/dist/workflow-tools.js +80 -0
  57. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  58. package/packages/mcp-server/src/workflow-tools.test.ts +22 -0
  59. package/packages/mcp-server/src/workflow-tools.ts +168 -0
  60. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  61. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  62. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  63. package/packages/pi-tui/dist/tui.js +5 -0
  64. package/packages/pi-tui/dist/tui.js.map +1 -1
  65. package/packages/pi-tui/src/tui.ts +6 -0
  66. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  67. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
  68. package/src/resources/extensions/gsd/auto/contracts.ts +46 -11
  69. package/src/resources/extensions/gsd/auto/orchestrator.ts +118 -6
  70. package/src/resources/extensions/gsd/auto.ts +136 -51
  71. package/src/resources/extensions/gsd/md-importer.ts +1 -1
  72. package/src/resources/extensions/gsd/migrate/command.ts +5 -0
  73. package/src/resources/extensions/gsd/migrate/preview.ts +10 -0
  74. package/src/resources/extensions/gsd/migrate/transformer.ts +58 -4
  75. package/src/resources/extensions/gsd/migrate/writer.ts +14 -1
  76. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +408 -4
  77. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +6 -5
  78. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +4 -4
  79. package/src/resources/extensions/gsd/tests/integration/migrate-command.test.ts +48 -3
  80. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +5 -1
  81. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +6 -1
  82. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +6 -0
  83. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +25 -0
  84. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +135 -0
  85. package/src/resources/extensions/gsd/worktree-lifecycle.ts +20 -2
  86. /package/dist/web/standalone/.next/static/{KDRTXR-22LPCsa80X9dey → Wop3A7KRGyR06H3rla_1-}/_buildManifest.js +0 -0
  87. /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 (!gate.allow) {
36
- const blocked: AutoAdvanceResult = { kind: "blocked", reason: gate.reason ?? "health gate blocked" };
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
- if (this.lastAdvanceKey === nextKey) {
68
- const blocked: AutoAdvanceResult = { kind: "blocked", reason: "idempotent advance: unit already active" };
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 = { kind: "advanced", stateSnapshot: reconciliation.stateSnapshot };
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 through
1052
- // `Lifecycle.restoreToProjectRoot()`, the sole owner of `s.basePath`
1053
- // mutation. The verb assigns `s.basePath` before any throwable work
1054
- // (rebuildGitService, cache invalidation), so a thrown error still leaves
1055
- // basePath restored no fallback assignment needed at the call site.
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` assigns s.basePath before any throwable work;
1388
- // no fallback assignment is needed at the call site.
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
- _pi: ExtensionAPI,
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
- const gate = await preDispatchHealthGate(dispatchBasePath);
1907
- return {
1908
- allow: gate.proceed,
1909
- reason: gate.reason,
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
- autoId++;
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: req.id && req.id.trim() !== '' ? req.id : padId('R', autoId, 3),
283
+ id,
247
284
  title: req.title,
248
285
  class: 'core-capability',
249
286
  status: normalizeStatus(req.status),
250
- description: req.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
- return decisions.map((d) => `- ${d}`).join('\n');
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 '# Decisions\n\n<!-- Append-only register of architectural and pattern decisions -->\n\n| ID | Decision | Rationale | Date |\n|----|----------|-----------|------|\n';
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
  }