@opengsd/gsd-pi 1.1.1-dev.9f86580 → 1.1.1-dev.b2556262

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 (261) hide show
  1. package/dist/headless-recover.js +56 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/browser-tools/index.js +39 -22
  4. package/dist/resources/extensions/browser-tools/state.js +12 -0
  5. package/dist/resources/extensions/browser-tools/tools/session.js +3 -2
  6. package/dist/resources/extensions/browser-tools/utils.js +3 -3
  7. package/dist/resources/extensions/gsd/auto/loop.js +4 -2
  8. package/dist/resources/extensions/gsd/auto/phases.js +43 -10
  9. package/dist/resources/extensions/gsd/auto/session.js +20 -1
  10. package/dist/resources/extensions/gsd/auto/workflow-kernel.js +1 -0
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +72 -12
  12. package/dist/resources/extensions/gsd/auto-model-selection.js +128 -9
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +19 -2
  14. package/dist/resources/extensions/gsd/auto-prompts.js +24 -19
  15. package/dist/resources/extensions/gsd/auto-recovery.js +4 -2
  16. package/dist/resources/extensions/gsd/auto-runtime-state.js +3 -0
  17. package/dist/resources/extensions/gsd/auto-start.js +1 -1
  18. package/dist/resources/extensions/gsd/auto.js +14 -11
  19. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
  20. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +172 -65
  21. package/dist/resources/extensions/gsd/closeout-wizard.js +32 -9
  22. package/dist/resources/extensions/gsd/commands/handlers/ops.js +2 -9
  23. package/dist/resources/extensions/gsd/commands-maintenance.js +93 -15
  24. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +2 -2
  25. package/dist/resources/extensions/gsd/db-writer.js +35 -0
  26. package/dist/resources/extensions/gsd/docs/preferences-reference.md +50 -1
  27. package/dist/resources/extensions/gsd/gsd-db.js +480 -172
  28. package/dist/resources/extensions/gsd/markdown-renderer.js +37 -53
  29. package/dist/resources/extensions/gsd/md-importer.js +38 -3
  30. package/dist/resources/extensions/gsd/migration-auto-check.js +126 -31
  31. package/dist/resources/extensions/gsd/parsers-legacy.js +23 -0
  32. package/dist/resources/extensions/gsd/planning-path-scope.js +22 -4
  33. package/dist/resources/extensions/gsd/pre-execution-checks.js +10 -2
  34. package/dist/resources/extensions/gsd/preferences-models.js +110 -43
  35. package/dist/resources/extensions/gsd/preferences-types.js +13 -0
  36. package/dist/resources/extensions/gsd/preferences-validation.js +68 -3
  37. package/dist/resources/extensions/gsd/preferences.js +4 -1
  38. package/dist/resources/extensions/gsd/prompts/gate-evaluate.md +1 -1
  39. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  40. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  41. package/dist/resources/extensions/gsd/roadmap-slices.js +5 -1
  42. package/dist/resources/extensions/gsd/safety/content-validator.js +6 -4
  43. package/dist/resources/extensions/gsd/source-observations.js +306 -0
  44. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +15 -8
  45. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +33 -5
  46. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-worker.js +34 -13
  47. package/dist/resources/extensions/gsd/state-reconciliation/index.js +39 -14
  48. package/dist/resources/extensions/gsd/state-reconciliation/spawn-gate.js +4 -4
  49. package/dist/resources/extensions/gsd/state.js +7 -3
  50. package/dist/resources/extensions/gsd/tool-contract.js +14 -0
  51. package/dist/resources/extensions/gsd/tool-presentation-plan.js +1 -9
  52. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -6
  53. package/dist/resources/extensions/gsd/tools/plan-slice.js +42 -11
  54. package/dist/resources/extensions/gsd/tools/plan-task.js +7 -1
  55. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +57 -429
  56. package/dist/resources/extensions/gsd/uat-policy.js +130 -0
  57. package/dist/resources/extensions/gsd/uat-run.js +414 -0
  58. package/dist/resources/extensions/gsd/unit-context-manifest.js +3 -4
  59. package/dist/resources/extensions/gsd/verdict-parser.js +3 -8
  60. package/dist/resources/extensions/gsd/workflow-manifest.js +132 -5
  61. package/dist/resources/extensions/gsd/workflow-projections.js +8 -0
  62. package/dist/resources/extensions/gsd/worktree-state-projection.js +18 -17
  63. package/dist/resources/extensions/subagent/agents.js +1 -0
  64. package/dist/resources/extensions/subagent/index.js +27 -12
  65. package/dist/resources/extensions/subagent/launch.js +7 -2
  66. package/dist/web/standalone/.next/BUILD_ID +1 -1
  67. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  68. package/dist/web/standalone/.next/build-manifest.json +2 -2
  69. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  70. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.html +1 -1
  87. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  94. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  95. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  97. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  98. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  99. package/dist/web/standalone/node_modules/@gsd/native/dist/native.js +22 -0
  100. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  101. package/package.json +4 -4
  102. package/packages/cloud-mcp-gateway/package.json +2 -2
  103. package/packages/contracts/package.json +1 -1
  104. package/packages/daemon/package.json +4 -4
  105. package/packages/gsd-agent-core/package.json +5 -5
  106. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  107. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js +21 -23
  108. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js.map +1 -1
  109. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +3 -0
  110. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  111. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +25 -0
  112. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  113. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts +1 -0
  114. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  115. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +66 -12
  116. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  117. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  118. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +18 -11
  119. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  120. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.d.ts.map +1 -1
  121. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js +16 -0
  122. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js.map +1 -1
  123. package/packages/gsd-agent-modes/package.json +7 -7
  124. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  125. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  126. package/packages/mcp-server/package.json +3 -3
  127. package/packages/native/dist/native.js +22 -0
  128. package/packages/native/package.json +1 -1
  129. package/packages/pi-agent-core/package.json +1 -1
  130. package/packages/pi-ai/dist/image-models.generated.d.ts +30 -0
  131. package/packages/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  132. package/packages/pi-ai/dist/image-models.generated.js +30 -0
  133. package/packages/pi-ai/dist/image-models.generated.js.map +1 -1
  134. package/packages/pi-ai/dist/models.generated.d.ts +23 -17
  135. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  136. package/packages/pi-ai/dist/models.generated.js +25 -24
  137. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  138. package/packages/pi-ai/package.json +1 -1
  139. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  140. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  141. package/packages/pi-coding-agent/dist/theme/themes.js +1 -1
  142. package/packages/pi-coding-agent/dist/theme/themes.js.map +1 -1
  143. package/packages/pi-coding-agent/package.json +7 -7
  144. package/packages/pi-tui/dist/utils.d.ts +11 -0
  145. package/packages/pi-tui/dist/utils.d.ts.map +1 -1
  146. package/packages/pi-tui/dist/utils.js +119 -6
  147. package/packages/pi-tui/dist/utils.js.map +1 -1
  148. package/packages/pi-tui/package.json +2 -1
  149. package/packages/rpc-client/package.json +2 -2
  150. package/pkg/dist/theme/themes.js +1 -1
  151. package/pkg/dist/theme/themes.js.map +1 -1
  152. package/pkg/package.json +1 -1
  153. package/src/resources/extensions/browser-tools/index.ts +39 -22
  154. package/src/resources/extensions/browser-tools/state.ts +13 -0
  155. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +57 -0
  156. package/src/resources/extensions/browser-tools/tools/session.ts +4 -2
  157. package/src/resources/extensions/browser-tools/utils.ts +3 -3
  158. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  159. package/src/resources/extensions/gsd/auto/loop.ts +4 -2
  160. package/src/resources/extensions/gsd/auto/phases.ts +42 -10
  161. package/src/resources/extensions/gsd/auto/session.ts +22 -1
  162. package/src/resources/extensions/gsd/auto/workflow-kernel.ts +1 -0
  163. package/src/resources/extensions/gsd/auto-dispatch.ts +85 -12
  164. package/src/resources/extensions/gsd/auto-model-selection.ts +164 -12
  165. package/src/resources/extensions/gsd/auto-post-unit.ts +20 -2
  166. package/src/resources/extensions/gsd/auto-prompts.ts +23 -20
  167. package/src/resources/extensions/gsd/auto-recovery.ts +22 -3
  168. package/src/resources/extensions/gsd/auto-runtime-state.ts +5 -0
  169. package/src/resources/extensions/gsd/auto-start.ts +1 -1
  170. package/src/resources/extensions/gsd/auto.ts +13 -10
  171. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
  172. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +225 -72
  173. package/src/resources/extensions/gsd/closeout-wizard.ts +47 -13
  174. package/src/resources/extensions/gsd/commands/handlers/ops.ts +2 -17
  175. package/src/resources/extensions/gsd/commands-maintenance.ts +124 -13
  176. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +2 -2
  177. package/src/resources/extensions/gsd/db-writer.ts +38 -0
  178. package/src/resources/extensions/gsd/docs/preferences-reference.md +50 -1
  179. package/src/resources/extensions/gsd/gsd-db.ts +564 -186
  180. package/src/resources/extensions/gsd/markdown-renderer.ts +44 -66
  181. package/src/resources/extensions/gsd/md-importer.ts +49 -2
  182. package/src/resources/extensions/gsd/migration-auto-check.ts +154 -34
  183. package/src/resources/extensions/gsd/parsers-legacy.ts +20 -0
  184. package/src/resources/extensions/gsd/planning-path-scope.ts +22 -4
  185. package/src/resources/extensions/gsd/pre-execution-checks.ts +9 -2
  186. package/src/resources/extensions/gsd/preferences-models.ts +112 -43
  187. package/src/resources/extensions/gsd/preferences-types.ts +39 -0
  188. package/src/resources/extensions/gsd/preferences-validation.ts +76 -2
  189. package/src/resources/extensions/gsd/preferences.ts +5 -0
  190. package/src/resources/extensions/gsd/prompts/gate-evaluate.md +1 -1
  191. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  192. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  193. package/src/resources/extensions/gsd/roadmap-slices.ts +6 -1
  194. package/src/resources/extensions/gsd/safety/content-validator.ts +8 -5
  195. package/src/resources/extensions/gsd/source-observations.ts +402 -0
  196. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +20 -8
  197. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +44 -5
  198. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-worker.ts +39 -11
  199. package/src/resources/extensions/gsd/state-reconciliation/index.ts +45 -15
  200. package/src/resources/extensions/gsd/state-reconciliation/spawn-gate.ts +4 -4
  201. package/src/resources/extensions/gsd/state.ts +7 -4
  202. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +15 -0
  203. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +299 -1
  204. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +32 -0
  205. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +75 -3
  206. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +22 -1
  207. package/src/resources/extensions/gsd/tests/before-provider-context-management.test.ts +145 -0
  208. package/src/resources/extensions/gsd/tests/closeout-wizard.test.ts +44 -0
  209. package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +26 -1
  210. package/src/resources/extensions/gsd/tests/content-validator.test.ts +74 -0
  211. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +16 -2
  212. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +1 -11
  213. package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +64 -0
  214. package/src/resources/extensions/gsd/tests/gate-storage.test.ts +15 -0
  215. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +62 -1
  216. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +15 -0
  217. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +42 -0
  218. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +99 -0
  219. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +99 -2
  220. package/src/resources/extensions/gsd/tests/plan-task.test.ts +19 -0
  221. package/src/resources/extensions/gsd/tests/preferences.test.ts +14 -0
  222. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +1 -0
  223. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +9 -0
  224. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +101 -1
  225. package/src/resources/extensions/gsd/tests/repository-registry.test.ts +2 -2
  226. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -0
  227. package/src/resources/extensions/gsd/tests/schema-v21-sequence.test.ts +5 -3
  228. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +162 -18
  229. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +8 -0
  230. package/src/resources/extensions/gsd/tests/source-observations.test.ts +275 -0
  231. package/src/resources/extensions/gsd/tests/stale-queued-milestone.test.ts +43 -0
  232. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -21
  233. package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +203 -0
  234. package/src/resources/extensions/gsd/tests/uat-policy.test.ts +170 -0
  235. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +7 -1
  236. package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
  237. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +306 -1
  238. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +73 -6
  239. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +511 -1
  240. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +44 -0
  241. package/src/resources/extensions/gsd/tool-contract.ts +28 -0
  242. package/src/resources/extensions/gsd/tool-presentation-plan.ts +1 -11
  243. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -6
  244. package/src/resources/extensions/gsd/tools/plan-slice.ts +54 -12
  245. package/src/resources/extensions/gsd/tools/plan-task.ts +8 -1
  246. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +66 -526
  247. package/src/resources/extensions/gsd/types.ts +1 -0
  248. package/src/resources/extensions/gsd/uat-policy.ts +191 -0
  249. package/src/resources/extensions/gsd/uat-run.ts +550 -0
  250. package/src/resources/extensions/gsd/unit-context-manifest.ts +3 -4
  251. package/src/resources/extensions/gsd/verdict-parser.ts +3 -10
  252. package/src/resources/extensions/gsd/workflow-manifest.ts +193 -7
  253. package/src/resources/extensions/gsd/workflow-projections.ts +9 -0
  254. package/src/resources/extensions/gsd/worktree-state-projection.ts +22 -22
  255. package/src/resources/extensions/shared/tests/format-utils.test.ts +8 -3
  256. package/src/resources/extensions/subagent/agents.ts +4 -0
  257. package/src/resources/extensions/subagent/index.ts +28 -3
  258. package/src/resources/extensions/subagent/launch.ts +8 -0
  259. package/src/resources/extensions/subagent/tests/model-override.test.ts +31 -0
  260. /package/dist/web/standalone/.next/static/{zzYMrKpPGfRQRxSFO32Jr → tJOKQbQRO-9MiFDO8DIDS}/_buildManifest.js +0 -0
  261. /package/dist/web/standalone/.next/static/{zzYMrKpPGfRQRxSFO32Jr → tJOKQbQRO-9MiFDO8DIDS}/_ssgManifest.js +0 -0
@@ -0,0 +1,402 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Active-unit source observations for provider payload context.
3
+
4
+ import { existsSync, readFileSync, statSync } from "node:fs";
5
+ import { isAbsolute, relative, resolve, sep } from "node:path";
6
+
7
+ import type { TaskRow } from "./db-task-slice-rows.js";
8
+ import { extractPlanningPathReference, normalizeFilePath } from "./pre-execution-checks.js";
9
+
10
+ export const WHOLE_FILE_OBSERVATION_MAX_BYTES = 50 * 1024;
11
+ export const WHOLE_FILE_OBSERVATION_MAX_LINES = 2000;
12
+
13
+ export type SourceObservationStatus =
14
+ | "whole"
15
+ | "missing"
16
+ | "binary/image"
17
+ | "over-threshold"
18
+ | "glob"
19
+ | "directory"
20
+ | "unresolved selector";
21
+
22
+ export interface SourceObservationUnit {
23
+ unitType: string;
24
+ unitId: string;
25
+ startedAt: number;
26
+ basePath: string;
27
+ }
28
+
29
+ export type SourceObservationSource = "plan" | "read" | "mutation";
30
+
31
+ export interface SourceObservation {
32
+ path: string;
33
+ absolutePath: string | null;
34
+ status: SourceObservationStatus;
35
+ source: SourceObservationSource;
36
+ text?: string;
37
+ bytes?: number;
38
+ lines?: number;
39
+ reason?: string;
40
+ }
41
+
42
+ interface ActiveSourceObservationSet {
43
+ unit: SourceObservationUnit;
44
+ observations: Map<string, SourceObservation>;
45
+ }
46
+
47
+ const SOURCE_CONTEXT_TITLE = "## Source Context Block";
48
+ const SOURCE_OBSERVATION_UNIT_TYPE = "execute-task";
49
+
50
+ export function supportsSourceObservationsForUnit(unitType: string): boolean {
51
+ return unitType === SOURCE_OBSERVATION_UNIT_TYPE;
52
+ }
53
+
54
+ export class SourceObservationStore {
55
+ private active: ActiveSourceObservationSet | null = null;
56
+
57
+ beginUnit(unit: SourceObservationUnit): void {
58
+ if (!supportsSourceObservationsForUnit(unit.unitType)) {
59
+ this.clear();
60
+ return;
61
+ }
62
+ if (this.matches(unit)) return;
63
+ this.active = { unit: { ...unit }, observations: new Map() };
64
+ }
65
+
66
+ clear(): void {
67
+ this.active = null;
68
+ }
69
+
70
+ degradeUnit(unit: Pick<SourceObservationUnit, "unitType" | "unitId" | "startedAt">): void {
71
+ if (!this.active) return;
72
+ const current = this.active.unit;
73
+ if (
74
+ current.unitType === unit.unitType &&
75
+ current.unitId === unit.unitId &&
76
+ current.startedAt === unit.startedAt
77
+ ) {
78
+ this.active = null;
79
+ }
80
+ }
81
+
82
+ observePlanTask(task: TaskRow): void {
83
+ if (!this.active) return;
84
+ for (const entry of planDeclaredSourceEntries(task)) {
85
+ this.observePath(entry.path, "plan");
86
+ }
87
+ }
88
+
89
+ observeRead(input: { path?: unknown; file_path?: unknown; [key: string]: unknown }): void {
90
+ if (!this.active) return;
91
+ const rawPath = readPathFromInput(input);
92
+ if (!rawPath.trim()) return;
93
+ this.observePath(rawPath, "read");
94
+ }
95
+
96
+ observeMutation(input: { path?: unknown; file_path?: unknown; [key: string]: unknown }): void {
97
+ if (!this.active) return;
98
+ const rawPath = readPathFromInput(input);
99
+ if (!rawPath.trim()) return;
100
+ this.observePath(rawPath, "mutation", { replaceExisting: true });
101
+ }
102
+
103
+ renderActiveBlock(): string | null {
104
+ if (!this.active || this.active.observations.size === 0) return null;
105
+ if (!supportsSourceObservationsForUnit(this.active.unit.unitType)) return null;
106
+ const observations = [...this.active.observations.values()]
107
+ .sort((a, b) => a.path.localeCompare(b.path));
108
+ const lines = [
109
+ SOURCE_CONTEXT_TITLE,
110
+ `Active Unit: ${this.active.unit.unitType} ${this.active.unit.unitId}`,
111
+ "",
112
+ "The files below are protected active-Unit source context. " +
113
+ "Use this block instead of rereading small files just to recover line windows.",
114
+ ];
115
+
116
+ const wholeFiles = observations.filter((observation) => observation.status === "whole");
117
+ const unavailable = observations.filter((observation) => observation.status !== "whole");
118
+
119
+ if (wholeFiles.length > 0) {
120
+ lines.push("", "### Whole-File Observations");
121
+ for (const observation of wholeFiles) {
122
+ lines.push(
123
+ "",
124
+ `#### ${observation.path}`,
125
+ `Status: whole-file (${observation.lines ?? 0} lines, ${formatSize(observation.bytes ?? 0)})`,
126
+ fencedSource(observation.text ?? ""),
127
+ );
128
+ }
129
+ }
130
+
131
+ if (unavailable.length > 0) {
132
+ lines.push("", "### Unavailable Source Observations");
133
+ for (const observation of unavailable) {
134
+ const detail = observation.reason ? ` - ${observation.reason}` : "";
135
+ lines.push(`- ${observation.path}: ${observation.status}${detail}`);
136
+ }
137
+ }
138
+
139
+ return lines.join("\n");
140
+ }
141
+
142
+ private matches(unit: SourceObservationUnit): boolean {
143
+ if (!this.active) return false;
144
+ const current = this.active.unit;
145
+ return current.unitType === unit.unitType &&
146
+ current.unitId === unit.unitId &&
147
+ current.startedAt === unit.startedAt &&
148
+ current.basePath === unit.basePath;
149
+ }
150
+
151
+ private observePath(
152
+ rawPath: string,
153
+ source: SourceObservationSource,
154
+ options: { replaceExisting?: boolean } = {},
155
+ ): void {
156
+ if (!this.active) return;
157
+ const observation = observeSourcePath(this.active.unit.basePath, rawPath, source);
158
+ const key = observation.absolutePath ?? `${observation.status}:${observation.path}`;
159
+ const existing = this.active.observations.get(key);
160
+ if (options.replaceExisting || !existing || observation.status === "whole" || existing.status !== "whole") {
161
+ this.active.observations.set(key, observation);
162
+ }
163
+ }
164
+ }
165
+
166
+ export function planDeclaredSourceEntries(
167
+ task: TaskRow,
168
+ ): Array<{ path: string; field: "files" | "inputs" }> {
169
+ const entries: Array<{ path: string; field: "files" | "inputs" }> = [];
170
+ for (const file of task.files) {
171
+ const path = extractPlanningPathReference(file) ?? file.trim();
172
+ if (path) entries.push({ path, field: "files" });
173
+ }
174
+ for (const input of task.inputs) {
175
+ const path = extractPlanningPathReference(input);
176
+ if (path) entries.push({ path, field: "inputs" });
177
+ }
178
+ return entries;
179
+ }
180
+
181
+ export function observeSourcePath(
182
+ basePath: string,
183
+ rawPath: string,
184
+ source: SourceObservationSource,
185
+ ): SourceObservation {
186
+ const normalizedRaw = normalizeFilePath(rawPath.trim());
187
+ const displayPath = normalizedRaw || rawPath.trim();
188
+ if (!normalizedRaw) {
189
+ return unavailable(displayPath || "<empty>", null, "unresolved selector", source);
190
+ }
191
+ if (containsGlobPattern(normalizedRaw)) {
192
+ return unavailable(displayPath, null, "glob", source, "glob selectors are not whole files");
193
+ }
194
+ if (rawPath.trim().endsWith("/")) {
195
+ return unavailable(displayPath, null, "directory", source, "directory selectors are not whole files");
196
+ }
197
+
198
+ const rootPath = resolve(basePath);
199
+ const absolutePath = isAbsolute(normalizedRaw) ? resolve(normalizedRaw) : resolve(rootPath, normalizedRaw);
200
+ const path = formatObservationPath(rootPath, absolutePath, displayPath);
201
+
202
+ if (!isPathInsideRoot(rootPath, absolutePath)) {
203
+ return unavailable(displayPath, absolutePath, "unresolved selector", source, "path is outside active Unit root");
204
+ }
205
+
206
+ if (!existsSync(absolutePath)) {
207
+ return unavailable(path, absolutePath, "missing", source, "file does not exist in the active Unit root");
208
+ }
209
+
210
+ let stat;
211
+ try {
212
+ stat = statSync(absolutePath);
213
+ } catch {
214
+ return unavailable(path, absolutePath, "missing", source, "file could not be inspected");
215
+ }
216
+
217
+ if (stat.isDirectory()) {
218
+ return unavailable(path, absolutePath, "directory", source, "directory selectors are not whole files");
219
+ }
220
+ if (!stat.isFile()) {
221
+ return unavailable(path, absolutePath, "unresolved selector", source, "path is not a regular file");
222
+ }
223
+ if (stat.size > WHOLE_FILE_OBSERVATION_MAX_BYTES) {
224
+ return unavailable(
225
+ path,
226
+ absolutePath,
227
+ "over-threshold",
228
+ source,
229
+ `${formatSize(stat.size)} exceeds ${formatSize(WHOLE_FILE_OBSERVATION_MAX_BYTES)}`,
230
+ );
231
+ }
232
+
233
+ let buffer: Buffer;
234
+ try {
235
+ buffer = readFileSync(absolutePath);
236
+ } catch {
237
+ return unavailable(path, absolutePath, "missing", source, "file could not be read");
238
+ }
239
+
240
+ if (isBinaryOrImage(buffer)) {
241
+ return unavailable(path, absolutePath, "binary/image", source, "binary or image files are not inlined as source text");
242
+ }
243
+
244
+ const text = buffer.toString("utf8");
245
+ const lines = countLines(text);
246
+ if (lines > WHOLE_FILE_OBSERVATION_MAX_LINES) {
247
+ return unavailable(
248
+ path,
249
+ absolutePath,
250
+ "over-threshold",
251
+ source,
252
+ `${lines} lines exceeds ${WHOLE_FILE_OBSERVATION_MAX_LINES}`,
253
+ );
254
+ }
255
+
256
+ return {
257
+ path,
258
+ absolutePath,
259
+ status: "whole",
260
+ source,
261
+ text,
262
+ bytes: buffer.byteLength,
263
+ lines,
264
+ };
265
+ }
266
+
267
+ export function injectSourceContextBlockIntoPayload(
268
+ payload: Record<string, unknown>,
269
+ block: string,
270
+ ): Record<string, unknown> {
271
+ const messages = payload.messages;
272
+ if (Array.isArray(messages)) {
273
+ return {
274
+ ...payload,
275
+ messages: [
276
+ ...withoutExistingSourceContextMessages(messages),
277
+ { role: "user", content: [{ type: "text", text: block }] },
278
+ ],
279
+ };
280
+ }
281
+
282
+ const input = payload.input;
283
+ if (Array.isArray(input)) {
284
+ return {
285
+ ...payload,
286
+ input: [
287
+ ...withoutExistingSourceContextItems(input),
288
+ { role: "user", content: [{ type: "input_text", text: block }] },
289
+ ],
290
+ };
291
+ }
292
+
293
+ return payload;
294
+ }
295
+
296
+ function unavailable(
297
+ path: string,
298
+ absolutePath: string | null,
299
+ status: Exclude<SourceObservationStatus, "whole">,
300
+ source: SourceObservationSource,
301
+ reason?: string,
302
+ ): SourceObservation {
303
+ return { path, absolutePath, status, source, reason };
304
+ }
305
+
306
+ function formatObservationPath(basePath: string, absolutePath: string, fallback: string): string {
307
+ const rel = relative(basePath, absolutePath);
308
+ if (rel && !rel.startsWith("..") && !isAbsolute(rel)) {
309
+ return rel.split(sep).join("/");
310
+ }
311
+ return fallback;
312
+ }
313
+
314
+ function isPathInsideRoot(rootPath: string, absolutePath: string): boolean {
315
+ const rel = relative(rootPath, absolutePath);
316
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
317
+ }
318
+
319
+ function containsGlobPattern(candidate: string): boolean {
320
+ return ["*", "?", "[", "]", "{", "}"].some((char) => candidate.includes(char));
321
+ }
322
+
323
+ function readPathFromInput(input: { path?: unknown; file_path?: unknown }): string {
324
+ if (typeof input.path === "string") return input.path;
325
+ if (typeof input.file_path === "string") return input.file_path;
326
+ return "";
327
+ }
328
+
329
+ function formatSize(bytes: number): string {
330
+ if (bytes < 1024) return `${bytes}B`;
331
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
332
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
333
+ }
334
+
335
+ function isBinaryOrImage(buffer: Buffer): boolean {
336
+ if (buffer.includes(0)) return true;
337
+ if (startsWith(buffer, [0xff, 0xd8, 0xff])) return true;
338
+ if (startsWith(buffer, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return true;
339
+ if (startsWithAscii(buffer, "GIF")) return true;
340
+ if (
341
+ startsWithAscii(buffer, "RIFF") &&
342
+ buffer.length >= 12 &&
343
+ buffer.subarray(8, 12).toString("ascii") === "WEBP"
344
+ ) return true;
345
+ return false;
346
+ }
347
+
348
+ function startsWith(buffer: Buffer, signature: readonly number[]): boolean {
349
+ if (buffer.length < signature.length) return false;
350
+ return signature.every((byte, index) => buffer[index] === byte);
351
+ }
352
+
353
+ function startsWithAscii(buffer: Buffer, text: string): boolean {
354
+ return buffer.length >= text.length && buffer.subarray(0, text.length).toString("ascii") === text;
355
+ }
356
+
357
+ function countLines(text: string): number {
358
+ if (text.length === 0) return 0;
359
+ return text.endsWith("\n") ? text.split("\n").length - 1 : text.split("\n").length;
360
+ }
361
+
362
+ function fencedSource(text: string): string {
363
+ const longest = longestBacktickRun(text);
364
+ const fence = longest >= 3 ? "`".repeat(longest + 1) : "```";
365
+ return `${fence}\n${text}\n${fence}`;
366
+ }
367
+
368
+ function longestBacktickRun(text: string): number {
369
+ let longest = 0;
370
+ let current = 0;
371
+ for (const char of text) {
372
+ if (char === "`") {
373
+ current += 1;
374
+ longest = Math.max(longest, current);
375
+ } else {
376
+ current = 0;
377
+ }
378
+ }
379
+ return longest;
380
+ }
381
+
382
+ function firstText(content: unknown): string | null {
383
+ if (typeof content === "string") return content;
384
+ if (!Array.isArray(content)) return null;
385
+ const block = content.find((entry) =>
386
+ entry && typeof entry === "object" && "text" in entry,
387
+ ) as { text?: unknown } | undefined;
388
+ return typeof block?.text === "string" ? block.text : null;
389
+ }
390
+
391
+ function isSourceContextMessage(message: unknown): boolean {
392
+ if (!message || typeof message !== "object") return false;
393
+ return firstText((message as { content?: unknown }).content)?.startsWith(SOURCE_CONTEXT_TITLE) === true;
394
+ }
395
+
396
+ function withoutExistingSourceContextMessages(messages: unknown[]): unknown[] {
397
+ return messages.filter((message) => !isSourceContextMessage(message));
398
+ }
399
+
400
+ function withoutExistingSourceContextItems(items: unknown[]): unknown[] {
401
+ return items.filter((item) => !isSourceContextMessage(item));
402
+ }
@@ -9,6 +9,7 @@
9
9
  import { existsSync, statSync } from "node:fs";
10
10
 
11
11
  import {
12
+ getAllMilestones,
12
13
  getMilestone,
13
14
  getMilestoneSlices,
14
15
  getSliceTasks,
@@ -41,17 +42,30 @@ function summaryMtimeIso(path: string): string | null {
41
42
  }
42
43
 
43
44
  export function detectMissingCompletionTimestampDrift(
44
- state: GSDState,
45
+ _state: GSDState,
45
46
  ctx: DriftContext,
46
47
  ): CompletionTimestampDrift[] {
47
48
  if (!isDbAvailable()) return [];
48
- const mid = state.activeMilestone?.id;
49
- if (!mid) return [];
50
-
51
- const milestone = getMilestone(mid);
52
- if (!milestone) return [];
53
49
 
50
+ // Scan every milestone, not just the active one. Markdown artifacts exist on
51
+ // disk for all milestones, so a user can manually complete a queued/parked
52
+ // milestone (edit the roadmap + drop a SUMMARY) and leave completed_at=null
53
+ // in the DB. Gating on the active milestone left that drift unrepaired until
54
+ // the milestone happened to become active.
54
55
  const drifts: CompletionTimestampDrift[] = [];
56
+ for (const { id: mid } of getAllMilestones()) {
57
+ collectMilestoneCompletionDrift(mid, ctx, drifts);
58
+ }
59
+ return drifts;
60
+ }
61
+
62
+ function collectMilestoneCompletionDrift(
63
+ mid: string,
64
+ ctx: DriftContext,
65
+ drifts: CompletionTimestampDrift[],
66
+ ): void {
67
+ const milestone = getMilestone(mid);
68
+ if (!milestone) return;
55
69
 
56
70
  // Milestone-level
57
71
  if (
@@ -105,8 +119,6 @@ export function detectMissingCompletionTimestampDrift(
105
119
  }
106
120
  }
107
121
  }
108
-
109
- return drifts;
110
122
  }
111
123
 
112
124
  export function repairMissingCompletionTimestamp(
@@ -61,6 +61,18 @@ function isRepairableStaleRenderReason(reason: string): boolean {
61
61
  );
62
62
  }
63
63
 
64
+ function canonicalizeMilestoneId(dirSegment: string): string {
65
+ if (getMilestone(dirSegment)) return dirSegment;
66
+ const suffixId = dirSegment.match(/^(M\d+-[a-z0-9]{6})(?:$|-)/)?.[1];
67
+ if (suffixId && getMilestone(suffixId)) return suffixId;
68
+
69
+ // Descriptor layout: e.g. M001-DESCRIPTOR → M001
70
+ const baseId = dirSegment.match(/^(M\d+)(?:$|-)/i)?.[1];
71
+ if (baseId && getMilestone(baseId)) return baseId;
72
+
73
+ return suffixId ?? baseId ?? dirSegment;
74
+ }
75
+
64
76
  function resolveRoadmapMilestoneIdFromPath(normPath: string): string {
65
77
  const milestoneMatch = normPath.match(/milestones\/([^/]+)\//);
66
78
  if (!milestoneMatch) {
@@ -106,7 +118,19 @@ async function repairStaleRenderFromBasePath(
106
118
  `stale-render drift: plan path missing milestone/slice segments: ${record.renderPath}`,
107
119
  );
108
120
  }
109
- await renderPlanCheckboxes(basePath, pathMatch[1], pathMatch[2]);
121
+ const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
122
+ const wrote = await renderPlanCheckboxes(
123
+ basePath,
124
+ milestoneId,
125
+ pathMatch[2],
126
+ record.renderPath,
127
+ );
128
+ if (!wrote) {
129
+ throw new Error(
130
+ `stale-render drift: plan re-render wrote nothing for ${milestoneId}/${pathMatch[2]} ` +
131
+ `(${record.renderPath}); slice has no tasks or its path is unresolvable`,
132
+ );
133
+ }
110
134
  return;
111
135
  }
112
136
 
@@ -120,7 +144,15 @@ async function repairStaleRenderFromBasePath(
120
144
  `stale-render drift: task summary path/reason malformed: ${record.renderPath} reason=${reason}`,
121
145
  );
122
146
  }
123
- await renderTaskSummary(basePath, pathMatch[1], pathMatch[2], taskMatch[1]);
147
+ const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
148
+ const wrote = await renderTaskSummary(basePath, milestoneId, pathMatch[2], taskMatch[1]);
149
+ if (!wrote) {
150
+ throw new Error(
151
+ `stale-render drift: task summary re-render wrote nothing for ` +
152
+ `${milestoneId}/${pathMatch[2]}/${taskMatch[1]} (${record.renderPath}); ` +
153
+ `task has no summary in DB or its slice path is unresolvable`,
154
+ );
155
+ }
124
156
  return;
125
157
  }
126
158
 
@@ -131,7 +163,7 @@ async function repairStaleRenderFromBasePath(
131
163
  `stale-render drift: slice summary path missing milestone/slice segments: ${record.renderPath}`,
132
164
  );
133
165
  }
134
- const milestoneId = pathMatch[1];
166
+ const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
135
167
  const sliceId = pathMatch[2];
136
168
  const slice = getSlice(milestoneId, sliceId);
137
169
  const uatPath = join(dirname(record.renderPath), buildSliceFileName(sliceId, "UAT"));
@@ -139,7 +171,14 @@ async function repairStaleRenderFromBasePath(
139
171
  if (slice?.full_uat_md && !existsSync(uatPath)) {
140
172
  setSliceSummaryMd(milestoneId, sliceId, slice.full_summary_md ?? "", "");
141
173
  }
142
- await renderSliceSummary(basePath, milestoneId, sliceId);
174
+ const wrote = await renderSliceSummary(basePath, milestoneId, sliceId);
175
+ if (!wrote) {
176
+ throw new Error(
177
+ `stale-render drift: slice summary re-render wrote nothing for ` +
178
+ `${milestoneId}/${sliceId} (${record.renderPath}); slice has no summary/UAT ` +
179
+ `in DB or its path is unresolvable`,
180
+ );
181
+ }
143
182
  return;
144
183
  }
145
184
 
@@ -152,7 +191,7 @@ async function repairStaleRenderFromBasePath(
152
191
  }
153
192
  // When UAT.md is removed from disk, mirror that intent by clearing stale
154
193
  // persisted UAT content instead of rehydrating it back onto disk.
155
- const milestoneId = pathMatch[1];
194
+ const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
156
195
  const sliceId = pathMatch[2];
157
196
  const slice = getSlice(milestoneId, sliceId);
158
197
  if (!slice) {
@@ -10,6 +10,11 @@ import {
10
10
  readSessionLockData,
11
11
  removeStaleSessionLock,
12
12
  } from "../../session-lock.js";
13
+ import { clearStaleWorkerLock } from "../../crash-recovery.js";
14
+ import { findStaleWorkerForProject } from "../../db/auto-workers.js";
15
+ import { isDbAvailable } from "../../gsd-db.js";
16
+ import { normalizeRealPath } from "../../paths.js";
17
+ import { logWarning } from "../../workflow-logger.js";
13
18
  import type { GSDState } from "../../types.js";
14
19
  import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
15
20
 
@@ -20,23 +25,46 @@ export function detectStaleWorkerDrift(
20
25
  ctx: DriftContext,
21
26
  ): StaleWorkerDrift[] {
22
27
  const data = readSessionLockData(ctx.basePath);
23
- if (!data) return [];
24
- if (typeof data.pid !== "number") return [];
25
- if (isSessionLockProcessAlive(data)) return [];
26
-
27
- return [
28
- {
29
- kind: "stale-worker",
30
- lockPath: effectiveLockFile(),
31
- pid: data.pid,
32
- },
33
- ];
28
+ if (data && typeof data.pid === "number") {
29
+ return isSessionLockProcessAlive(data)
30
+ ? []
31
+ : [{ kind: "stale-worker", lockPath: effectiveLockFile(), pid: data.pid }];
32
+ }
33
+
34
+ // The lock file is missing or unparseable. It is not the only source of
35
+ // truth: a crashed worker can leave a workers row 'active' with held leases
36
+ // and in-flight dispatches even when its lock file is gone. Fall back to the
37
+ // DB worker registry so that state is still detected and repaired.
38
+ if (isDbAvailable()) {
39
+ try {
40
+ const stale = findStaleWorkerForProject(normalizeRealPath(ctx.basePath));
41
+ if (stale && typeof stale.pid === "number") {
42
+ return [{ kind: "stale-worker", lockPath: effectiveLockFile(), pid: stale.pid }];
43
+ }
44
+ } catch (err) {
45
+ // Best-effort: detection must never throw and abort the reconcile cycle.
46
+ logWarning(
47
+ "reconcile",
48
+ `stale-worker DB fallback detection failed: ${(err as Error).message}`,
49
+ );
50
+ }
51
+ }
52
+
53
+ return [];
34
54
  }
35
55
 
36
56
  export function repairStaleWorker(_record: StaleWorkerDrift, ctx: DriftContext): void {
37
57
  // removeStaleSessionLock is idempotent: it re-reads lock state and is a
38
58
  // no-op when the lock is held by an alive process. Safe under cap=2 retry.
39
59
  removeStaleSessionLock(ctx.basePath);
60
+
61
+ // Removing the lock file alone leaves the DB-side worker state dangling: the
62
+ // dead worker's milestone_leases stay 'held' and its unit_dispatches stay
63
+ // 'running'/'claimed', blocking new claims until the lease TTL expires.
64
+ // clearStaleWorkerLock cancels those dispatches, releases the leases, and
65
+ // marks the worker stopping — the same cleanup the startup crash-recovery
66
+ // path performs. It is DB-gated, idempotent, and best-effort.
67
+ clearStaleWorkerLock(ctx.basePath);
40
68
  }
41
69
 
42
70
  export const staleWorkerHandler: DriftHandler<StaleWorkerDrift> = {