gsd-pi 2.79.0-dev.5c910bb05 → 2.79.0-dev.9941c9c24

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 (109) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/contracts.js +1 -0
  3. package/dist/resources/extensions/gsd/auto/orchestrator.js +146 -0
  4. package/dist/resources/extensions/gsd/auto/phases.js +61 -7
  5. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  6. package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  8. package/dist/resources/extensions/gsd/auto-recovery.js +63 -55
  9. package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
  10. package/dist/resources/extensions/gsd/auto-start.js +3 -2
  11. package/dist/resources/extensions/gsd/auto.js +159 -2
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -8
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
  15. package/dist/resources/extensions/gsd/gsd-db.js +34 -1
  16. package/dist/resources/extensions/gsd/guided-flow.js +40 -0
  17. package/dist/resources/extensions/gsd/paths.js +5 -1
  18. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +45 -4
  19. package/dist/resources/extensions/gsd/uok/audit.js +23 -9
  20. package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
  21. package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
  22. package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
  23. package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
  24. package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
  25. package/dist/resources/extensions/shared/interview-ui.js +15 -4
  26. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  27. package/dist/web/standalone/.next/BUILD_ID +1 -1
  28. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  29. package/dist/web/standalone/.next/build-manifest.json +2 -2
  30. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.html +1 -1
  48. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  55. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  57. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  58. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  59. package/package.json +1 -1
  60. package/packages/mcp-server/src/workflow-tools.test.ts +13 -2
  61. package/src/resources/extensions/gsd/auto/contracts.ts +87 -0
  62. package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -3
  63. package/src/resources/extensions/gsd/auto/orchestrator.ts +161 -0
  64. package/src/resources/extensions/gsd/auto/phases.ts +88 -9
  65. package/src/resources/extensions/gsd/auto/session.ts +11 -0
  66. package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
  67. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  68. package/src/resources/extensions/gsd/auto-recovery.ts +59 -53
  69. package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
  70. package/src/resources/extensions/gsd/auto-start.ts +3 -2
  71. package/src/resources/extensions/gsd/auto.ts +167 -1
  72. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +14 -1
  73. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +11 -8
  74. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
  75. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
  76. package/src/resources/extensions/gsd/gsd-db.ts +35 -1
  77. package/src/resources/extensions/gsd/guided-flow.ts +47 -0
  78. package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
  79. package/src/resources/extensions/gsd/paths.ts +6 -1
  80. package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +32 -0
  81. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +353 -0
  82. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
  83. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +39 -0
  84. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +3 -0
  85. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
  86. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
  87. package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
  88. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
  89. package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
  90. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
  91. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +79 -0
  92. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +134 -0
  93. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +2 -0
  94. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +27 -0
  95. package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
  96. package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
  97. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +36 -7
  98. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +47 -4
  99. package/src/resources/extensions/gsd/uok/audit.ts +25 -9
  100. package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
  101. package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
  102. package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
  103. package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
  104. package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
  105. package/src/resources/extensions/shared/interview-ui.ts +18 -5
  106. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
  107. package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
  108. /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → zSHLdEn5cpkqivbJZq0Qq}/_buildManifest.js +0 -0
  109. /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → zSHLdEn5cpkqivbJZq0Qq}/_ssgManifest.js +0 -0
@@ -31,7 +31,7 @@ import { selectAndApplyModel, resolveModelId, clearToolBaseline } from "./auto-m
31
31
  import { resetRoutingHistory, recordOutcome } from "./routing-history.js";
32
32
  import { resetHookState, runPreDispatchHooks, restoreHookState, clearPersistedHookState, } from "./post-unit-hooks.js";
33
33
  import { runGSDDoctor, rebuildState } from "./doctor.js";
34
- import { preDispatchHealthGate, resetProactiveHealing, setLevelChangeCallback, } from "./doctor-proactive.js";
34
+ import { preDispatchHealthGate, recordHealthSnapshot, resetProactiveHealing, setLevelChangeCallback, } from "./doctor-proactive.js";
35
35
  import { clearSkillSnapshot } from "./skill-discovery.js";
36
36
  import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.js";
37
37
  import { getRtkSessionSavings } from "../shared/rtk-session-stats.js";
@@ -43,7 +43,7 @@ import { isAbsolute, join } from "node:path";
43
43
  import { pathToFileURL } from "node:url";
44
44
  import { readFileSync, existsSync, mkdirSync } from "node:fs";
45
45
  import { atomicWriteSync } from "./atomic-write.js";
46
- import { autoCommitCurrentBranch, captureIntegrationBranch, detectWorktreeName, getCurrentBranch, getMainBranch, setActiveMilestoneId, } from "./worktree.js";
46
+ import { autoCommitCurrentBranch, captureIntegrationBranch, detectWorktreeName, getCurrentBranch, getMainBranch, setActiveMilestoneId, resolveProjectRoot, } from "./worktree.js";
47
47
  import { GitServiceImpl } from "./git-service.js";
48
48
  import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
49
49
  import { createAutoWorktree, enterAutoWorktree, enterBranchModeForMilestone, teardownAutoWorktree, isInAutoWorktree, getAutoWorktreePath, mergeMilestoneToMain, autoWorktreeBranch, syncWorktreeStateBack, syncProjectRootToWorktree, checkResourcesStale, escapeStaleWorktree, } from "./auto-worktree.js";
@@ -82,6 +82,7 @@ import { resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSess
82
82
  import { runAutoLoopWithUok } from "./uok/kernel.js";
83
83
  import { resolveUokFlags } from "./uok/flags.js";
84
84
  import { validateDirectory } from "./validate-directory.js";
85
+ import { createAutoOrchestrator } from "./auto/orchestrator.js";
85
86
  import { WorktreeResolver, } from "./worktree-resolver.js";
86
87
  import { reorderForCaching } from "./prompt-ordering.js";
87
88
  export { STUB_RECOVERY_THRESHOLD, NEW_SESSION_TIMEOUT_MS, } from "./auto/session.js";
@@ -904,6 +905,12 @@ export async function stopAuto(ctx, pi, reason) {
904
905
  // changes the user made between sessions (#4959 / CodeRabbit).
905
906
  if (pi)
906
907
  clearToolBaseline(pi);
908
+ try {
909
+ await s.orchestration?.stop(reason ?? "stop");
910
+ }
911
+ catch (err) {
912
+ debugLog("stop-orchestration-stop", { error: err instanceof Error ? err.message : String(err) });
913
+ }
907
914
  // Reset all session state in one call
908
915
  s.reset();
909
916
  }
@@ -953,6 +960,7 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
953
960
  activeRunDir: s.activeRunDir,
954
961
  autoStartTime: s.autoStartTime,
955
962
  milestoneLock: s.sessionMilestoneLock ?? undefined,
963
+ pauseReason: _errorContext?.message,
956
964
  };
957
965
  setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, pausedMeta);
958
966
  }
@@ -992,6 +1000,12 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
992
1000
  // Unblock pending unitPromise so autoLoop exits cleanly (#1799)
993
1001
  resolveAgentEnd({ messages: [] });
994
1002
  _resetPendingResolve();
1003
+ try {
1004
+ await s.orchestration?.stop("pause");
1005
+ }
1006
+ catch (err) {
1007
+ debugLog("pause-orchestration-stop", { error: err instanceof Error ? err.message : String(err) });
1008
+ }
995
1009
  s.active = false;
996
1010
  s.paused = true;
997
1011
  deactivateGSD();
@@ -1041,6 +1055,132 @@ function buildResolverDeps() {
1041
1055
  function buildResolver() {
1042
1056
  return new WorktreeResolver(s, buildResolverDeps());
1043
1057
  }
1058
+ /**
1059
+ * Thin entry glue for the new Auto Orchestration module.
1060
+ *
1061
+ * This intentionally wires only dispatch + error notification today, with
1062
+ * no behavior changes to the existing auto loop. It provides a concrete seam
1063
+ * the next refactor steps can adopt incrementally.
1064
+ */
1065
+ export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, runtimeBasePath = resolveProjectRoot(dispatchBasePath)) {
1066
+ const flowId = `auto-orchestrator-${Date.now()}`;
1067
+ let seq = 0;
1068
+ const deps = {
1069
+ dispatch: {
1070
+ async decideNextUnit() {
1071
+ const state = await deriveState(dispatchBasePath);
1072
+ const active = state.activeMilestone;
1073
+ if (!active)
1074
+ return null;
1075
+ const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
1076
+ const action = await resolveDispatch({
1077
+ basePath: dispatchBasePath,
1078
+ mid: active.id,
1079
+ midTitle: active.title,
1080
+ state,
1081
+ prefs,
1082
+ });
1083
+ if (action.action !== "dispatch")
1084
+ return null;
1085
+ return {
1086
+ unitType: action.unitType,
1087
+ unitId: action.unitId,
1088
+ reason: action.matchedRule ?? "dispatch",
1089
+ preconditions: [],
1090
+ };
1091
+ },
1092
+ },
1093
+ recovery: {
1094
+ async classifyAndRecover(input) {
1095
+ const reason = input.error instanceof Error ? input.error.message : String(input.error ?? "unknown auto error");
1096
+ return { action: "escalate", reason };
1097
+ },
1098
+ },
1099
+ worktree: {
1100
+ async prepareForUnit() { },
1101
+ async syncAfterUnit() { },
1102
+ async cleanupOnStop() { },
1103
+ },
1104
+ health: {
1105
+ async preAdvanceGate() {
1106
+ const gate = await preDispatchHealthGate(dispatchBasePath);
1107
+ return {
1108
+ allow: gate.proceed,
1109
+ reason: gate.reason,
1110
+ };
1111
+ },
1112
+ async postAdvanceRecord(result) {
1113
+ if (result.kind === "error") {
1114
+ recordHealthSnapshot(1, 0, 0, [{
1115
+ code: "orchestration-error",
1116
+ message: result.reason ?? "orchestration error",
1117
+ severity: "error",
1118
+ unitId: "orchestration",
1119
+ }], [], "orchestration");
1120
+ }
1121
+ else if (result.kind === "blocked") {
1122
+ recordHealthSnapshot(0, 1, 0, [{
1123
+ code: "orchestration-blocked",
1124
+ message: result.reason ?? "orchestration blocked",
1125
+ severity: "warning",
1126
+ unitId: "orchestration",
1127
+ }], [], "orchestration");
1128
+ }
1129
+ },
1130
+ },
1131
+ runtime: {
1132
+ async ensureLockOwnership() {
1133
+ const status = getSessionLockStatus(runtimeBasePath);
1134
+ if (!status.valid || status.failureReason === "pid-mismatch") {
1135
+ throw new Error("session lock held by another process");
1136
+ }
1137
+ },
1138
+ async journalTransition(event) {
1139
+ const eventType = event.name === "start"
1140
+ ? "iteration-start"
1141
+ : event.name === "resume"
1142
+ ? "iteration-start"
1143
+ : event.name === "advance"
1144
+ ? "dispatch-match"
1145
+ : event.name === "advance-blocked"
1146
+ ? "guard-block"
1147
+ : event.name === "advance-stopped"
1148
+ ? "dispatch-stop"
1149
+ : event.name === "advance-error"
1150
+ ? "iteration-end"
1151
+ : event.name === "advance-paused" || event.name === "advance-retry"
1152
+ ? "guard-block"
1153
+ : event.name === "stop"
1154
+ ? "terminal"
1155
+ : "iteration-end";
1156
+ _emitJournalEvent(runtimeBasePath, {
1157
+ ts: new Date().toISOString(),
1158
+ flowId,
1159
+ seq: ++seq,
1160
+ eventType,
1161
+ data: {
1162
+ source: "auto-orchestrator",
1163
+ name: event.name,
1164
+ reason: event.reason,
1165
+ unitType: event.unitType,
1166
+ unitId: event.unitId,
1167
+ },
1168
+ });
1169
+ },
1170
+ },
1171
+ notifications: {
1172
+ async notifyLifecycle(event) {
1173
+ if (event.name === "error") {
1174
+ ctx.ui.notify(event.detail ?? "auto orchestration error", "error");
1175
+ }
1176
+ },
1177
+ },
1178
+ };
1179
+ return createAutoOrchestrator(deps);
1180
+ }
1181
+ function ensureOrchestrationModule(ctx, pi, basePath) {
1182
+ s.orchestration = createWiredAutoOrchestrationModule(ctx, pi, basePath, lockBase());
1183
+ }
1044
1184
  /**
1045
1185
  * Build the LoopDeps object from auto.ts private scope.
1046
1186
  * This bundles all private functions that autoLoop needs without exporting them.
@@ -1398,6 +1538,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1398
1538
  // s.basePath may have been updated to a worktree path by enterMilestone.
1399
1539
  rebuildScope(s.basePath, s.currentMilestoneId);
1400
1540
  }
1541
+ ensureOrchestrationModule(ctx, pi, s.basePath || base);
1401
1542
  registerSigtermHandler(lockBase());
1402
1543
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1403
1544
  ctx.ui.setWidget("gsd-health", undefined);
@@ -1457,6 +1598,12 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1457
1598
  clearPausedSession("paused-session DB cleanup failed (resume activation)");
1458
1599
  }
1459
1600
  pi.events.emit(CMUX_CHANNELS.LOG, { preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, message: s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", level: "progress" });
1601
+ try {
1602
+ await s.orchestration?.resume();
1603
+ }
1604
+ catch (err) {
1605
+ debugLog("resume-orchestration-resume", { error: err instanceof Error ? err.message : String(err) });
1606
+ }
1460
1607
  startAutoCommandPolling(s.basePath);
1461
1608
  await runAutoLoopWithUok({
1462
1609
  ctx,
@@ -1482,6 +1629,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1482
1629
  // Build scope after bootstrap has populated s.basePath / s.originalBasePath /
1483
1630
  // s.currentMilestoneId (including worktree setup inside bootstrapAutoSession).
1484
1631
  rebuildScope(s.basePath, s.currentMilestoneId);
1632
+ ensureOrchestrationModule(ctx, pi, s.basePath || base);
1485
1633
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1486
1634
  registerAutoWorkerForSession(s);
1487
1635
  try {
@@ -1492,6 +1640,12 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1492
1640
  logWarning("engine", `cmux sync failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1493
1641
  }
1494
1642
  pi.events.emit(CMUX_CHANNELS.LOG, { preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, message: requestedStepMode ? "Step-mode started." : "Auto-mode started.", level: "progress" });
1643
+ try {
1644
+ await s.orchestration?.start({ basePath: s.basePath, trigger: "auto-loop" });
1645
+ }
1646
+ catch (err) {
1647
+ debugLog("start-orchestration-start", { error: err instanceof Error ? err.message : String(err) });
1648
+ }
1495
1649
  startAutoCommandPolling(s.basePath);
1496
1650
  // Dispatch the first unit
1497
1651
  await runAutoLoopWithUok({
@@ -1576,6 +1730,9 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
1576
1730
  s.pendingQuickTasks = [];
1577
1731
  }
1578
1732
  s.basePath = targetBasePath;
1733
+ if (!s.orchestration) {
1734
+ ensureOrchestrationModule(ctx, pi, s.basePath);
1735
+ }
1579
1736
  const hookUnitType = `hook/${hookName}`;
1580
1737
  const hookStartedAt = Date.now();
1581
1738
  s.currentUnit = {
@@ -39,6 +39,14 @@ function resolveAgentEndBasePath() {
39
39
  return undefined;
40
40
  }
41
41
  }
42
+ export function _buildAbortedPauseContext(lastMsg) {
43
+ const hasErrorMessage = Object.prototype.hasOwnProperty.call(lastMsg, "errorMessage") && !!lastMsg.errorMessage;
44
+ return {
45
+ message: hasErrorMessage ? String(lastMsg.errorMessage) : "Operation aborted",
46
+ category: "aborted",
47
+ isTransient: true,
48
+ };
49
+ }
42
50
  async function pauseTransientWithBackoff(cls, pi, ctx, errorDetail, isRateLimit) {
43
51
  retryState.consecutiveTransientCount += 1;
44
52
  const baseRetryAfterMs = "retryAfterMs" in cls ? cls.retryAfterMs : 15_000;
@@ -134,7 +142,7 @@ export async function handleAgentEnd(pi, event, ctx) {
134
142
  }
135
143
  return;
136
144
  }
137
- await pauseAuto(ctx, pi);
145
+ await pauseAuto(ctx, pi, _buildAbortedPauseContext(lastMsg));
138
146
  return;
139
147
  }
140
148
  if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
@@ -166,7 +166,7 @@ export function registerHooks(pi, ecosystemHandlers) {
166
166
  const { getEcosystemReadyPromise } = await import("../ecosystem/loader.js");
167
167
  await getEcosystemReadyPromise();
168
168
  const beforeAgentBasePath = process.cwd();
169
- const pendingApprovalGate = getPendingGate();
169
+ const pendingApprovalGate = getPendingGate(beforeAgentBasePath);
170
170
  if (pendingApprovalGate && isExplicitApprovalResponse(event.prompt, pendingApprovalGate)) {
171
171
  markApprovalGateVerified(pendingApprovalGate, beforeAgentBasePath);
172
172
  const milestoneId = extractDepthVerificationMilestoneId(pendingApprovalGate);
@@ -363,15 +363,15 @@ export function registerHooks(pi, ecosystemHandlers) {
363
363
  // ── Discussion gate enforcement: block tool calls while gate is pending ──
364
364
  // If ask_user_questions was called with a gate ID but hasn't been confirmed,
365
365
  // block all non-read-only tool calls to prevent the model from skipping gates.
366
- if (getPendingGate()) {
366
+ if (getPendingGate(discussionBasePath)) {
367
367
  const milestoneId = await getDiscussionMilestoneIdFor(discussionBasePath);
368
368
  if (isToolCallEventType("bash", event)) {
369
- const bashGuard = shouldBlockPendingGateBash(event.input.command, milestoneId, isQueuePhaseActive());
369
+ const bashGuard = shouldBlockPendingGateBash(event.input.command, milestoneId, isQueuePhaseActive(discussionBasePath), discussionBasePath);
370
370
  if (bashGuard.block)
371
371
  return bashGuard;
372
372
  }
373
373
  else {
374
- const gateGuard = shouldBlockPendingGate(toolName, milestoneId, isQueuePhaseActive());
374
+ const gateGuard = shouldBlockPendingGate(toolName, milestoneId, isQueuePhaseActive(discussionBasePath), discussionBasePath);
375
375
  if (gateGuard.block)
376
376
  return gateGuard;
377
377
  }
@@ -380,7 +380,7 @@ export function registerHooks(pi, ecosystemHandlers) {
380
380
  // When /gsd queue is active, the agent should only create milestones,
381
381
  // not execute work. Block write/edit to non-.gsd/ paths and bash commands
382
382
  // that would modify files.
383
- if (isQueuePhaseActive()) {
383
+ if (isQueuePhaseActive(discussionBasePath)) {
384
384
  let queueInput = "";
385
385
  if (isToolCallEventType("write", event)) {
386
386
  queueInput = event.input.path;
@@ -445,7 +445,7 @@ export function registerHooks(pi, ecosystemHandlers) {
445
445
  }
446
446
  if (!isToolCallEventType("write", event))
447
447
  return;
448
- const result = shouldBlockContextWrite(event.toolName, event.input.path, await getDiscussionMilestoneIdFor(discussionBasePath), isQueuePhaseActive());
448
+ const result = shouldBlockContextWrite(event.toolName, event.input.path, await getDiscussionMilestoneIdFor(discussionBasePath), isQueuePhaseActive(discussionBasePath), discussionBasePath);
449
449
  if (result.block)
450
450
  return result;
451
451
  });
@@ -500,7 +500,7 @@ export function registerHooks(pi, ecosystemHandlers) {
500
500
  return;
501
501
  const basePath = process.cwd();
502
502
  const milestoneId = await getDiscussionMilestoneIdFor(basePath);
503
- const queueActive = isQueuePhaseActive();
503
+ const queueActive = isQueuePhaseActive(basePath);
504
504
  const details = event.details;
505
505
  // ── Discussion gate enforcement: handle gate question responses ──
506
506
  // If the result is cancelled or has no response, the pending gate stays active
@@ -508,7 +508,7 @@ export function registerHooks(pi, ecosystemHandlers) {
508
508
  // If the user responded at all (even "needs adjustment"), clear the pending gate
509
509
  // because the user engaged — the prompt handles the re-ask-after-adjustment flow.
510
510
  const questions = event.input?.questions ?? [];
511
- const currentPendingGate = getPendingGate();
511
+ const currentPendingGate = getPendingGate(basePath);
512
512
  if (currentPendingGate) {
513
513
  if (details?.cancelled || !details?.response) {
514
514
  // Gate stays pending. Direct the agent to the most reliable recovery
@@ -298,8 +298,8 @@ export function getPendingGate(basePath = process.cwd()) {
298
298
  * Returns { block: true, reason } if the tool should be blocked.
299
299
  * ask_user_questions itself is allowed so the model can re-ask the gate.
300
300
  */
301
- export function shouldBlockPendingGate(toolName, milestoneId, queuePhaseActive) {
302
- return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(), toolName, milestoneId, queuePhaseActive);
301
+ export function shouldBlockPendingGate(toolName, milestoneId, queuePhaseActive, basePath = process.cwd()) {
302
+ return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(basePath), toolName, milestoneId, queuePhaseActive);
303
303
  }
304
304
  export function shouldBlockPendingGateInSnapshot(snapshot, toolName, _milestoneId, _queuePhaseActive) {
305
305
  if (!snapshot.pendingGateId)
@@ -322,8 +322,8 @@ export function shouldBlockPendingGateInSnapshot(snapshot, toolName, _milestoneI
322
322
  * Check whether a bash command should be blocked because a discussion gate is pending.
323
323
  * All bash is blocked while waiting for confirmation so the question stays visible.
324
324
  */
325
- export function shouldBlockPendingGateBash(command, milestoneId, queuePhaseActive) {
326
- return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(), command, milestoneId, queuePhaseActive);
325
+ export function shouldBlockPendingGateBash(command, milestoneId, queuePhaseActive, basePath = process.cwd()) {
326
+ return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(basePath), command, milestoneId, queuePhaseActive);
327
327
  }
328
328
  export function shouldBlockPendingGateBashInSnapshot(snapshot, command, _milestoneId, _queuePhaseActive) {
329
329
  if (!snapshot.pendingGateId)
@@ -363,7 +363,7 @@ export function isDepthConfirmationAnswer(selected, options) {
363
363
  // Returning false prevents any free-form string from unlocking the gate.
364
364
  return false;
365
365
  }
366
- export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queuePhaseActive) {
366
+ export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queuePhaseActive, basePath = process.cwd()) {
367
367
  if (toolName !== "write")
368
368
  return { block: false };
369
369
  if (!MILESTONE_CONTEXT_RE.test(inputPath))
@@ -379,7 +379,7 @@ export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queue
379
379
  ].join(" "),
380
380
  };
381
381
  }
382
- if (isMilestoneDepthVerified(targetMilestoneId))
382
+ if (isMilestoneDepthVerified(targetMilestoneId, basePath))
383
383
  return { block: false };
384
384
  return {
385
385
  block: true,
@@ -397,8 +397,8 @@ export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queue
397
397
  * Slice-level CONTEXT artifacts are allowed; milestone-level CONTEXT writes
398
398
  * require the milestone to be depth-verified first.
399
399
  */
400
- export function shouldBlockContextArtifactSave(artifactType, milestoneId, sliceId) {
401
- return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(), artifactType, milestoneId, sliceId);
400
+ export function shouldBlockContextArtifactSave(artifactType, milestoneId, sliceId, basePath = process.cwd()) {
401
+ return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(basePath), artifactType, milestoneId, sliceId);
402
402
  }
403
403
  export function shouldBlockContextArtifactSaveInSnapshot(snapshot, artifactType, milestoneId, sliceId) {
404
404
  if (artifactType !== "CONTEXT")
@@ -669,6 +669,10 @@ function columnExists(db, table, column) {
669
669
  const rows = db.prepare(`PRAGMA table_info(${table})`).all();
670
670
  return rows.some((row) => row["name"] === column);
671
671
  }
672
+ function formatFtsUnavailableError(err) {
673
+ const message = err instanceof Error ? err.message : String(err);
674
+ return message.replace(/\bmoduel\s*:\s*/gi, "module: ");
675
+ }
672
676
  /**
673
677
  * Create the FTS5 virtual table for memories plus the triggers that keep it
674
678
  * in sync with the base table. FTS5 may be unavailable on stripped-down
@@ -704,7 +708,7 @@ export function tryCreateMemoriesFts(db) {
704
708
  return true;
705
709
  }
706
710
  catch (err) {
707
- logWarning("db", `FTS5 unavailable — memory queries will use LIKE fallback: ${err.message}`);
711
+ logWarning("db", `FTS5 unavailable — memory queries will use LIKE fallback: ${formatFtsUnavailableError(err)}`);
708
712
  return false;
709
713
  }
710
714
  }
@@ -1653,6 +1657,35 @@ export function closeDatabase() {
1653
1657
  _lastDbError = null;
1654
1658
  _lastDbPhase = null;
1655
1659
  }
1660
+ /**
1661
+ * Re-open the active database connection from disk.
1662
+ *
1663
+ * Auto-mode can observe artifacts written by a workflow server running in a
1664
+ * different process before its long-lived singleton has re-synchronized. The
1665
+ * recovery path uses this to force the next state derivation to read from the
1666
+ * current on-disk database instead of continuing with a possibly stale handle.
1667
+ */
1668
+ export function refreshOpenDatabaseFromDisk() {
1669
+ if (!currentDb || !currentPath)
1670
+ return false;
1671
+ if (currentPath === ":memory:")
1672
+ return false;
1673
+ const dbPath = currentPath;
1674
+ const identityKey = _currentIdentityKey;
1675
+ try {
1676
+ closeDatabase();
1677
+ const opened = openDatabase(dbPath);
1678
+ if (opened && identityKey && currentDb) {
1679
+ _dbCache.set(identityKey, { dbPath, db: currentDb });
1680
+ _currentIdentityKey = identityKey;
1681
+ }
1682
+ return opened;
1683
+ }
1684
+ catch (e) {
1685
+ logWarning("db", `database refresh failed: ${e.message}`);
1686
+ return false;
1687
+ }
1688
+ }
1656
1689
  /** Run a full VACUUM — call sparingly (e.g. after milestone completion). */
1657
1690
  export function vacuumDatabase() {
1658
1691
  if (!currentDb)
@@ -44,6 +44,7 @@ import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForGuidedUnit
44
44
  import { runPreparation, formatCodebaseBrief, formatPriorContextBrief, } from "./preparation.js";
45
45
  import { verifyExpectedArtifact } from "./auto-recovery.js";
46
46
  import { createWorkspace, scopeMilestone } from "./workspace.js";
47
+ import { getPendingGate, extractDepthVerificationMilestoneId } from "./bootstrap/write-gate.js";
47
48
  // ─── Re-exports (preserve public API for existing importers) ────────────────
48
49
  export { MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId, extractMilestoneSeq, parseMilestoneId, milestoneIdSort, maxMilestoneNum, findMilestoneIds, reserveMilestoneId, claimReservedId, getReservedMilestoneIds, clearReservedMilestoneIds, } from "./milestone-ids.js";
49
50
  export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js";
@@ -295,6 +296,14 @@ export async function checkDeepProjectSetupAfterTurn(_event, ctx, basePath) {
295
296
  return false;
296
297
  }
297
298
  }
299
+ // R2: a depth-verification gate is still pending — the LLM emitted the
300
+ // confirmation question (via ask_user_questions or plain chat) but the user
301
+ // has not approved yet. Returning false keeps the entry in the
302
+ // pendingDeepProjectSetupMap so the next user message can resume.
303
+ const pendingGateId = getPendingGate(entry.basePath);
304
+ if (pendingGateId) {
305
+ return false;
306
+ }
298
307
  return dispatchNextDeepProjectSetupStage(entry);
299
308
  }
300
309
  async function dispatchNextDeepProjectSetupStage(entry) {
@@ -368,6 +377,23 @@ export function checkAutoStartAfterDiscuss() {
368
377
  const roadmapFile = existsSync(roadmapFilePath) ? roadmapFilePath : null;
369
378
  if (!contextFile && !roadmapFile)
370
379
  return false; // neither artifact yet — keep waiting
380
+ // Gate 1a: a depth-verification gate is still pending for THIS milestone — the
381
+ // LLM emitted the confirmation question (via ask_user_questions or plain chat)
382
+ // but the user has not answered yet. Advancing now would skip the gate and
383
+ // race ahead with unverified context.
384
+ const basePathForGate = entry.scope.workspace.projectRoot;
385
+ const pendingGateId = getPendingGate(basePathForGate);
386
+ if (pendingGateId) {
387
+ const pendingMilestoneId = extractDepthVerificationMilestoneId(pendingGateId);
388
+ // Block advancement if the gate is for THIS milestone, OR if it's a
389
+ // project/requirements gate (no milestone id encoded) for the deep setup flow.
390
+ const isProjectGate = pendingGateId === "depth_verification_project_confirm" ||
391
+ pendingGateId === "depth_verification_requirements_confirm" ||
392
+ pendingGateId === "depth_verification_research_decision_confirm";
393
+ if (pendingMilestoneId === milestoneId || isProjectGate) {
394
+ return false;
395
+ }
396
+ }
371
397
  // Gate 1b: Discriminate plan-blocked from discuss-incomplete when the DB row is queued.
372
398
  // If the DB is available and the row is still "queued" but CONTEXT.md already exists on
373
399
  // disk, the discuss phase completed but gsd_plan_milestone was hard-blocked by the
@@ -489,6 +515,20 @@ export function checkAutoStartAfterDiscuss() {
489
515
  logWarning("guided", `manifest unlink failed: ${e.message}`);
490
516
  }
491
517
  }
518
+ // R3b: belt-and-suspenders for silent registration failure. The discuss flow
519
+ // finished and STATE.md exists, but the milestone may never have landed in
520
+ // the DB. Without this guard, the user sees "Milestone M001 ready." and then
521
+ // /gsd reports "No Active Milestone".
522
+ if (isDbAvailable()) {
523
+ const milestoneRow = getMilestone(milestoneId);
524
+ if (!milestoneRow) {
525
+ ctx.ui.notify(`Milestone ${milestoneId}: discuss artifacts on disk but no DB row exists. ` +
526
+ `PROJECT.md may have failed to register milestones. ` +
527
+ `Re-save PROJECT.md with canonical "- [ ] M001: Title — One-liner" lines, ` +
528
+ `then re-run /gsd to recover.`, "error");
529
+ return false;
530
+ }
531
+ }
492
532
  pendingAutoStartMap.delete(basePath);
493
533
  ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
494
534
  startAutoDetached(ctx, pi, basePath, false, { step });
@@ -177,8 +177,12 @@ export function resolveDir(parentDir, idPrefix) {
177
177
  const exact = entries.find(e => e.isDirectory() && e.name === idPrefix);
178
178
  if (exact)
179
179
  return exact.name;
180
+ const idLower = idPrefix.toLowerCase();
181
+ const exactCaseInsensitive = entries.find(e => e.isDirectory() && e.name.toLowerCase() === idLower);
182
+ if (exactCaseInsensitive)
183
+ return exactCaseInsensitive.name;
180
184
  // Prefix match for legacy descriptor dirs: M001-SOMETHING
181
- const prefixed = entries.find(e => e.isDirectory() && e.name.startsWith(idPrefix + "-"));
185
+ const prefixed = entries.find(e => e.isDirectory() && e.name.toLowerCase().startsWith(idLower + "-"));
182
186
  return prefixed ? prefixed.name : null;
183
187
  }
184
188
  catch {
@@ -141,7 +141,6 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
141
141
  task_id: isRootArtifact ? undefined : params.task_id,
142
142
  }, basePath);
143
143
  let registeredMilestones = [];
144
- let registrationWarning;
145
144
  if (params.artifact_type === "PROJECT") {
146
145
  try {
147
146
  registeredMilestones = registerProjectMilestoneSequence(contentToSave);
@@ -150,12 +149,55 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
150
149
  }
151
150
  catch (err) {
152
151
  const msg = err instanceof Error ? err.message : String(err);
153
- registrationWarning = `PROJECT artifact saved, but milestone registration failed: ${msg}`;
154
- logWarning("tool", registrationWarning, {
152
+ logError("tool", `gsd_summary_save: PROJECT artifact persisted but milestone registration threw: ${msg}`, {
155
153
  tool: "gsd_summary_save",
156
154
  error: String(err),
157
155
  stack: err instanceof Error ? err.stack ?? "" : "",
158
156
  });
157
+ // PROJECT.md was persisted by saveArtifactToDb above; the artifacts row
158
+ // changed even though no milestones registered. Invalidate so subsequent
159
+ // /gsd reads see the persisted artifact instead of the pre-save cache.
160
+ invalidateStateCache();
161
+ return {
162
+ content: [{
163
+ type: "text",
164
+ text: `Error: PROJECT.md was saved to ${relativePath} but milestone registration failed: ${msg}. ` +
165
+ `The DB has no milestone rows for this project, so /gsd will report "No Active Milestone". ` +
166
+ `Re-call gsd_summary_save(PROJECT) once the underlying error is resolved — INSERT OR IGNORE makes registration idempotent.`,
167
+ }],
168
+ details: {
169
+ operation: "save_summary",
170
+ path: relativePath,
171
+ artifact_type: params.artifact_type,
172
+ error: "milestone_registration_threw",
173
+ registration_error: msg,
174
+ },
175
+ isError: true,
176
+ };
177
+ }
178
+ if (registeredMilestones.length === 0) {
179
+ logError("tool", `gsd_summary_save: PROJECT.md saved to ${relativePath} but parsed zero milestones — registration produced no DB rows`, {
180
+ tool: "gsd_summary_save",
181
+ });
182
+ // PROJECT.md was persisted; invalidate so subsequent reads see the new
183
+ // artifacts row even though no milestones registered.
184
+ invalidateStateCache();
185
+ return {
186
+ content: [{
187
+ type: "text",
188
+ text: `Error: PROJECT.md was saved to ${relativePath} but contains zero parseable milestone lines, ` +
189
+ `so no milestones were registered in the DB. /gsd will report "No Active Milestone". ` +
190
+ `Rewrite PROJECT.md so the "Milestone Sequence" section uses canonical lines: ` +
191
+ `\`- [ ] M001: <Title> — <One-liner>\` (em-dash, double-dash \`--\`, or single-dash \`-\` separator), then re-call gsd_summary_save(PROJECT).`,
192
+ }],
193
+ details: {
194
+ operation: "save_summary",
195
+ path: relativePath,
196
+ artifact_type: params.artifact_type,
197
+ error: "milestone_registration_empty_parse",
198
+ },
199
+ isError: true,
200
+ };
159
201
  }
160
202
  }
161
203
  if (params.artifact_type === "CONTEXT" && !params.task_id) {
@@ -178,7 +220,6 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
178
220
  artifact_type: params.artifact_type,
179
221
  content_source: contentSource,
180
222
  ...(registeredMilestones.length > 0 ? { registeredMilestones } : {}),
181
- ...(registrationWarning ? { warning: registrationWarning } : {}),
182
223
  },
183
224
  };
184
225
  }
@@ -1,3 +1,4 @@
1
+ // GSD2 UOK Audit Events and DB-First Projection Writes
1
2
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { randomUUID } from "node:crypto";
@@ -5,6 +6,7 @@ import { isStaleWrite } from "../auto/turn-epoch.js";
5
6
  import { withFileLockSync } from "../file-lock.js";
6
7
  import { gsdRoot } from "../paths.js";
7
8
  import { isDbAvailable, insertAuditEvent } from "../gsd-db.js";
9
+ import { CURRENT_UOK_CONTRACT_VERSION, validateAuditEvent } from "./contracts.js";
8
10
  function auditLogPath(basePath) {
9
11
  return join(gsdRoot(basePath), "audit", "events.jsonl");
10
12
  }
@@ -13,6 +15,7 @@ function ensureAuditDir(basePath) {
13
15
  }
14
16
  export function buildAuditEnvelope(args) {
15
17
  return {
18
+ version: CURRENT_UOK_CONTRACT_VERSION,
16
19
  eventId: randomUUID(),
17
20
  traceId: args.traceId,
18
21
  turnId: args.turnId,
@@ -27,6 +30,25 @@ export function emitUokAuditEvent(basePath, event) {
27
30
  // Drop writes from a turn superseded by timeout recovery / cancellation.
28
31
  if (isStaleWrite("uok-audit"))
29
32
  return;
33
+ const validation = validateAuditEvent(event);
34
+ if (!validation.ok) {
35
+ throw new Error(`Invalid UOK audit event: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
36
+ }
37
+ const canonical = validation.value;
38
+ if (isDbAvailable()) {
39
+ try {
40
+ insertAuditEvent({
41
+ ...canonical,
42
+ payload: {
43
+ ...canonical.payload,
44
+ contractVersion: canonical.version ?? CURRENT_UOK_CONTRACT_VERSION,
45
+ },
46
+ });
47
+ }
48
+ catch (err) {
49
+ throw new Error(`DB authoritative audit write failed: ${err instanceof Error ? err.message : String(err)}`);
50
+ }
51
+ }
30
52
  try {
31
53
  ensureAuditDir(basePath);
32
54
  const path = auditLogPath(basePath);
@@ -39,18 +61,10 @@ export function emitUokAuditEvent(basePath, event) {
39
61
  // POSIX O_APPEND atomicity still protects small line writes, so skipping
40
62
  // the lock rather than stalling orchestration is the correct tradeoff.
41
63
  withFileLockSync(path, () => {
42
- appendFileSync(path, `${JSON.stringify(event)}\n`, "utf-8");
64
+ appendFileSync(path, `${JSON.stringify(canonical)}\n`, "utf-8");
43
65
  }, { onLocked: "skip" });
44
66
  }
45
67
  catch {
46
68
  // Best-effort: audit writes must never break orchestration.
47
69
  }
48
- if (!isDbAvailable())
49
- return;
50
- try {
51
- insertAuditEvent(event);
52
- }
53
- catch {
54
- // Projection failures are non-fatal while legacy readers are still active.
55
- }
56
70
  }