@opengsd/gsd-pi 1.3.0-dev.65546769 → 1.3.0-dev.eed73bea

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 (183) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +11 -2
  3. package/dist/resources/extensions/google-cli/stream-adapter.js +82 -15
  4. package/dist/resources/extensions/gsd/auto/orchestrator.js +12 -3
  5. package/dist/resources/extensions/gsd/auto-dispatch.js +17 -14
  6. package/dist/resources/extensions/gsd/auto-prompts.js +43 -12
  7. package/dist/resources/extensions/gsd/auto-recovery.js +13 -6
  8. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +103 -13
  9. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +6 -1
  10. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -3
  12. package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -19
  13. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +75 -1
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
  15. package/dist/resources/extensions/gsd/commands-context.js +19 -1
  16. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +16 -10
  17. package/dist/resources/extensions/gsd/commands-worktree.js +12 -10
  18. package/dist/resources/extensions/gsd/dashboard-overlay.js +32 -3
  19. package/dist/resources/extensions/gsd/db/queries.js +60 -0
  20. package/dist/resources/extensions/gsd/doctor-providers.js +92 -8
  21. package/dist/resources/extensions/gsd/exec-sandbox.js +45 -9
  22. package/dist/resources/extensions/gsd/forensics.js +2 -32
  23. package/dist/resources/extensions/gsd/git-service.js +4 -4
  24. package/dist/resources/extensions/gsd/guided-flow-queue.js +59 -5
  25. package/dist/resources/extensions/gsd/health-widget.js +55 -29
  26. package/dist/resources/extensions/gsd/markdown-renderer.js +6 -2
  27. package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +44 -21
  28. package/dist/resources/extensions/gsd/milestone-implementation-evidence.js +26 -20
  29. package/dist/resources/extensions/gsd/quick.js +45 -2
  30. package/dist/resources/extensions/gsd/session-forensics.js +11 -1
  31. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +52 -3
  32. package/dist/resources/extensions/gsd/tools/complete-slice.js +34 -3
  33. package/dist/resources/extensions/gsd/tools/complete-task.js +78 -16
  34. package/dist/resources/extensions/gsd/tools/exec-tool.js +7 -2
  35. package/dist/resources/extensions/gsd/unit-context-composer.js +23 -7
  36. package/dist/resources/extensions/gsd/unit-registry.js +25 -3
  37. package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +33 -3
  38. package/dist/resources/extensions/gsd/validation-block-guard.js +9 -4
  39. package/dist/resources/extensions/gsd/workspace-git-preflight.js +30 -1
  40. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  41. package/dist/web/standalone/.next/BUILD_ID +1 -1
  42. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  43. package/dist/web/standalone/.next/build-manifest.json +3 -3
  44. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  45. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/index.html +1 -1
  64. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  71. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/dist/web/standalone/.next/static/chunks/{796.e0bdc932325d7e03.js → 796.3976108148518f7d.js} +3 -3
  77. package/dist/web/standalone/.next/static/chunks/{webpack-f46ea08200a0227e.js → webpack-7c1d97e39be2da11.js} +1 -1
  78. package/package.json +1 -1
  79. package/packages/cloud-mcp-gateway/package.json +2 -2
  80. package/packages/contracts/dist/workflow.d.ts +1 -0
  81. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  82. package/packages/contracts/dist/workflow.js +2 -0
  83. package/packages/contracts/dist/workflow.js.map +1 -1
  84. package/packages/contracts/package.json +1 -1
  85. package/packages/daemon/package.json +4 -4
  86. package/packages/gsd-agent-core/package.json +5 -5
  87. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  88. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +21 -9
  89. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  90. package/packages/gsd-agent-modes/package.json +7 -7
  91. package/packages/mcp-server/README.md +1 -1
  92. package/packages/mcp-server/dist/server.d.ts +1 -1
  93. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  94. package/packages/mcp-server/dist/server.js +3 -3
  95. package/packages/mcp-server/dist/server.js.map +1 -1
  96. package/packages/mcp-server/dist/workflow-tools.d.ts +13 -1
  97. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  98. package/packages/mcp-server/dist/workflow-tools.js +34 -20
  99. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  100. package/packages/mcp-server/package.json +4 -4
  101. package/packages/native/package.json +1 -1
  102. package/packages/pi-agent-core/package.json +1 -1
  103. package/packages/pi-ai/package.json +1 -1
  104. package/packages/pi-coding-agent/package.json +7 -7
  105. package/packages/pi-tui/package.json +2 -2
  106. package/packages/rpc-client/package.json +2 -2
  107. package/pkg/package.json +1 -1
  108. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +20 -2
  109. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +80 -0
  110. package/src/resources/extensions/google-cli/stream-adapter.ts +106 -19
  111. package/src/resources/extensions/gsd/auto/orchestrator.ts +25 -11
  112. package/src/resources/extensions/gsd/auto-dispatch.ts +18 -17
  113. package/src/resources/extensions/gsd/auto-prompts.ts +54 -12
  114. package/src/resources/extensions/gsd/auto-recovery.ts +13 -6
  115. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -12
  116. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +6 -1
  117. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -0
  118. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -3
  119. package/src/resources/extensions/gsd/bootstrap/system-context.ts +52 -18
  120. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +82 -1
  121. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
  122. package/src/resources/extensions/gsd/commands-context.ts +18 -1
  123. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -9
  124. package/src/resources/extensions/gsd/commands-worktree.ts +12 -10
  125. package/src/resources/extensions/gsd/dashboard-overlay.ts +32 -3
  126. package/src/resources/extensions/gsd/db/queries.ts +79 -0
  127. package/src/resources/extensions/gsd/doctor-providers.ts +103 -9
  128. package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
  129. package/src/resources/extensions/gsd/forensics.ts +2 -33
  130. package/src/resources/extensions/gsd/git-service.ts +5 -5
  131. package/src/resources/extensions/gsd/guided-flow-queue.ts +82 -4
  132. package/src/resources/extensions/gsd/health-widget.ts +69 -32
  133. package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
  134. package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +51 -19
  135. package/src/resources/extensions/gsd/milestone-implementation-evidence.ts +35 -21
  136. package/src/resources/extensions/gsd/quick.ts +43 -2
  137. package/src/resources/extensions/gsd/session-forensics.ts +11 -1
  138. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +76 -8
  139. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -1
  140. package/src/resources/extensions/gsd/tests/commands-context.test.ts +26 -0
  141. package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +80 -0
  142. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +11 -0
  143. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +48 -8
  144. package/src/resources/extensions/gsd/tests/complete-task.test.ts +75 -0
  145. package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +55 -2
  146. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +26 -1
  147. package/src/resources/extensions/gsd/tests/doctor-forensics-db-open-regression.test.ts +70 -2
  148. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +107 -0
  149. package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +38 -0
  150. package/src/resources/extensions/gsd/tests/exec-tool.test.ts +45 -1
  151. package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +88 -0
  152. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +42 -0
  153. package/src/resources/extensions/gsd/tests/health-widget.test.ts +268 -3
  154. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +119 -1
  155. package/src/resources/extensions/gsd/tests/integration/queue-active-milestone-context-budget.test.ts +93 -0
  156. package/src/resources/extensions/gsd/tests/integration/quick-branch-lifecycle.test.ts +56 -9
  157. package/src/resources/extensions/gsd/tests/knowledge-cold-start.test.ts +14 -0
  158. package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +78 -0
  159. package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +43 -1
  160. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +26 -0
  161. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +1 -1
  162. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +54 -1
  163. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +195 -1
  164. package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +185 -0
  165. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +87 -0
  166. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -0
  167. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +68 -0
  168. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +26 -0
  169. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +193 -14
  170. package/src/resources/extensions/gsd/tests/unmerged-milestone-guard.test.ts +25 -0
  171. package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +79 -0
  172. package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +66 -0
  173. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +151 -2
  174. package/src/resources/extensions/gsd/tools/complete-slice.ts +30 -3
  175. package/src/resources/extensions/gsd/tools/complete-task.ts +86 -16
  176. package/src/resources/extensions/gsd/tools/exec-tool.ts +7 -3
  177. package/src/resources/extensions/gsd/unit-context-composer.ts +33 -7
  178. package/src/resources/extensions/gsd/unit-registry.ts +25 -3
  179. package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +41 -5
  180. package/src/resources/extensions/gsd/validation-block-guard.ts +13 -7
  181. package/src/resources/extensions/gsd/workspace-git-preflight.ts +31 -0
  182. /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_buildManifest.js +0 -0
  183. /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_ssgManifest.js +0 -0
@@ -94,27 +94,48 @@ function getActiveDecisions() {
94
94
  return [];
95
95
  }
96
96
  }
97
- /**
98
- * True when a memory row has a `structured_fields` JSON payload containing
99
- * the given `markerKey: "value"` pair. Matches the LIKE pattern used by
100
- * `backfillDecisionsToMemories` so the scanner is consistent with the
101
- * backfill's idempotency check.
102
- */
103
- function memoryHasSourceMarker(markerKey, value) {
97
+ function emptyMemorySourceMarkers() {
98
+ return { decisionIds: new Set(), knowledgeIds: new Set() };
99
+ }
100
+ function getMemorySourceMarkers() {
101
+ const markers = emptyMemorySourceMarkers();
104
102
  if (!isDbAvailable())
105
- return false;
103
+ return markers;
106
104
  const adapter = _getAdapter();
107
105
  if (!adapter)
108
- return false;
106
+ return markers;
109
107
  try {
110
- const pattern = `%"${markerKey}":"${value}"%`;
111
- const row = adapter
112
- .prepare("SELECT 1 FROM memories WHERE structured_fields LIKE :pattern LIMIT 1")
113
- .get({ ":pattern": pattern });
114
- return row !== undefined;
108
+ const rows = adapter
109
+ .prepare("SELECT structured_fields FROM memories WHERE structured_fields IS NOT NULL")
110
+ .all();
111
+ for (const row of rows) {
112
+ collectMemorySourceMarker(markers, row["structured_fields"]);
113
+ }
115
114
  }
116
115
  catch {
117
- return false;
116
+ return markers;
117
+ }
118
+ return markers;
119
+ }
120
+ function collectMemorySourceMarker(markers, raw) {
121
+ if (typeof raw !== "string" || raw.length === 0)
122
+ return;
123
+ try {
124
+ const parsed = JSON.parse(raw);
125
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
126
+ return;
127
+ const fields = parsed;
128
+ const decisionId = fields["sourceDecisionId"];
129
+ if (typeof decisionId === "string" && decisionId.length > 0) {
130
+ markers.decisionIds.add(decisionId);
131
+ }
132
+ const knowledgeId = fields["sourceKnowledgeId"];
133
+ if (typeof knowledgeId === "string" && knowledgeId.length > 0) {
134
+ markers.knowledgeIds.add(knowledgeId);
135
+ }
136
+ }
137
+ catch {
138
+ return;
118
139
  }
119
140
  }
120
141
  // ─── Public API ──────────────────────────────────────────────────────────────
@@ -127,10 +148,14 @@ const SAMPLE_LIMIT = 5;
127
148
  export function scanConsolidationGaps(basePath) {
128
149
  // ── Decisions ────────────────────────────────────────────────────────
129
150
  const decisions = getActiveDecisions();
151
+ const knowledgeRows = parseKnowledgeRows(knowledgeMdContent(basePath));
152
+ const memorySourceMarkers = decisions.length > 0 || knowledgeRows.length > 0
153
+ ? getMemorySourceMarkers()
154
+ : emptyMemorySourceMarkers();
130
155
  const decisionSamples = [];
131
156
  let decisionMigrated = 0;
132
157
  for (const decision of decisions) {
133
- if (memoryHasSourceMarker("sourceDecisionId", decision.id)) {
158
+ if (memorySourceMarkers.decisionIds.has(decision.id)) {
134
159
  decisionMigrated += 1;
135
160
  continue;
136
161
  }
@@ -142,16 +167,14 @@ export function scanConsolidationGaps(basePath) {
142
167
  }
143
168
  }
144
169
  // ── KNOWLEDGE.md ─────────────────────────────────────────────────────
145
- const knowledgeRows = parseKnowledgeRows(knowledgeMdContent(basePath));
146
170
  const knowledgeByTable = { rules: 0, patterns: 0, lessons: 0 };
147
171
  const knowledgeSamples = [];
148
172
  let knowledgeMigrated = 0;
149
173
  for (const row of knowledgeRows) {
150
174
  knowledgeByTable[row.table] += 1;
151
- // Phase 6 will introduce a `sourceKnowledgeId` marker as part of the
152
- // KNOWLEDGE.md backfill. Until that path ships, this check returns
153
- // false for every row, which is the honest state of the consolidation.
154
- if (memoryHasSourceMarker("sourceKnowledgeId", row.id)) {
175
+ // KNOWLEDGE.md backfill writes `sourceKnowledgeId`; rows without that
176
+ // marker are still reported as consolidation gaps.
177
+ if (memorySourceMarkers.knowledgeIds.has(row.id)) {
155
178
  knowledgeMigrated += 1;
156
179
  continue;
157
180
  }
@@ -10,6 +10,8 @@ import { logWarning } from "./workflow-logger.js";
10
10
  import { resolveTasksDir } from "./paths.js";
11
11
  /** Large enough for unbounded milestone-history git log scans in big repos. */
12
12
  const GIT_LOG_MAX_BUFFER = 16 * 1024 * 1024;
13
+ const LOG_FIELD_SEPARATOR = "\x1f";
14
+ const LOG_RECORD_SEPARATOR = "\x1e";
13
15
  /**
14
16
  * Check whether a milestone produced implementation artifacts (non-`.gsd/`
15
17
  * files) in git history. The primary signal is the branch diff against the
@@ -179,7 +181,7 @@ function getChangedFilesFromMilestoneTaggedCommits(basePath, milestoneId) {
179
181
  // Primary: path-scoped log against .gsd/milestones/<id>. Fast and unbounded
180
182
  // by depth when .gsd/ is tracked in git.
181
183
  const scoped = scanGsdTaggedCommits(basePath, milestoneId, [
182
- "log", "--format=%H%x1f%B%x1e", "HEAD", "--", `.gsd/milestones/${milestoneId}`,
184
+ "log", "--full-diff", "--name-only", "--format=%x1e%H%x1f%B%x1f", "HEAD", "--", `.gsd/milestones/${milestoneId}`,
183
185
  ]);
184
186
  if (!scoped.ok)
185
187
  return scoped;
@@ -195,7 +197,7 @@ function getChangedFilesFromMilestoneTaggedCommits(basePath, milestoneId) {
195
197
  // reintroducing the rolling-depth failure class removed in #4699 where
196
198
  // milestone evidence aged out behind unrelated activity.
197
199
  const unscoped = scanGsdTaggedCommits(basePath, milestoneId, [
198
- "log", "--format=%H%x1f%B%x1e", "HEAD",
200
+ "log", "--name-only", "--format=%x1e%H%x1f%B%x1f", "HEAD",
199
201
  ]);
200
202
  if (!unscoped.ok)
201
203
  return scoped.matched ? scoped : unscoped;
@@ -280,8 +282,7 @@ function backfillChangedFilesFromUntaggedMilestoneCommits(basePath, milestoneId)
280
282
  continue;
281
283
  if (commitMessageHasGsdTrailer(record.message))
282
284
  continue;
283
- const commitFiles = getChangedFilesForCommit(basePath, record.hash);
284
- const implementationFiles = commitFiles.map(normalizeRepoPath).filter(isImplementationPath);
285
+ const implementationFiles = record.files.map(normalizeRepoPath).filter(isImplementationPath);
285
286
  if (implementationFiles.length === 0)
286
287
  continue;
287
288
  if (!implementationFiles.some((file) => hintSet.has(file)))
@@ -306,24 +307,28 @@ function backfillChangedFilesFromUntaggedMilestoneCommits(basePath, milestoneId)
306
307
  }
307
308
  }
308
309
  function getCommitRecords(basePath) {
309
- const logOutput = execFileSync("git", ["log", "--format=%H%x1f%P%x1f%cI%x1f%B%x1e", "HEAD"], {
310
+ const logOutput = execFileSync("git", ["log", "--name-only", "--format=%x1e%H%x1f%P%x1f%cI%x1f%B%x1f", "HEAD"], {
310
311
  cwd: basePath,
311
312
  stdio: ["ignore", "pipe", "pipe"],
312
313
  encoding: "utf-8",
313
314
  maxBuffer: GIT_LOG_MAX_BUFFER,
314
315
  });
315
316
  return logOutput
316
- .split("\x1e")
317
- .map((record) => record.trim())
317
+ .split(LOG_RECORD_SEPARATOR)
318
318
  .filter(Boolean)
319
319
  .flatMap((record) => {
320
- const parts = record.split("\x1f");
321
- if (parts.length < 4)
320
+ const parts = record.split(LOG_FIELD_SEPARATOR);
321
+ if (parts.length < 5)
322
322
  return [];
323
- const [hash, parents, committedAt, ...messageParts] = parts;
324
- return [{ hash: hash.trim(), parents: parents.trim(), committedAt: committedAt.trim(), message: messageParts.join("\x1f") }];
323
+ const [hash, parents, committedAt] = parts;
324
+ const files = parseNameOnlyFiles(parts.at(-1) ?? "");
325
+ const message = parts.slice(3, -1).join(LOG_FIELD_SEPARATOR);
326
+ return [{ hash: hash.trim(), parents: parents.trim(), committedAt: committedAt.trim(), message, files }];
325
327
  });
326
328
  }
329
+ function parseNameOnlyFiles(rawFiles) {
330
+ return rawFiles.split(/\r?\n/).map((file) => file.trim()).filter(Boolean);
331
+ }
327
332
  function isFullCommitSha(value) {
328
333
  return /^[0-9a-f]{40}$/i.test(value);
329
334
  }
@@ -336,23 +341,24 @@ function scanGsdTaggedCommits(basePath, milestoneId, gitArgs) {
336
341
  maxBuffer: GIT_LOG_MAX_BUFFER,
337
342
  });
338
343
  const records = logOutput
339
- .split("\x1e")
340
- .map((record) => record.trim())
344
+ .split(LOG_RECORD_SEPARATOR)
341
345
  .filter(Boolean)
342
346
  .flatMap((record) => {
343
- const sep = record.indexOf("\x1f");
344
- if (sep === -1)
347
+ const parts = record.split(LOG_FIELD_SEPARATOR);
348
+ if (parts.length < 3)
349
+ return [];
350
+ const hash = parts[0].trim();
351
+ if (!hash)
345
352
  return [];
346
- const hash = record.slice(0, sep).trim();
347
- const message = record.slice(sep + 1);
348
- return [{ hash, message }];
353
+ const files = parseNameOnlyFiles(parts.at(-1) ?? "");
354
+ const message = parts.slice(1, -1).join(LOG_FIELD_SEPARATOR);
355
+ return [{ message, files }];
349
356
  });
350
357
  const files = new Set();
351
358
  let matched = false;
352
- for (const { hash, message } of records) {
359
+ for (const { message, files: commitFiles } of records) {
353
360
  if (!commitMessageHasGsdTrailer(message))
354
361
  continue;
355
- const commitFiles = getChangedFilesForCommit(basePath, hash);
356
362
  if (!commitMatchesMilestone(basePath, message, milestoneId, commitFiles))
357
363
  continue;
358
364
  matched = true;
@@ -8,8 +8,8 @@
8
8
  * Quick tasks live in `.gsd/quick/` and are tracked in STATE.md's
9
9
  * "Quick Tasks Completed" table.
10
10
  */
11
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
12
- import { isAbsolute, join, relative } from "node:path";
11
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
12
+ import { isAbsolute, join, relative, resolve } from "node:path";
13
13
  import { QUICK_BRANCH_RE } from "./branch-patterns.js";
14
14
  import { loadPrompt } from "./prompt-loader.js";
15
15
  import { gsdRoot } from "./paths.js";
@@ -19,6 +19,7 @@ import { nativeBranchExists, nativeDetectMainBranch, nativeDiffNumstat } from ".
19
19
  import { nativeHasStagedChanges } from "./native-git-bridge.js";
20
20
  import { resolveUokFlags } from "./uok/flags.js";
21
21
  let pendingQuickReturn = null;
22
+ const pendingQuickReturnMisses = new Map();
22
23
  // ─── Quick Task Helpers ───────────────────────────────────────────────────────
23
24
  /**
24
25
  * Generate a URL-friendly slug from a description.
@@ -169,11 +170,36 @@ export function buildQuickCommitInstruction(basePath, root) {
169
170
  " - Quick tasks run outside the auto-mode lifecycle — there is no system auto-commit, so commit directly here.",
170
171
  ].join("\n");
171
172
  }
173
+ function readHeadBranchName(basePath) {
174
+ try {
175
+ const gitPath = join(basePath, ".git");
176
+ if (!existsSync(gitPath))
177
+ return null;
178
+ let headPath;
179
+ if (lstatSync(gitPath).isDirectory()) {
180
+ headPath = join(gitPath, "HEAD");
181
+ }
182
+ else {
183
+ const gitFile = readFileSync(gitPath, "utf-8").trim();
184
+ if (!gitFile.startsWith("gitdir: "))
185
+ return null;
186
+ headPath = join(resolve(basePath, gitFile.slice("gitdir: ".length)), "HEAD");
187
+ }
188
+ const head = readFileSync(headPath, "utf-8").trim();
189
+ if (!head.startsWith("ref: refs/heads/"))
190
+ return null;
191
+ return head.slice("ref: refs/heads/".length);
192
+ }
193
+ catch {
194
+ return null;
195
+ }
196
+ }
172
197
  function quickReturnStatePath(basePath) {
173
198
  return join(gsdRoot(basePath), "runtime", "quick-return.json");
174
199
  }
175
200
  function persistPendingReturn(state) {
176
201
  pendingQuickReturn = state;
202
+ pendingQuickReturnMisses.delete(state.basePath);
177
203
  mkdirSync(join(gsdRoot(state.basePath), "runtime"), { recursive: true });
178
204
  writeFileSync(quickReturnStatePath(state.basePath), JSON.stringify(state) + "\n", "utf-8");
179
205
  }
@@ -181,6 +207,13 @@ function readPendingReturn(basePath) {
181
207
  if (pendingQuickReturn && pendingQuickReturn.basePath === basePath) {
182
208
  return pendingQuickReturn;
183
209
  }
210
+ if (pendingQuickReturnMisses.has(basePath)) {
211
+ const statePath = quickReturnStatePath(basePath);
212
+ if (!existsSync(statePath) && readHeadBranchName(basePath) === pendingQuickReturnMisses.get(basePath)) {
213
+ return null;
214
+ }
215
+ pendingQuickReturnMisses.delete(basePath);
216
+ }
184
217
  try {
185
218
  const raw = readFileSync(quickReturnStatePath(basePath), "utf-8");
186
219
  const parsed = JSON.parse(raw);
@@ -191,6 +224,7 @@ function readPendingReturn(basePath) {
191
224
  && typeof parsed.slug === "string"
192
225
  && typeof parsed.description === "string") {
193
226
  pendingQuickReturn = parsed;
227
+ pendingQuickReturnMisses.delete(basePath);
194
228
  return pendingQuickReturn;
195
229
  }
196
230
  }
@@ -200,14 +234,23 @@ function readPendingReturn(basePath) {
200
234
  const inferred = inferQuickReturnFromBranch(basePath);
201
235
  if (inferred) {
202
236
  pendingQuickReturn = inferred;
237
+ pendingQuickReturnMisses.delete(basePath);
203
238
  return inferred;
204
239
  }
240
+ const branchAtMiss = readHeadBranchName(basePath);
241
+ if (branchAtMiss) {
242
+ pendingQuickReturnMisses.set(basePath, branchAtMiss);
243
+ }
205
244
  return null;
206
245
  }
207
246
  function clearPendingReturn(basePath) {
208
247
  if (pendingQuickReturn?.basePath === basePath) {
209
248
  pendingQuickReturn = null;
210
249
  }
250
+ const branchAtMiss = readHeadBranchName(basePath);
251
+ if (branchAtMiss) {
252
+ pendingQuickReturnMisses.set(basePath, branchAtMiss);
253
+ }
211
254
  rmSync(quickReturnStatePath(basePath), { force: true });
212
255
  }
213
256
  function hasStagedChanges(basePath) {
@@ -176,11 +176,21 @@ export function extractTrace(entries) {
176
176
  }
177
177
  // Flush any pending tool calls that never got results (crash mid-tool)
178
178
  for (const [, pending] of pendingTools) {
179
+ const missingResultError = `Tool call ${pending.name} started but no toolResult was recorded`;
179
180
  toolCalls.push({
180
181
  name: pending.name,
181
182
  input: redactInput(pending.name, pending.input),
182
- isError: false,
183
+ result: "missing tool result (stream/tool-call abort before execution)",
184
+ isError: true,
183
185
  });
186
+ errors.push(missingResultError);
187
+ // Mark the matching commandsRun entry as failed so it is consistent with
188
+ // the isError: true on the tool call above (bash/bg_shell only).
189
+ if (pending.name === "bash" || pending.name === "bg_shell") {
190
+ const lastCmd = findLast(commandsRun, c => c.command === String(pending.input.command));
191
+ if (lastCmd)
192
+ lastCmd.failed = true;
193
+ }
184
194
  }
185
195
  return {
186
196
  toolCalls,
@@ -5,9 +5,12 @@
5
5
  // the detect+repair composition moves here. The previous repairStaleRenders
6
6
  // had zero callers in production code — wiring it through
7
7
  // reconcileBeforeDispatch closes that gap.
8
+ import { existsSync } from "node:fs";
9
+ import { join } from "node:path";
8
10
  import { detectStaleRenders, renderPlanCheckboxes, renderRoadmapFromDb, renderSliceSummary, renderTaskSummary, } from "../../markdown-renderer.js";
9
- import { getMilestone, getMilestoneSlices, getSlice, getSliceTasks, setSliceSummaryMd } from "../../gsd-db.js";
10
- import { resolveSliceFile } from "../../paths.js";
11
+ import { getMilestone, getMilestoneSlices, getSlice, getSliceTasks, isDbAvailable, setSliceSummaryMd, } from "../../gsd-db.js";
12
+ import { getWorkflowDatabasePath, openWorkflowDatabasePath, refreshWorkflowDatabaseFromDisk, } from "../../db-workspace.js";
13
+ import { gsdRoot, resolveSliceFile } from "../../paths.js";
11
14
  import { logWarning } from "../../workflow-logger.js";
12
15
  // ─── Core (basePath-only — usable by both drift API and legacy wrapper) ──────
13
16
  function detectStaleRenderDriftFromBasePath(basePath) {
@@ -82,7 +85,42 @@ function resolveRoadmapMilestoneIdFromPath(normPath) {
82
85
  }
83
86
  return fileMatch?.[1] ?? milestoneMatch[1];
84
87
  }
88
+ function expectedDbPathForStaleRenderRepair(basePath) {
89
+ return join(gsdRoot(basePath), "gsd.db");
90
+ }
91
+ function ensureDbForStaleRenderRepair(basePath) {
92
+ const dbPath = expectedDbPathForStaleRenderRepair(basePath);
93
+ if (isDbAvailable() && getWorkflowDatabasePath() === dbPath)
94
+ return true;
95
+ if (!existsSync(dbPath))
96
+ return false;
97
+ try {
98
+ return openWorkflowDatabasePath(dbPath);
99
+ }
100
+ catch (err) {
101
+ logWarning("reconcile", `stale-render repair could not reopen DB: ${err.message}`);
102
+ return false;
103
+ }
104
+ }
105
+ function retryDbForStaleRenderRepair(basePath) {
106
+ const dbPath = expectedDbPathForStaleRenderRepair(basePath);
107
+ if (!existsSync(dbPath))
108
+ return false;
109
+ try {
110
+ if (isDbAvailable() && getWorkflowDatabasePath() === dbPath && refreshWorkflowDatabaseFromDisk()) {
111
+ return true;
112
+ }
113
+ return openWorkflowDatabasePath(dbPath);
114
+ }
115
+ catch (err) {
116
+ logWarning("reconcile", `stale-render repair could not reopen DB: ${err.message}`);
117
+ return false;
118
+ }
119
+ }
85
120
  async function repairStaleRenderFromBasePath(record, basePath) {
121
+ if (!ensureDbForStaleRenderRepair(basePath)) {
122
+ throw new Error(`stale-render drift: database unavailable for repair (${basePath})`);
123
+ }
86
124
  const normPath = record.renderPath.replace(/\\/g, "/");
87
125
  const reason = record.reason;
88
126
  if (reason.includes("in roadmap")) {
@@ -101,7 +139,18 @@ async function repairStaleRenderFromBasePath(record, basePath) {
101
139
  const sliceId = pathMatch[2] && pathMatch[3] && /^\d+$/.test(pathMatch[2])
102
140
  ? `S${String(parseInt(pathMatch[3], 10)).padStart(2, "0")}`
103
141
  : pathMatch[2];
104
- const wrote = await renderPlanCheckboxes(basePath, milestoneId, sliceId, record.renderPath);
142
+ let wrote = false;
143
+ try {
144
+ wrote = await renderPlanCheckboxes(basePath, milestoneId, sliceId, record.renderPath);
145
+ }
146
+ catch (err) {
147
+ if (!retryDbForStaleRenderRepair(basePath))
148
+ throw err;
149
+ wrote = await renderPlanCheckboxes(basePath, milestoneId, sliceId, record.renderPath);
150
+ }
151
+ if (!wrote && retryDbForStaleRenderRepair(basePath)) {
152
+ wrote = await renderPlanCheckboxes(basePath, milestoneId, sliceId, record.renderPath);
153
+ }
105
154
  if (!wrote) {
106
155
  throw new Error(`stale-render drift: plan re-render wrote nothing for ${milestoneId}/${pathMatch[2]} ` +
107
156
  `(${record.renderPath}); slice has no tasks or its path is unresolvable`);
@@ -21,7 +21,7 @@ import { classifyUatContent, escalatesArtifactUatToBrowser } from "../uat-policy
21
21
  import { invalidateStateCache } from "../state.js";
22
22
  import { renderRoadmapFromDb, roadmapRenderMarksSliceDone } from "../markdown-renderer.js";
23
23
  import { isStaleWrite } from "../auto/turn-epoch.js";
24
- import { flushWorkflowProjections } from "../projection-flush.js";
24
+ import { renderStateProjection, renderTopLevelQueueFromDb, renderTopLevelRoadmapFromDb } from "../workflow-projections.js";
25
25
  import { writeManifest } from "../workflow-manifest.js";
26
26
  import { appendEvent } from "../workflow-events.js";
27
27
  import { logWarning, logError } from "../workflow-logger.js";
@@ -457,11 +457,42 @@ export async function handleCompleteSlice(params, basePath) {
457
457
  // ── Post-mutation hook: projections, manifest, event log ───────────────
458
458
  // Separate try/catch per step so a projection failure doesn't prevent
459
459
  // the event log entry (critical for worktree reconciliation).
460
+ //
461
+ // If the primary summary/UAT/roadmap write block failed (projectionStale),
462
+ // retry the milestone-level roadmap here so ROADMAP.md is not left stale
463
+ // after a committed slice completion. This restores the recovery that the
464
+ // removed flushWorkflowProjections/renderAllProjections provided.
465
+ if (projectionStale) {
466
+ try {
467
+ await renderRoadmapFromDb(artifactBasePath, params.milestoneId);
468
+ }
469
+ catch (projErr) {
470
+ logWarning("tool", `complete-slice milestone roadmap retry warning for ${params.milestoneId}/${params.sliceId}: ${projErr.message}`);
471
+ }
472
+ }
473
+ try {
474
+ await renderRoadmapFromDb(artifactBasePath, params.milestoneId);
475
+ }
476
+ catch (projErr) {
477
+ logWarning("tool", `complete-slice milestone roadmap projection warning for ${params.milestoneId}/${params.sliceId}: ${projErr.message}`);
478
+ }
479
+ try {
480
+ renderTopLevelRoadmapFromDb(artifactBasePath);
481
+ }
482
+ catch (projErr) {
483
+ logWarning("tool", `complete-slice roadmap projection warning for ${params.milestoneId}/${params.sliceId}: ${projErr.message}`);
484
+ }
485
+ try {
486
+ renderTopLevelQueueFromDb(artifactBasePath);
487
+ }
488
+ catch (projErr) {
489
+ logWarning("tool", `complete-slice queue projection warning for ${params.milestoneId}/${params.sliceId}: ${projErr.message}`);
490
+ }
460
491
  try {
461
- await flushWorkflowProjections(artifactBasePath, { milestoneId: params.milestoneId });
492
+ await renderStateProjection(artifactBasePath);
462
493
  }
463
494
  catch (projErr) {
464
- logWarning("tool", `complete-slice projection warning for ${params.milestoneId}/${params.sliceId}: ${projErr.message}`);
495
+ logWarning("tool", `complete-slice state projection warning for ${params.milestoneId}/${params.sliceId}: ${projErr.message}`);
465
496
  }
466
497
  try {
467
498
  writeManifest(artifactBasePath);
@@ -5,22 +5,23 @@
5
5
  *
6
6
  * Validates inputs, writes task row and rendered SUMMARY.md to DB in a
7
7
  * transaction, then renders projections to disk and invalidates caches.
8
- * Projection write failures are reported as stale projections and do not roll
9
- * back committed DB state.
8
+ * If the critical task summary / plan projection write fails, the DB
9
+ * completion is compensated back to pending so DB state does not drift ahead
10
+ * of PLAN.md.
10
11
  */
11
- import { existsSync } from "node:fs";
12
+ import { existsSync, unlinkSync } from "node:fs";
12
13
  import { join } from "node:path";
13
14
  import { isClosedStatus } from "../status-guards.js";
14
- import { transaction, insertMilestone, insertSlice, insertTask, insertVerificationEvidence, getMilestone, getSlice, getTask, updateTaskStatus, deleteVerificationEvidence, saveGateResult, getPendingGatesForTurn, } from "../gsd-db.js";
15
+ import { transaction, insertMilestone, insertSlice, insertTask, insertVerificationEvidence, getMilestone, getSlice, getTask, updateTaskStatus, deleteVerificationEvidence, saveGateResult, getPendingGatesForTurn, isDbAvailable, } from "../gsd-db.js";
16
+ import { getWorkflowDatabasePath, openWorkflowDatabasePath } from "../db-workspace.js";
15
17
  import { getGatesForTurn } from "../gate-registry.js";
16
18
  import { gsdProjectionRoot, clearPathCache, resolveMilestonePath, resolveSlicePath } from "../paths.js";
17
19
  import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
18
20
  import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
19
21
  import { saveFile, clearParseCache } from "../files.js";
20
22
  import { invalidateStateCache } from "../state.js";
21
- import { renderPlanCheckboxes } from "../markdown-renderer.js";
22
- import { renderSummaryContent } from "../workflow-projections.js";
23
- import { flushWorkflowProjections } from "../projection-flush.js";
23
+ import { renderPlanCheckboxes, renderRoadmapFromDb } from "../markdown-renderer.js";
24
+ import { renderStateProjection, renderSummaryContent, renderTopLevelQueueFromDb, renderTopLevelRoadmapFromDb, } from "../workflow-projections.js";
24
25
  import { writeManifest } from "../workflow-manifest.js";
25
26
  import { appendEvent } from "../workflow-events.js";
26
27
  import { logWarning, logError } from "../workflow-logger.js";
@@ -44,6 +45,39 @@ function taskSummaryPath(basePath, milestoneId, sliceId, taskId) {
44
45
  // Fallback: legacy hardcoded path (milestone/slice dir not on disk yet)
45
46
  return join(gsdProjectionRoot(basePath), "milestones", milestoneId, "slices", sliceId, "tasks", `${taskId}-SUMMARY.md`);
46
47
  }
48
+ async function renderCompleteTaskProjections(basePath, milestoneId) {
49
+ try {
50
+ await renderRoadmapFromDb(basePath, milestoneId);
51
+ }
52
+ catch (err) {
53
+ logWarning("projection", `renderRoadmapFromDb failed for ${milestoneId}: ${err.message}`);
54
+ }
55
+ try {
56
+ renderTopLevelRoadmapFromDb(basePath);
57
+ }
58
+ catch (err) {
59
+ logWarning("projection", `renderTopLevelRoadmapFromDb failed: ${err.message}`);
60
+ }
61
+ try {
62
+ renderTopLevelQueueFromDb(basePath);
63
+ }
64
+ catch (err) {
65
+ logWarning("projection", `renderTopLevelQueueFromDb failed: ${err.message}`);
66
+ }
67
+ try {
68
+ await renderStateProjection(basePath);
69
+ }
70
+ catch (err) {
71
+ logWarning("projection", `renderStateProjection failed: ${err.message}`);
72
+ }
73
+ }
74
+ function ensureCompleteTaskDbOpen(dbPath) {
75
+ if (!dbPath || dbPath === ":memory:")
76
+ return isDbAvailable();
77
+ if (isDbAvailable() && getWorkflowDatabasePath() === dbPath)
78
+ return true;
79
+ return openWorkflowDatabasePath(dbPath);
80
+ }
47
81
  async function repairMissingTaskSummaryProjection(artifactBasePath, taskRow) {
48
82
  const summaryPath = taskSummaryPath(artifactBasePath, taskRow.milestone_id, taskRow.slice_id, taskRow.id);
49
83
  const summaryMd = renderSummaryContent(taskRow, taskRow.slice_id, taskRow.milestone_id, []);
@@ -60,7 +94,7 @@ async function repairMissingTaskSummaryProjection(artifactBasePath, taskRow) {
60
94
  clearPathCache();
61
95
  clearParseCache();
62
96
  try {
63
- await flushWorkflowProjections(artifactBasePath, { milestoneId: taskRow.milestone_id });
97
+ await renderCompleteTaskProjections(artifactBasePath, taskRow.milestone_id);
64
98
  }
65
99
  catch (projErr) {
66
100
  logWarning("tool", `complete-task repair projection warning: ${projErr.message}`);
@@ -181,6 +215,7 @@ export async function handleCompleteTask(params, basePath) {
181
215
  let guardError = null;
182
216
  let summaryMd = "";
183
217
  let repairTaskSummaryRow = null;
218
+ const rollbackDbPath = getWorkflowDatabasePath();
184
219
  // ── ADR-011 Phase 2: validate escalation payload BEFORE any side effects ─
185
220
  // Building the artifact runs the full shape validation (2-4 options, unique
186
221
  // ids, recommendation references a real id). If the payload is malformed
@@ -317,20 +352,48 @@ export async function handleCompleteTask(params, basePath) {
317
352
  if (guardError) {
318
353
  return { error: guardError };
319
354
  }
320
- let projectionStale = false;
321
355
  // Resolve and write summary to disk
322
356
  const summaryPath = taskSummaryPath(artifactBasePath, params.milestoneId, params.sliceId, params.taskId);
323
357
  try {
324
358
  await saveFile(summaryPath, summaryMd);
325
359
  // Toggle or regenerate the plan projection from DB. Missing projection
326
360
  // files are rebuilt by the renderer instead of being skipped.
327
- await renderPlanCheckboxes(artifactBasePath, params.milestoneId, params.sliceId);
361
+ if (!ensureCompleteTaskDbOpen(rollbackDbPath)) {
362
+ throw new Error(`database unavailable before plan projection render for ${params.milestoneId}/${params.sliceId}`);
363
+ }
364
+ const wrotePlan = await renderPlanCheckboxes(artifactBasePath, params.milestoneId, params.sliceId);
365
+ if (!wrotePlan) {
366
+ throw new Error(`plan projection write returned false for ${params.milestoneId}/${params.sliceId}`);
367
+ }
328
368
  }
329
369
  catch (renderErr) {
330
- projectionStale = true;
331
- logWarning("projection", `complete_task projection write failed for ${params.milestoneId}/${params.sliceId}/${params.taskId}; DB completion remains committed`, {
332
- error: renderErr.message,
333
- });
370
+ logWarning("projection", `complete_task projection write failed for ${params.milestoneId}/${params.sliceId}/${params.taskId}`, { error: renderErr.message });
371
+ let rollbackSucceeded = false;
372
+ try {
373
+ ensureCompleteTaskDbOpen(rollbackDbPath);
374
+ deleteVerificationEvidence(params.milestoneId, params.sliceId, params.taskId);
375
+ updateTaskStatus(params.milestoneId, params.sliceId, params.taskId, "pending");
376
+ invalidateStateCache();
377
+ rollbackSucceeded = true;
378
+ }
379
+ catch (rollbackErr) {
380
+ logWarning("projection", `complete_task rollback failed after projection write failure for ${params.milestoneId}/${params.sliceId}/${params.taskId}: ${rollbackErr.message}`);
381
+ }
382
+ try {
383
+ if (existsSync(summaryPath))
384
+ unlinkSync(summaryPath);
385
+ }
386
+ catch (summaryErr) {
387
+ logWarning("projection", `complete_task could not remove SUMMARY.md after projection write failure for ${params.milestoneId}/${params.sliceId}/${params.taskId}: ${summaryErr.message}`);
388
+ }
389
+ // Clear path/parse caches regardless of rollback outcome so stale
390
+ // entries from the failed write attempt don't leak into subsequent calls.
391
+ clearPathCache();
392
+ clearParseCache();
393
+ const returnMsg = rollbackSucceeded
394
+ ? `complete_task projection write failed for ${params.milestoneId}/${params.sliceId}/${params.taskId}; rolled completion back to pending`
395
+ : `complete_task projection write failed for ${params.milestoneId}/${params.sliceId}/${params.taskId}; rollback also failed — task may remain complete with stale plan`;
396
+ return { error: returnMsg };
334
397
  }
335
398
  // ── Close gates owned by execute-task (Q5/Q6/Q7) for this task ────────
336
399
  // Each gate id maps to a specific params field via taskGateFieldForId.
@@ -435,7 +498,7 @@ export async function handleCompleteTask(params, basePath) {
435
498
  // Separate try/catch per step so a projection failure doesn't prevent
436
499
  // the event log entry (critical for worktree reconciliation).
437
500
  try {
438
- await flushWorkflowProjections(artifactBasePath, { milestoneId: params.milestoneId });
501
+ await renderCompleteTaskProjections(artifactBasePath, params.milestoneId);
439
502
  }
440
503
  catch (projErr) {
441
504
  logWarning("tool", `complete-task projection warning: ${projErr.message}`);
@@ -465,6 +528,5 @@ export async function handleCompleteTask(params, basePath) {
465
528
  milestoneId: params.milestoneId,
466
529
  summaryPath,
467
530
  ...(escalationMetadata ? { escalation: escalationMetadata } : {}),
468
- ...(projectionStale ? { stale: true } : {}),
469
531
  };
470
532
  }
@@ -144,7 +144,7 @@ export async function executeGsdExec(params, deps) {
144
144
  if (bashReferencesProjectRootOutsideWorktree(script, deps.baseDir)) {
145
145
  return paramError("script references the original project root while running inside a milestone worktree; use the active worktree path or relative paths");
146
146
  }
147
- const opts = buildExecOptions(deps.baseDir, deps.preferences?.context_mode, { env: deps.env, now: deps.now, generateId: deps.generateId });
147
+ const opts = buildExecOptions(deps.baseDir, deps.preferences?.context_mode, { env: deps.env, now: deps.now, generateId: deps.generateId, signal: deps.signal });
148
148
  const run = deps.run ?? runExecSandbox;
149
149
  try {
150
150
  const result = await run({
@@ -235,6 +235,7 @@ function formatResult(result) {
235
235
  exit_code: result.exit_code,
236
236
  signal: result.signal,
237
237
  timed_out: result.timed_out,
238
+ aborted: result.aborted === true,
238
239
  force_resolved: result.force_resolved,
239
240
  duration_ms: result.duration_ms,
240
241
  stdout_bytes: result.stdout_bytes,
@@ -245,12 +246,16 @@ function formatResult(result) {
245
246
  stderr_path: result.stderr_path,
246
247
  meta_path: result.meta_path,
247
248
  },
248
- isError: result.timed_out || result.signal !== null || result.exit_code !== 0,
249
+ isError: result.aborted === true || result.timed_out || result.signal !== null || result.exit_code !== 0,
249
250
  };
250
251
  }
251
252
  function formatExit(result) {
252
253
  // force_resolved means a non-closing (D-state) child was force-resolved past its
253
254
  // hard deadline rather than observed exiting; distinguish it from a clean timeout.
255
+ if (result.aborted && result.force_resolved)
256
+ return "aborted(force-killed)";
257
+ if (result.aborted)
258
+ return "aborted";
254
259
  if (result.force_resolved)
255
260
  return "timeout(force-killed)";
256
261
  if (result.timed_out)