wicked-crew 0.16.11 → 0.16.13

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 (341) hide show
  1. package/README.md +28 -8
  2. package/dist/api/artifact-outline.d.ts +64 -0
  3. package/dist/api/artifact-outline.d.ts.map +1 -0
  4. package/dist/api/artifact-outline.js +254 -0
  5. package/dist/api/artifact-outline.js.map +1 -0
  6. package/dist/api/beneath.d.ts +76 -0
  7. package/dist/api/beneath.d.ts.map +1 -0
  8. package/dist/api/beneath.js +214 -0
  9. package/dist/api/beneath.js.map +1 -0
  10. package/dist/api/capture.d.ts +2 -2
  11. package/dist/api/chat-citations.d.ts +25 -1
  12. package/dist/api/chat-citations.d.ts.map +1 -1
  13. package/dist/api/chat-citations.js +186 -61
  14. package/dist/api/chat-citations.js.map +1 -1
  15. package/dist/api/chat-context.d.ts +236 -0
  16. package/dist/api/chat-context.d.ts.map +1 -0
  17. package/dist/api/chat-context.js +808 -0
  18. package/dist/api/chat-context.js.map +1 -0
  19. package/dist/api/chat-history.d.ts +20 -0
  20. package/dist/api/chat-history.d.ts.map +1 -1
  21. package/dist/api/chat-history.js +46 -1
  22. package/dist/api/chat-history.js.map +1 -1
  23. package/dist/api/chat-proposals.d.ts +534 -0
  24. package/dist/api/chat-proposals.d.ts.map +1 -0
  25. package/dist/api/chat-proposals.js +1562 -0
  26. package/dist/api/chat-proposals.js.map +1 -0
  27. package/dist/api/chat-scope.d.ts +41 -0
  28. package/dist/api/chat-scope.d.ts.map +1 -1
  29. package/dist/api/chat-scope.js +87 -15
  30. package/dist/api/chat-scope.js.map +1 -1
  31. package/dist/api/chat-transcripts.d.ts +27 -2
  32. package/dist/api/chat-transcripts.d.ts.map +1 -1
  33. package/dist/api/chat-transcripts.js +105 -4
  34. package/dist/api/chat-transcripts.js.map +1 -1
  35. package/dist/api/diagnostics.d.ts +15 -0
  36. package/dist/api/diagnostics.d.ts.map +1 -1
  37. package/dist/api/diagnostics.js +56 -0
  38. package/dist/api/diagnostics.js.map +1 -1
  39. package/dist/api/elicitation-cache.d.ts +13 -0
  40. package/dist/api/elicitation-cache.d.ts.map +1 -1
  41. package/dist/api/elicitation-cache.js +12 -0
  42. package/dist/api/elicitation-cache.js.map +1 -1
  43. package/dist/api/eval-sample.d.ts +12 -12
  44. package/dist/api/frame-guard.d.ts +62 -0
  45. package/dist/api/frame-guard.d.ts.map +1 -0
  46. package/dist/api/frame-guard.js +97 -0
  47. package/dist/api/frame-guard.js.map +1 -0
  48. package/dist/api/governance-steering.d.ts +4 -4
  49. package/dist/api/multiscope.d.ts +3 -0
  50. package/dist/api/multiscope.d.ts.map +1 -1
  51. package/dist/api/multiscope.js +13 -0
  52. package/dist/api/multiscope.js.map +1 -1
  53. package/dist/api/needs.d.ts +6 -0
  54. package/dist/api/needs.d.ts.map +1 -1
  55. package/dist/api/needs.js +31 -4
  56. package/dist/api/needs.js.map +1 -1
  57. package/dist/api/product.d.ts.map +1 -1
  58. package/dist/api/product.js +13 -2
  59. package/dist/api/product.js.map +1 -1
  60. package/dist/api/project-aggregates.d.ts +6 -6
  61. package/dist/api/project-aggregates.d.ts.map +1 -1
  62. package/dist/api/project-aggregates.js +16 -24
  63. package/dist/api/project-aggregates.js.map +1 -1
  64. package/dist/api/requirements.d.ts +261 -6
  65. package/dist/api/requirements.d.ts.map +1 -1
  66. package/dist/api/requirements.js +344 -64
  67. package/dist/api/requirements.js.map +1 -1
  68. package/dist/api/routes.d.ts +156 -4
  69. package/dist/api/routes.d.ts.map +1 -1
  70. package/dist/api/routes.js +851 -78
  71. package/dist/api/routes.js.map +1 -1
  72. package/dist/api/run-brief.d.ts +78 -0
  73. package/dist/api/run-brief.d.ts.map +1 -0
  74. package/dist/api/run-brief.js +318 -0
  75. package/dist/api/run-brief.js.map +1 -0
  76. package/dist/api/run-files.d.ts.map +1 -1
  77. package/dist/api/run-files.js +5 -1
  78. package/dist/api/run-files.js.map +1 -1
  79. package/dist/api/run-timing-index.d.ts +27 -0
  80. package/dist/api/run-timing-index.d.ts.map +1 -1
  81. package/dist/api/run-timing-index.js +65 -0
  82. package/dist/api/run-timing-index.js.map +1 -1
  83. package/dist/api/seat-images.d.ts +102 -0
  84. package/dist/api/seat-images.d.ts.map +1 -0
  85. package/dist/api/seat-images.js +333 -0
  86. package/dist/api/seat-images.js.map +1 -0
  87. package/dist/api/server.d.ts +3 -0
  88. package/dist/api/server.d.ts.map +1 -1
  89. package/dist/api/server.js +265 -2
  90. package/dist/api/server.js.map +1 -1
  91. package/dist/api/steering-landing.d.ts +48 -1
  92. package/dist/api/steering-landing.d.ts.map +1 -1
  93. package/dist/api/steering-landing.js +116 -42
  94. package/dist/api/steering-landing.js.map +1 -1
  95. package/dist/api/system-snapshot.d.ts +51 -2
  96. package/dist/api/system-snapshot.d.ts.map +1 -1
  97. package/dist/api/system-snapshot.js +151 -15
  98. package/dist/api/system-snapshot.js.map +1 -1
  99. package/dist/api/testing.d.ts +4 -4
  100. package/dist/api/testing.js +2 -2
  101. package/dist/api/testing.js.map +1 -1
  102. package/dist/apps/spec.d.ts +3009 -0
  103. package/dist/apps/spec.d.ts.map +1 -0
  104. package/dist/apps/spec.js +870 -0
  105. package/dist/apps/spec.js.map +1 -0
  106. package/dist/campaigns/routes.js +1 -1
  107. package/dist/campaigns/routes.js.map +1 -1
  108. package/dist/cli/index.js +3 -3
  109. package/dist/cli/index.js.map +1 -1
  110. package/dist/core/adapter.d.ts +12 -0
  111. package/dist/core/adapter.d.ts.map +1 -1
  112. package/dist/core/adapter.js +36 -1
  113. package/dist/core/adapter.js.map +1 -1
  114. package/dist/core/deliver-text.d.ts.map +1 -1
  115. package/dist/core/deliver-text.js +22 -10
  116. package/dist/core/deliver-text.js.map +1 -1
  117. package/dist/core/estate-mcp-client.d.ts +7 -1
  118. package/dist/core/estate-mcp-client.d.ts.map +1 -1
  119. package/dist/core/estate-mcp-client.js +10 -2
  120. package/dist/core/estate-mcp-client.js.map +1 -1
  121. package/dist/core/exec.d.ts +12 -0
  122. package/dist/core/exec.d.ts.map +1 -1
  123. package/dist/core/exec.js +23 -0
  124. package/dist/core/exec.js.map +1 -1
  125. package/dist/core/reassign-refusal.d.ts.map +1 -1
  126. package/dist/core/reassign-refusal.js +23 -3
  127. package/dist/core/reassign-refusal.js.map +1 -1
  128. package/dist/core/repoPaths.d.ts +32 -7
  129. package/dist/core/repoPaths.d.ts.map +1 -1
  130. package/dist/core/repoPaths.js +56 -9
  131. package/dist/core/repoPaths.js.map +1 -1
  132. package/dist/core/requirements-graph-setting.d.ts +32 -0
  133. package/dist/core/requirements-graph-setting.d.ts.map +1 -0
  134. package/dist/core/requirements-graph-setting.js +84 -0
  135. package/dist/core/requirements-graph-setting.js.map +1 -0
  136. package/dist/core/run-boot-resume.d.ts +4 -1
  137. package/dist/core/run-boot-resume.d.ts.map +1 -1
  138. package/dist/core/run-boot-resume.js +6 -1
  139. package/dist/core/run-boot-resume.js.map +1 -1
  140. package/dist/core/run-identity.d.ts.map +1 -1
  141. package/dist/core/run-identity.js +6 -0
  142. package/dist/core/run-identity.js.map +1 -1
  143. package/dist/core/types.d.ts +14 -0
  144. package/dist/core/types.d.ts.map +1 -1
  145. package/dist/core/types.js.map +1 -1
  146. package/dist/decisions/chat-recorder.d.ts +14 -3
  147. package/dist/decisions/chat-recorder.d.ts.map +1 -1
  148. package/dist/decisions/chat-recorder.js +54 -9
  149. package/dist/decisions/chat-recorder.js.map +1 -1
  150. package/dist/editors/csp.d.ts +24 -4
  151. package/dist/editors/csp.d.ts.map +1 -1
  152. package/dist/editors/csp.js +35 -8
  153. package/dist/editors/csp.js.map +1 -1
  154. package/dist/editors/registry.d.ts +46 -21
  155. package/dist/editors/registry.d.ts.map +1 -1
  156. package/dist/editors/registry.js +13 -2
  157. package/dist/editors/registry.js.map +1 -1
  158. package/dist/editors/routes.d.ts +4 -1
  159. package/dist/editors/routes.d.ts.map +1 -1
  160. package/dist/editors/routes.js +30 -3
  161. package/dist/editors/routes.js.map +1 -1
  162. package/dist/editors/visuals.d.ts +51 -0
  163. package/dist/editors/visuals.d.ts.map +1 -0
  164. package/dist/editors/visuals.js +164 -0
  165. package/dist/editors/visuals.js.map +1 -0
  166. package/dist/editors/webrtc-guard.d.ts +94 -0
  167. package/dist/editors/webrtc-guard.d.ts.map +1 -0
  168. package/dist/editors/webrtc-guard.js +506 -0
  169. package/dist/editors/webrtc-guard.js.map +1 -0
  170. package/dist/inspect/reading.d.ts +110 -0
  171. package/dist/inspect/reading.d.ts.map +1 -0
  172. package/dist/inspect/reading.js +325 -0
  173. package/dist/inspect/reading.js.map +1 -0
  174. package/dist/inspect/routes.d.ts +14 -0
  175. package/dist/inspect/routes.d.ts.map +1 -0
  176. package/dist/inspect/routes.js +44 -0
  177. package/dist/inspect/routes.js.map +1 -0
  178. package/dist/inspect/service.d.ts +161 -0
  179. package/dist/inspect/service.d.ts.map +1 -0
  180. package/dist/inspect/service.js +466 -0
  181. package/dist/inspect/service.js.map +1 -0
  182. package/dist/interactive/bridge-pool.d.ts +11 -2
  183. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  184. package/dist/interactive/bridge-pool.js +10 -1
  185. package/dist/interactive/bridge-pool.js.map +1 -1
  186. package/dist/interactive/doc-binding.d.ts +156 -0
  187. package/dist/interactive/doc-binding.d.ts.map +1 -0
  188. package/dist/interactive/doc-binding.js +277 -0
  189. package/dist/interactive/doc-binding.js.map +1 -0
  190. package/dist/interactive/doc-delete-routes.d.ts +3 -0
  191. package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
  192. package/dist/interactive/doc-delete-routes.js +4 -0
  193. package/dist/interactive/doc-delete-routes.js.map +1 -1
  194. package/dist/interactive/draft-events.d.ts +89 -3
  195. package/dist/interactive/draft-events.d.ts.map +1 -1
  196. package/dist/interactive/draft-events.js +441 -30
  197. package/dist/interactive/draft-events.js.map +1 -1
  198. package/dist/interactive/draft-skill.d.ts +7 -0
  199. package/dist/interactive/draft-skill.d.ts.map +1 -1
  200. package/dist/interactive/draft-skill.js +10 -3
  201. package/dist/interactive/draft-skill.js.map +1 -1
  202. package/dist/interactive/edit-events.d.ts +2 -0
  203. package/dist/interactive/edit-events.d.ts.map +1 -1
  204. package/dist/interactive/edit-events.js +11 -0
  205. package/dist/interactive/edit-events.js.map +1 -1
  206. package/dist/interactive/proposal-create.d.ts +143 -0
  207. package/dist/interactive/proposal-create.d.ts.map +1 -0
  208. package/dist/interactive/proposal-create.js +318 -0
  209. package/dist/interactive/proposal-create.js.map +1 -0
  210. package/dist/interactive/proxy-routes.d.ts +60 -3
  211. package/dist/interactive/proxy-routes.d.ts.map +1 -1
  212. package/dist/interactive/proxy-routes.js +217 -3
  213. package/dist/interactive/proxy-routes.js.map +1 -1
  214. package/dist/interactive/source-packet.d.ts +126 -0
  215. package/dist/interactive/source-packet.d.ts.map +1 -0
  216. package/dist/interactive/source-packet.js +482 -0
  217. package/dist/interactive/source-packet.js.map +1 -0
  218. package/dist/learnings/estate.d.ts +16 -0
  219. package/dist/learnings/estate.d.ts.map +1 -0
  220. package/dist/learnings/estate.js +68 -0
  221. package/dist/learnings/estate.js.map +1 -0
  222. package/dist/learnings/extract.d.ts +56 -0
  223. package/dist/learnings/extract.d.ts.map +1 -0
  224. package/dist/learnings/extract.js +252 -0
  225. package/dist/learnings/extract.js.map +1 -0
  226. package/dist/learnings/recall.d.ts +43 -0
  227. package/dist/learnings/recall.d.ts.map +1 -0
  228. package/dist/learnings/recall.js +104 -0
  229. package/dist/learnings/recall.js.map +1 -0
  230. package/dist/learnings/service.d.ts +183 -0
  231. package/dist/learnings/service.d.ts.map +1 -0
  232. package/dist/learnings/service.js +571 -0
  233. package/dist/learnings/service.js.map +1 -0
  234. package/dist/mcp/ask-server.d.ts.map +1 -1
  235. package/dist/mcp/ask-server.js +37 -4
  236. package/dist/mcp/ask-server.js.map +1 -1
  237. package/dist/mcp/broker.d.ts +12 -1
  238. package/dist/mcp/broker.d.ts.map +1 -1
  239. package/dist/mcp/broker.js +24 -5
  240. package/dist/mcp/broker.js.map +1 -1
  241. package/dist/mcp/invoke.d.ts +26 -2
  242. package/dist/mcp/invoke.d.ts.map +1 -1
  243. package/dist/mcp/invoke.js +51 -5
  244. package/dist/mcp/invoke.js.map +1 -1
  245. package/dist/persona/routes.d.ts +92 -0
  246. package/dist/persona/routes.d.ts.map +1 -0
  247. package/dist/persona/routes.js +69 -0
  248. package/dist/persona/routes.js.map +1 -0
  249. package/dist/persona/store.d.ts +104 -0
  250. package/dist/persona/store.d.ts.map +1 -0
  251. package/dist/persona/store.js +176 -0
  252. package/dist/persona/store.js.map +1 -0
  253. package/dist/persona/voice.d.ts +4 -0
  254. package/dist/persona/voice.d.ts.map +1 -0
  255. package/dist/persona/voice.js +58 -0
  256. package/dist/persona/voice.js.map +1 -0
  257. package/dist/projects/routes.d.ts +2 -2
  258. package/dist/setup/known.d.ts +56 -0
  259. package/dist/setup/known.d.ts.map +1 -0
  260. package/dist/setup/known.js +159 -0
  261. package/dist/setup/known.js.map +1 -0
  262. package/dist/setup/routes.d.ts +13 -0
  263. package/dist/setup/routes.d.ts.map +1 -0
  264. package/dist/setup/routes.js +20 -0
  265. package/dist/setup/routes.js.map +1 -0
  266. package/dist/standing-orders/history.d.ts +2 -0
  267. package/dist/standing-orders/history.d.ts.map +1 -1
  268. package/dist/standing-orders/history.js +1 -0
  269. package/dist/standing-orders/history.js.map +1 -1
  270. package/dist/studio/assets/WickedSans-Regular-fb698rcD.woff2 +0 -0
  271. package/dist/studio/assets/WickedSans-SemiBold-CD53n1AC.woff2 +0 -0
  272. package/dist/studio/assets/WickedScript-Regular-CY5juO6I.woff2 +0 -0
  273. package/dist/studio/assets/WickedSerif-Regular-mdPEjaVZ.woff2 +0 -0
  274. package/dist/studio/assets/index-BUZ_-Syk.css +32 -0
  275. package/dist/studio/assets/index-u4O_7N-b.js +986 -0
  276. package/dist/studio/editors/wicked-deck/editor.json +6 -1
  277. package/dist/studio/editors/wicked-deck/icon.svg +1 -0
  278. package/dist/studio/editors/wicked-deck/index.html +1 -1
  279. package/dist/studio/editors/wicked-doc/editor.json +6 -1
  280. package/dist/studio/editors/wicked-doc/icon.svg +1 -0
  281. package/dist/studio/editors/wicked-doc/index.html +1 -1
  282. package/dist/studio/editors/wicked-insights/editor.json +6 -1
  283. package/dist/studio/editors/wicked-insights/icon.svg +1 -0
  284. package/dist/studio/editors/wicked-insights/index.html +1 -1
  285. package/dist/studio/editors/wicked-page/editor.json +6 -1
  286. package/dist/studio/editors/wicked-page/icon.svg +1 -0
  287. package/dist/studio/editors/wicked-page/index.html +1 -1
  288. package/dist/studio/editors/wicked-video/editor.json +6 -1
  289. package/dist/studio/editors/wicked-video/icon.svg +1 -0
  290. package/dist/studio/editors/wicked-video/index.html +1 -1
  291. package/dist/studio/index.html +2 -2
  292. package/dist/studio/testid-inventory.json +5155 -2707
  293. package/dist/voice/dashboard.d.ts +376 -0
  294. package/dist/voice/dashboard.d.ts.map +1 -0
  295. package/dist/voice/dashboard.js +92 -0
  296. package/dist/voice/dashboard.js.map +1 -0
  297. package/dist/voice/inject-input.d.ts +51 -0
  298. package/dist/voice/inject-input.d.ts.map +1 -0
  299. package/dist/voice/inject-input.js +126 -0
  300. package/dist/voice/inject-input.js.map +1 -0
  301. package/dist/voice/routes.d.ts +41 -0
  302. package/dist/voice/routes.d.ts.map +1 -0
  303. package/dist/voice/routes.js +116 -0
  304. package/dist/voice/routes.js.map +1 -0
  305. package/dist/voice/service.d.ts +177 -0
  306. package/dist/voice/service.d.ts.map +1 -0
  307. package/dist/voice/service.js +400 -0
  308. package/dist/voice/service.js.map +1 -0
  309. package/dist/workflow-catalog/build.d.ts +35 -0
  310. package/dist/workflow-catalog/build.d.ts.map +1 -0
  311. package/dist/workflow-catalog/build.js +229 -0
  312. package/dist/workflow-catalog/build.js.map +1 -0
  313. package/dist/workflow-catalog/catalog-data.d.ts +36 -0
  314. package/dist/workflow-catalog/catalog-data.d.ts.map +1 -0
  315. package/dist/workflow-catalog/catalog-data.js +430 -0
  316. package/dist/workflow-catalog/catalog-data.js.map +1 -0
  317. package/dist/working-doc/event-lines.d.ts +76 -0
  318. package/dist/working-doc/event-lines.d.ts.map +1 -0
  319. package/dist/working-doc/event-lines.js +247 -0
  320. package/dist/working-doc/event-lines.js.map +1 -0
  321. package/dist/working-doc/routes.d.ts +1 -1
  322. package/dist/working-doc/routes.d.ts.map +1 -1
  323. package/dist/working-doc/routes.js +2 -1
  324. package/dist/working-doc/routes.js.map +1 -1
  325. package/dist/working-doc/sections.d.ts +3 -1
  326. package/dist/working-doc/sections.d.ts.map +1 -1
  327. package/dist/working-doc/sections.js +4 -2
  328. package/dist/working-doc/sections.js.map +1 -1
  329. package/dist/working-doc/service.d.ts +66 -1
  330. package/dist/working-doc/service.d.ts.map +1 -1
  331. package/dist/working-doc/service.js +298 -22
  332. package/dist/working-doc/service.js.map +1 -1
  333. package/dist/working-doc/store.d.ts +8 -2
  334. package/dist/working-doc/store.d.ts.map +1 -1
  335. package/dist/working-doc/store.js.map +1 -1
  336. package/endpoint-manifest.json +205 -2
  337. package/package.json +5 -4
  338. package/workflows/distill-learnings.json +27 -0
  339. package/workflows/inspect-unit.json +27 -0
  340. package/dist/studio/assets/index-B-f_VEL2.css +0 -32
  341. package/dist/studio/assets/index-DhkxIK-0.js +0 -845
@@ -3,40 +3,58 @@ import { busSeamsOffWarning, busUnavailableWarning } from '../core/bus-notice.js
3
3
  import { vaultBinWarning, vaultDiagnostics } from '../core/vault-bin.js';
4
4
  import { PlanSchema, toLaunchPlan } from './plan-schema.js';
5
5
  import { z } from 'zod';
6
- import { listRequirements, getRequirement, patchRequirement } from './requirements.js';
6
+ import { listRequirementsRead, getRequirement, patchRequirement, readRequirementsGraph, REQUIREMENTS_NOT_GENERATED, } from './requirements.js';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { homedir } from 'node:os';
9
- import { readFileSync, existsSync, mkdirSync } from 'node:fs';
9
+ import { readFileSync, existsSync, mkdirSync, realpathSync } from 'node:fs';
10
10
  import { promises as fsp } from 'node:fs';
11
- import { isAbsolute, join, relative, resolve } from 'node:path';
11
+ import { basename, isAbsolute, join, relative, resolve } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { videoPalette } from '../appearance/video-palette.js';
14
14
  import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, GovernanceReplayUnsupportedError, PlanLaunchUnsupportedError, SteeringUnsupportedError, addonSupportsQeOverride, addonSupportsReducedAssurance, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
15
- import { codeGraphDb, codeGraphErrorStatus, requirementsGraph } from '../core/repoPaths.js';
15
+ import { codeGraphDb, codeGraphErrorStatus, repoFolderMissing, repoFolderMissingBody } from '../core/repoPaths.js';
16
16
  import { resolveCursorUnit } from '../core/cursor.js';
17
17
  /** Create `dir` (private) when missing and answer it — a read root must exist when a run launches. */
18
18
  function ensureDir(dir) {
19
19
  mkdirSync(dir, { recursive: true, mode: 0o700 });
20
20
  return dir;
21
21
  }
22
+ /** DES-CHAT-ARTIFACTS-001 §5.1: the chat's `open/` and `context/` read roots, made and proven real beneath its scratch
23
+ * root, as the seat sees them; none where the platform cannot open beneath a root, or when they cannot be made. */
24
+ function turnReadRoots(scratchRoot) {
25
+ if (!beneathSupported())
26
+ return [];
27
+ try {
28
+ ensureRealDir(contextReadRoots(scratchRoot).open);
29
+ ensureRealDir(contextReadRoots(scratchRoot).context);
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ const real = seatPathOf(scratchRoot);
35
+ return [join(real, 'open'), join(real, 'context')];
36
+ }
22
37
  import { detectRefusal, isOpenGate, offeredChoices, replayOpenGate } from './gate-cache.js';
23
38
  import { acceptanceRequirementOf, buildAcceptanceView } from '../qe/acceptance.js';
24
39
  import { runIdentityOf, wireIdentity } from '../core/run-identity.js';
40
+ import { buildWorkflowCatalog } from '../workflow-catalog/build.js';
25
41
  import { buildEvidenceBundle, coreUnitId, evidenceFilename } from './evidence.js';
26
42
  import { attemptUnavailableReason, capturedAttempts, outputUnavailableReason, resolveUnit, storedAttempt, unitKeysFor } from './unit-output.js';
27
43
  import { execCapped, ExecOutputTooLarge } from '../core/exec.js';
28
- import { callEstateTool, EstateMcpError, memoryStoreInfo } from '../core/estate-mcp-client.js';
44
+ import { callEstateTool, EstateMcpError, ESTATE_INSTALL_HINT, memoryStoreInfo } from '../core/estate-mcp-client.js';
45
+ import { NO_LEARNINGS_WORKFLOWS } from '../learnings/service.js';
29
46
  import { SeatHealthTracker } from './seat-health.js';
30
47
  import { applyWorkerConfigRoot, signedInHeuristic } from './seat-signin.js';
31
48
  import { applyCouncilSettings, isCouncilSize, isCouncilStaggerMs } from '../core/council-settings.js';
49
+ import { applyRequirementsGraphSettings, isRequirementsGraphSettings, REQUIREMENTS_GRAPH_SETTINGS_RULE, } from '../core/requirements-graph-setting.js';
32
50
  import { defaultLaunchRoster, markOptIn } from '../core/opt-in-seats.js';
33
- import { chatSeatAdmission } from './seat-standing.js';
51
+ import { chatSeatAdmission, homeless } from './seat-standing.js';
34
52
  import { seatGovernanceDiagnostics, seatGovernanceWarnings, } from './seat-governance.js';
35
53
  import { seatWorkDefaultNow } from './seat-work-default.js';
36
54
  import { ROSTER_REFRESH_WAIT_MS, rosterWithStandingFactory } from './roster-standing.js';
37
55
  import { SeatContainerStatus, gatewayDirOf, isContainerLoginCli, isContainerSeatFloor, isHostSeatFloor, loginDetail, runSeatLogin, seatLoginDepsFor, } from './seat-container-login.js';
38
56
  import { ChatTurnIndex } from './chat-turns.js';
39
- import { ChatScopeIndex, chatScopeDeps, chatScopeStatement, prepareChatScratch, removeChatScratch, namedKindRefusal, resolveChatScope, } from './chat-scope.js';
57
+ import { CHAT_ID, chatMakeDigest, ChatScopeIndex, chatScopeDeps, chatScopeStatement, prepareChatScratch, removeChatScratch, namedKindRefusal, resolveChatScope, } from './chat-scope.js';
40
58
  import { ASK_TURN_BUDGET_SECS, AskPathIndex, isAskTurn } from './ask-paths.js';
41
59
  import { chatTurnStep, resolveChatWorkflow, servedChatWorkflows, writeCapableStep } from './chat-workflow.js';
42
60
  import { reassignRefusedSeats } from '../core/reassign-refusal.js';
@@ -55,7 +73,7 @@ import { registerTeamRoutes } from '../team/routes.js';
55
73
  import { registerCampaignRoutes } from '../campaigns/routes.js';
56
74
  import { registerGovernanceWikiRoutes } from './governance-wiki.js';
57
75
  import { DEFAULT_STEERING_TYPE, STEERING_TYPE_VALUES, STEERING_TYPES, registerGovernanceSteeringRoutes, } from './governance-steering.js';
58
- import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js';
76
+ import { atProposeGate, draftedSteeringRules, isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js';
59
77
  import { inventoryComplete, parseInventoryClaims } from '../core/inventory-claim.js';
60
78
  import { ApproveEditSchema, approveEdited, captureLaunchRoots, editInFlight, FileProposalSchema, registerCaptureRoutes } from './capture.js';
61
79
  import { registerTestingRoutes } from './testing.js';
@@ -86,21 +104,31 @@ import { composeDeliverText, configuredPublicOrigin, extractFollowUps, factsFrom
86
104
  import { deliverIdentityFor, deliverRepoFor, deliverTargetView, isGitHubLogin, pushIdentityOf, readDeliverOriginUrl, resolvePullRequest as resolvePullRequestViaGh, unverifiedTreesFrom, } from '../core/deliver.js';
87
105
  import { credentialsMissingIn, deliverCredentialsProbe, missingCredentials } from '../core/deliver-credentials.js';
88
106
  import { codebaseArchiveUrl } from './codebase-archive.js';
89
- import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
107
+ import { projectRepoCandidates } from '../interactive/doc-grounding.js';
108
+ import { artifactStatus, beneathSupported, buildTurnContext, chatMount, ChatMessageContextSchema, contextReadRoots, ensureRealDir, sha256Hex, parseArtifactRef, readGroundingBeneath, seatPathOf, UNFILED_MOUNT, } from './chat-context.js';
109
+ import { createDocInProcess, registerInteractiveProxy } from '../interactive/proxy-routes.js';
110
+ import { CreateLauncher } from '../interactive/proposal-create.js';
111
+ import { buildRunBrief } from './run-brief.js';
112
+ import { readBeneath } from './beneath.js';
113
+ /** A context brief copied into a document's packet, at most (the packet's per-file cap). */
114
+ const BRIEF_COPY_MAX = 512 * 1024;
90
115
  import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
91
116
  import { registerInteractiveDocChecks } from '../interactive/doc-checks-routes.js';
92
117
  import { crewStateHome, stateHomeOfDb } from '../projects/state-home.js';
118
+ import { ChatProposals, LaunchOnceRegistry, RecordingStubDispatcher, runCostFromEvents } from './chat-proposals.js';
119
+ import { CATALOG_SEEDS } from '../workflow-catalog/catalog-data.js';
93
120
  import { registerInteractiveDocList } from '../interactive/doc-list-routes.js';
94
121
  import { REVIEWS_DIRNAME } from '../interactive/review-ledger.js';
95
122
  import { MembershipIndex } from '../projects/membership-index.js';
96
123
  import { MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
97
124
  import { resolveLinkedIssues } from '../core/linked-issues.js';
98
125
  import { AuditLog } from './audit.js';
99
- import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion, hostSandboxLauncher, seatRosterFile, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
126
+ import { AcpFoldCache, EngineVersionCache, FloorCacheCache, eventsDirOf, installedPackageVersion, hostSandboxLauncher, seatRosterFile, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
100
127
  import { GovernanceDiagnostics } from './governance-health.js';
101
128
  import { ATTACHMENT_LIMITS, ATTACHMENT_MAX_MESSAGE_BYTES, AttachmentError, ChatAttachmentStore, attachmentWire, attachmentsBlock, boundaryOf, parseMultipart, stageForSeat } from './chat-attachments.js';
102
129
  import { CHAT_HISTORY_MAX_LIMIT, ChatHistoryStore, decodeCursor } from './chat-history.js';
103
- import { PROBE_TIMEOUT_MS, SYSTEM_BUDGET_MS, SYSTEM_STATS_DAYS, SYSTEM_STATS_TTL_MS, WeekFoldCache, assembleSnapshot, buildNeeds, buildClis, buildGovernance, diskRow, estateReadonlyCall, infraRow, judgeGaps, livenessThresholds, loadRow, probeBroker, probeBus, probeEstate, probeGateway, probeInteractive, probeVault, readBridgeLockAsync, readWeek, resolveContainerRuntime, runLiveness, settleWithin, skillsRow, sortRuns, stateHomeRow, unfinished, } from './system-snapshot.js';
130
+ import { PROBE_TIMEOUT_MS, SYSTEM_BUDGET_MS, SYSTEM_STATS_DAYS, SYSTEM_STATS_TTL_MS, WeekFoldCache, assembleSnapshot, buildNeeds, buildClis, buildGovernance, diskRow, estateReadonlyCall, infraRow, judgeGaps, livenessThresholds, loadRow, probeBroker, probeBus, probeEstate, probeGateway, probeInteractive, probeVault, readBridgeLockAsync, readWeek, resolveContainerRuntime, runLiveness, settleWithin, skillsRow, sortRuns, stateHomeRow, reposRow, unfinished, } from './system-snapshot.js';
131
+ import { parseSeatImageStatus, probeSeatImages, SeatImageRebuilder } from './seat-images.js';
104
132
  import { RunCostCache } from './needs.js';
105
133
  import { governanceStoreFix, legacyHomeOutboxPath } from '../core/governance-store.js';
106
134
  import { UsageError, replayOutboxInto } from '../cli/governance.js';
@@ -109,6 +137,10 @@ import { RetryIndex } from './retry-index.js';
109
137
  import { SEAT_RECORD_DEFAULT_DAYS, SEAT_RECORD_MAX_DAYS, seatRecord } from './seat-record.js';
110
138
  import { jobTitle } from './job-title.js';
111
139
  import { DeliveryFreeze, frozenRefusal, isDeliverUnit, registerDeliveryFreezeRoutes } from './delivery-freeze.js';
140
+ import { VoiceService, defaultVoiceDir } from '../voice/service.js';
141
+ import { VoiceDashboardStore } from '../voice/dashboard.js';
142
+ import { registerVoiceRoutes } from '../voice/routes.js';
143
+ import { VoiceInjectInputs } from '../voice/inject-input.js';
112
144
  import { GroupIndex } from './group-index.js';
113
145
  import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
114
146
  import { GuidanceIndex } from './guidance-index.js';
@@ -173,6 +205,18 @@ function codeGraphDbOr503(repo, reply) {
173
205
  return null;
174
206
  }
175
207
  }
208
+ /**
209
+ * `true` after answering **409** when the repo's registered folder is gone, BEFORE anything spawns
210
+ * there: Node reports a missing spawn `cwd` as `spawn <binary> ENOENT`, which named an installed
211
+ * binary as the fault (rig, tiny-math). The words are the finding's, the same everywhere.
212
+ */
213
+ function refusedMissingFolder(repo, reply) {
214
+ const missing = repoFolderMissing(repo);
215
+ if (missing === null)
216
+ return false;
217
+ void reply.code(409).send(repoFolderMissingBody(missing));
218
+ return true;
219
+ }
176
220
  function message(err) {
177
221
  return err instanceof Error ? err.message : String(err);
178
222
  }
@@ -588,7 +632,15 @@ export const GateSchema = z.object({
588
632
  /** api-types 0.44.0 — the gate this decision answers (its unit `ord`). A mismatch with the run's
589
633
  * open gate is a 409 `gate_changed`: the decision was made on a gate that is no longer open. */
590
634
  ord: z.number().int().nonnegative().optional(),
591
- }).strict().refine((b) => b.action === undefined
635
+ /** (api-types 0.156.0, DES-VOICE) The person answered by voice and confirmed it on screen: recorded on the
636
+ * `gate.decided` audit line and the decided-gate history ("by voice, confirmed"). It grants nothing. */
637
+ input: z.literal('voice').optional(),
638
+ /** (program DES-LEARN §3.1, api-types 0.160.0) On a steering-author propose gate: the drafted rule ids
639
+ * (`GET /governance/steering/author/:runId/draft`) the person keeps. Only those land; the rest are
640
+ * declined. Absent = every drafted rule lands, as before. 409 `no_draft` unless the run waits at its
641
+ * steering-author propose gate with a readable draft; 400 with anything but a plain approve. */
642
+ keepRules: z.array(z.string().min(1).max(256)).max(500).optional(),
643
+ }).strict().refine((b) => b.keepRules === undefined || (b.approve && (b.action === undefined || b.action === 'approve') && b.plan === undefined), { message: 'keepRules goes with a plain approve (no action other than approve, no plan) of a steering-author propose gate', path: ['keepRules'] }).refine((b) => b.action === undefined
592
644
  || (b.action === 'approve' || b.action === 'edit_plan' || b.action === 'amend_intent' || b.action === OPERATOR_JUDGMENT_ACTION || b.action === SKIP_QE_ACTION || isEscalationAction(b.action)) === b.approve, { message: '`action` disagrees with `approve`: request_changes and reject require approve: false; approve, edit_plan, amend_intent, operator_judgment, skip_qe, extend, targeted, accept_partial, accept_suggestion, rejudge and consent:<id> require approve: true', path: ['action'] }).refine(
593
645
  // (operator ruling 2026-10-21) The override's rationale IS its record: a blank one is a 400 here,
594
646
  // before the engine is asked.
@@ -607,10 +659,19 @@ export const GuidanceSchema = z.object({
607
659
  }).strict();
608
660
  /** ~8KB — a guidance note is operator prose, not a document store. */
609
661
  const GUIDANCE_MAX_BYTES = 8192;
662
+ /** `POST /runs/:id/cancel` (api-types `CancelRunRequest`): optional, so a bodiless cancel stays valid. */
663
+ const CancelSchema = z.object({
664
+ reason: z.string().trim().max(2000).optional(),
665
+ input: z.literal('voice').optional(),
666
+ }).strict();
610
667
  const InjectSchema = z.object({
611
668
  message: z.string().min(1),
612
669
  /** `"all"` broadcasts to every active worker; any other value is a CLI key. */
613
670
  target: z.string().min(1).default('all'),
671
+ /** (api-types 0.161.0, DES-VOICE) The person said this note and confirmed it on screen. A person's only (403 for
672
+ * any other actor); kept on the `run.injected` audit line and stamped on the note's queued/injected frames so the
673
+ * thread says "You (by voice)". It grants nothing a typed note does not. */
674
+ input: z.literal('voice').optional(),
614
675
  }).strict();
615
676
  // The manual reassign lever (crew#442): `cli` omitted lets the engine's council re-pick, the
616
677
  // same shape the automatic stall-watchdog escalation uses for an unknown failover target.
@@ -740,6 +801,9 @@ export const ChatOpenSchema = z.object({
740
801
  /** (api-types 0.141.0, crew#1157) The read-only workflow every turn runs (`analyze`); see
741
802
  * `chat-workflow.ts`. Omitted ⇒ a plain chat. */
742
803
  workflow: z.string().min(1).max(64).optional(),
804
+ /** (api-types 0.169.0, DES-CHAT-ARTIFACTS-001 §6.3) "Ask about this" on a document no open chat holds: seed the
805
+ * chat's repositories from the document's recorded binding, after an access check. */
806
+ seedFromArtifact: z.string().max(200).optional(),
743
807
  }).strict();
744
808
  /**
745
809
  * The `singleSeat` disclosure (crew#641, hole closed in crew#650) — built in ONE place so the 201
@@ -837,6 +901,17 @@ runtime = {}) {
837
901
  const seatHealth = runtime.seatHealth ?? new SeatHealthTracker();
838
902
  const deliveryFreeze = runtime.deliveryFreeze ?? new DeliveryFreeze();
839
903
  registerDeliveryFreezeRoutes(app, { freeze: deliveryFreeze, audit });
904
+ const voice = runtime.voice ?? {
905
+ service: new VoiceService({ dir: defaultVoiceDir() }),
906
+ dashboard: (() => { try {
907
+ return VoiceDashboardStore.under(crewStateHome());
908
+ }
909
+ catch {
910
+ return null;
911
+ } })(),
912
+ };
913
+ registerVoiceRoutes(app, { voice: voice.service, dashboard: voice.dashboard, audit });
914
+ const voiceInputs = runtime.voiceInputs ?? new VoiceInjectInputs();
840
915
  const openWithOs = runtime.openWithOs ?? openWithSystemDefault;
841
916
  const signedIn = runtime.signedIn ?? signedInHeuristic;
842
917
  const retryIndex = runtime.retryIndex ?? new RetryIndex();
@@ -845,10 +920,35 @@ runtime = {}) {
845
920
  const guidanceIndex = runtime.guidanceIndex ?? new GuidanceIndex();
846
921
  const chatScopes = runtime.chatScopes ?? new ChatScopeIndex();
847
922
  const chatTurns = runtime.chatTurns ?? new ChatTurnIndex();
923
+ // DES-LEARN §3.3: the voice a chat in this project is told (the project's override, else the person's).
924
+ const voiceFor = (projectId) => {
925
+ const v = runtime.personaVoice?.(projectId ?? null) ?? null;
926
+ return v === null ? {} : { voice: v };
927
+ };
848
928
  const askPaths = runtime.askPaths ?? new AskPathIndex();
849
929
  const chatTranscripts = runtime.chatTranscripts;
850
930
  const chatAttachments = runtime.chatAttachments ?? new ChatAttachmentStore();
851
931
  const chatHistory = runtime.chatHistory ?? (chatTranscripts !== undefined ? new ChatHistoryStore(chatTranscripts, chatAttachments) : null);
932
+ // DES-CHAT-ARTIFACTS-001 §14 (A2): what a chat can do with artifacts. Production: `proposals` on and every operation
933
+ // off — each later slice turns its own on and registers its launcher. The TEST configuration (§13 A2) turns every
934
+ // operation's resolution on and registers a recording stub that starts nothing.
935
+ const chatArtifactsStub = runtime.chatArtifactsStub === true;
936
+ // A3: a real `create` exists whenever the draft seam can launch a proposal-bound draft (the launcher is registered
937
+ // below, once the bridge pool exists). Production advertises it; the test configuration keeps the recording stub only
938
+ // for the operations whose slices have not landed.
939
+ const realCreate = runtime.docBindings !== undefined && runtime.launchBoundDraft !== undefined;
940
+ const stubOps = realCreate ? ['refine', 'theme', 'demo'] : ['create', 'refine', 'theme', 'demo'];
941
+ const chatArtifactFlags = chatArtifactsStub
942
+ ? { context: true, proposals: true, create: true, refine: true, theme: true, demo: true, stub: true, stubOps }
943
+ : { context: true, proposals: true, create: realCreate, refine: false, theme: false, demo: false };
944
+ const proposalLaunchers = new LaunchOnceRegistry();
945
+ if (chatArtifactsStub) {
946
+ // Under the registered `chats` entry (a new top-level state-home entry would make core refuse every launch), and
947
+ // not a `.jsonl` there (the boot sweep clears orphaned transcripts by that suffix).
948
+ const stub = new RecordingStubDispatcher(join(crewStateHome(), 'chats', 'proposal-stub', 'dispatches.ndjson'));
949
+ for (const kind of stubOps)
950
+ proposalLaunchers.register(kind, stub);
951
+ }
852
952
  const deliveryIndex = runtime.deliveryIndex ?? new DeliveryIndex();
853
953
  // Wave 6: the doc↔run binding and the registered test sets — `createServer` injects the real
854
954
  // ones; a directly-driven route set gets an empty index (every run `document_id: null`, no sets).
@@ -1157,6 +1257,15 @@ runtime = {}) {
1157
1257
  const chatId = runTimingIndex.chatIdFor(view.session.id);
1158
1258
  if (chatId !== undefined)
1159
1259
  view.session.chat_id = chatId;
1260
+ // DES-CHAT-ARTIFACTS-001 (A3): the proposal a run answers, the artifact it makes, and a landing that failed.
1261
+ const proposalOf = runTimingIndex.proposalOf(view.session.id);
1262
+ if (proposalOf !== undefined) {
1263
+ view.session.proposal_id = proposalOf.proposalId;
1264
+ view.session.artifact_ref = proposalOf.artifactRef;
1265
+ }
1266
+ const landingFailed = runTimingIndex.landingFailedOf(view.session.id);
1267
+ if (landingFailed !== undefined)
1268
+ view.session.landing_failed = landingFailed;
1160
1269
  // crew#552: the onboarding run a chained capture was launched from — ABSENT otherwise.
1161
1270
  const chainedFrom = runTimingIndex.chainedFromOf(view.session.id);
1162
1271
  if (chainedFrom !== undefined)
@@ -1325,6 +1434,15 @@ runtime = {}) {
1325
1434
  workingDoc: app.hasRoute({ method: 'GET', url: `${V}/runs/:id/working-doc` }),
1326
1435
  // (api-types 0.149.0) Power mode: the routes are served AND the engine runs it.
1327
1436
  powerMode: powerSupported(),
1437
+ // (api-types 0.156.0, DES-VOICE) Voice mode's routes are served: local speech to text, the model with
1438
+ // consent, and the per-person dashboard. Whether transcription can run now is `GET /voice/status`.
1439
+ voice: app.hasRoute({ method: 'POST', url: `${V}/voice/transcribe` }),
1440
+ // (api-types 0.168.0, wf-catalog) The categorised workflow catalog with the operator's usage.
1441
+ workflowCatalog: app.hasRoute({ method: 'GET', url: `${V}/workflows/catalog` }),
1442
+ // (api-types 0.169.0, DES-CHAT-ARTIFACTS-001 §14) Per operation: A1 serves `context` (the chat sees the open
1443
+ // artifact and the runs the person names; `seedFromArtifact`); A2 (0.170.0) `proposals`. The operations are later
1444
+ // slices (all on, with `stub`, in the test configuration).
1445
+ chatArtifacts: { ...chatArtifactFlags },
1328
1446
  };
1329
1447
  // wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
1330
1448
  // daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
@@ -1359,6 +1477,7 @@ runtime = {}) {
1359
1477
  // can never turn this GET into an event-log re-reader or a `--version` spawner per request.
1360
1478
  const acpFoldCache = new AcpFoldCache();
1361
1479
  const engineVersionCache = new EngineVersionCache();
1480
+ const floorCacheCache = new FloorCacheCache();
1362
1481
  // The governance block (crew#495): the store the engine's emit seam writes to (as the adapter
1363
1482
  // exported it — `null` on a boot that resolved none), its record count through the engine
1364
1483
  // binding when the addon has one, and the dead-letter outbox folded on change. Built at boot so
@@ -1370,13 +1489,14 @@ runtime = {}) {
1370
1489
  // `dbPath` is a readonly field of the real adapter; a stub-driven route set may lack it,
1371
1490
  // and diagnostics over an unknown store honestly reports no stores and no ACP record.
1372
1491
  const dbPath = typeof adapter.dbPath === 'string' && adapter.dbPath !== '' ? adapter.dbPath : null;
1373
- const [stores, byCli, engineBinaries, governance] = await Promise.all([
1492
+ const [stores, byCli, engineBinaries, governance, floorCache] = await Promise.all([
1374
1493
  dbPath !== null ? listStoreFiles(dbPath) : Promise.resolve([]),
1375
1494
  dbPath !== null
1376
1495
  ? acpFoldCache.get(eventsDirOf(dbPath))
1377
1496
  : Promise.resolve({}),
1378
1497
  engineVersionCache.get(),
1379
1498
  governanceDiagnostics.health(),
1499
+ floorCacheCache.get(),
1380
1500
  ]);
1381
1501
  const uptimeMs = Math.round(process.uptime() * 1000);
1382
1502
  const addr = app.server.address();
@@ -1423,6 +1543,8 @@ runtime = {}) {
1423
1543
  // crew#1068: the wicked-vault CLI handed to the engine, with a remedy when it has none.
1424
1544
  // `null` on a route set driven without the real adapter.
1425
1545
  vault: adapter.vaultBin != null ? vaultDiagnostics(adapter.vaultBin) : null,
1546
+ // crew#1147: the repo-checks floor cache's disk usage (cached 60 s); `null` on an addon without it.
1547
+ floorCache,
1426
1548
  };
1427
1549
  };
1428
1550
  app.get(`${V}/diagnostics`, { config: { manifest: { responseType: 'DiagnosticsResponse', statusCodes: [200] } } }, async () => diagnosticsNow());
@@ -1948,6 +2070,8 @@ runtime = {}) {
1948
2070
  const repo = repos.find((r) => r.id === id);
1949
2071
  if (!repo)
1950
2072
  return reply.code(404).send({ error: `Repo ${id} not found` });
2073
+ if (refusedMissingFolder(repo, reply))
2074
+ return reply;
1951
2075
  const body = OnboardBodySchema.safeParse(req.body ?? {});
1952
2076
  if (!body.success)
1953
2077
  return reply.code(400).send(invalidBody(body.error, 'Invalid request body'));
@@ -2039,6 +2163,20 @@ runtime = {}) {
2039
2163
  const garden = gardenRefusalBody();
2040
2164
  if (garden !== null)
2041
2165
  return reply.code(422).send(garden);
2166
+ // A run on a repo whose folder is gone could only fail at its first worktree or floor, with the
2167
+ // spawn's ENOENT for a reason: refused here by name, before the engine sees it.
2168
+ // A registry that cannot be read here refuses nothing: the engine judges the ref as before.
2169
+ if (b.repoRef !== undefined) {
2170
+ let target;
2171
+ try {
2172
+ target = (await adapter.listRepos()).find((r) => r.id === b.repoRef);
2173
+ }
2174
+ catch {
2175
+ target = undefined;
2176
+ }
2177
+ if (target !== undefined && refusedMissingFolder(target, reply))
2178
+ return reply;
2179
+ }
2042
2180
  // crew#645: an expired login reads signed out BEFORE routing — wait (bounded) for every seat's
2043
2181
  // missing or stale login check, so the standing below is the seats' own answer.
2044
2182
  if (b.clisJson === undefined)
@@ -2198,6 +2336,12 @@ runtime = {}) {
2198
2336
  else {
2199
2337
  input.power = true;
2200
2338
  input.humanConfirm = 'none';
2339
+ // DES-LEARN §3.3 / wicked-core#1216: the orchestrator talks to the person in their personality
2340
+ // for this project (the project's override, else theirs). The engine hands it to the
2341
+ // orchestrator only; reviewers, judges and QE never hear it.
2342
+ const { voice } = voiceFor(b.projectId);
2343
+ if (voice !== undefined)
2344
+ input.voice = voice;
2201
2345
  }
2202
2346
  }
2203
2347
  // QE-IN-APP-WORKFLOWS: the operator's explicit skip (with its reason) or force — forwarded only
@@ -2289,6 +2433,20 @@ runtime = {}) {
2289
2433
  catch (err) {
2290
2434
  req.log.warn(`linked-issue resolution failed; launching on the intent alone: ${message(err)}`);
2291
2435
  }
2436
+ // Run learnings: the approved learnings that apply to this repo/project ride in the brief, in a
2437
+ // marked block the deliver text takes back out (src/learnings/recall.ts). Workflow launches only,
2438
+ // for the linked-issues reason above; best-effort: an unreadable store launches without it.
2439
+ if (runtime.recallLearnings !== undefined && !NO_LEARNINGS_WORKFLOWS.has(b.workflow)) {
2440
+ try {
2441
+ const recalled = await runtime.recallLearnings(input.problem, b.repoRef ?? null, b.projectId ?? null);
2442
+ input.problem = recalled.problem;
2443
+ if (recalled.recalled > 0)
2444
+ req.log.info(`[learnings] ${input.sessionId}: ${recalled.recalled} approved learning(s) in the brief`);
2445
+ }
2446
+ catch (err) {
2447
+ req.log.warn(`[learnings] the memory store could not be read; launching without approved learnings: ${message(err)}`);
2448
+ }
2449
+ }
2292
2450
  }
2293
2451
  // DES-RUNS-REVIEW-001 §3: a hand launch of the run review (`/workflow-review-recent-work`) is prepared like a
2294
2452
  // scheduled one: its snapshot written, repo-less, one seat, its review directory as the only write root.
@@ -2313,6 +2471,13 @@ runtime = {}) {
2313
2471
  }
2314
2472
  deliver = 'none';
2315
2473
  }
2474
+ // DES-CHAT-ARTIFACTS-001 §4.3 (A3): one live job per chat, work and artifact runs together. A work launch filed under
2475
+ // a chat whose proposal's document attempt has no outcome yet is refused, as Go is refused the other way round.
2476
+ if (b.chatId !== undefined && chatArtifactFlags.proposals) {
2477
+ const live = await chatProposals.liveJobOf(b.chatId).catch(() => null);
2478
+ if (live !== null)
2479
+ return reply.code(409).send({ code: 'chat_busy', error: `This chat already has a job running (${live}). Wait, or stop that run.` });
2480
+ }
2316
2481
  try {
2317
2482
  const runId = await adapter.launchRun(input);
2318
2483
  // crew#641/#642 (item 5): when a run is promoted from a chat, record how many seats
@@ -2654,6 +2819,47 @@ runtime = {}) {
2654
2819
  await backfillEndedAt([run]);
2655
2820
  return { run: decorateRun(run) };
2656
2821
  });
2822
+ // Run learnings: what a run proposed to the approve queue (its end card, "3 learnings proposed · Review").
2823
+ // Proposals are attributed by provenance (`run_id`); a distil seat still reading the run says so.
2824
+ app.get(`${V}/runs/:id/learnings`, { config: { manifest: { responseType: 'RunLearningsResponse', statusCodes: [200, 404, 501, 502] } } }, async (req, reply) => {
2825
+ const { id } = req.params;
2826
+ if (runtime.runLearnings === undefined)
2827
+ return reply.code(501).send({ error: 'run learnings are not wired on this daemon' });
2828
+ if (!(await adapter.sessions()).includes(id))
2829
+ return reply.code(404).send({ error: 'Run not found' });
2830
+ try {
2831
+ const body = await runtime.runLearnings.view(id);
2832
+ return body;
2833
+ }
2834
+ catch (err) {
2835
+ return estateUpstreamError(reply, err);
2836
+ }
2837
+ });
2838
+ // (program DES-LEARN §3.1) A steering-author run's DRAFTED rules, read before the person decides: the
2839
+ // same sources and the same normalization the landing uses, so the ids a person keeps are the ids
2840
+ // `POST /runs/:id/gate {keepRules}` names. `ready: false` until the propose step has run and its gate
2841
+ // is the run's open gate. Read-only.
2842
+ app.get(`${V}/governance/steering/author/:runId/draft`, { config: { manifest: { responseType: 'SteeringDraftResponse', statusCodes: [200, 404, 500] } } }, async (req, reply) => {
2843
+ const { runId } = req.params;
2844
+ const views = await adapter.sessionsDetail();
2845
+ const run = views.find((v) => v.session.id === runId);
2846
+ if (!run)
2847
+ return reply.code(404).send({ error: 'Run not found' });
2848
+ if (!isSteeringAuthorRun(run))
2849
+ return reply.code(404).send({ error: `run ${runId} is not a steering-author run`, code: 'not_steering_author' });
2850
+ const open = run.session.status === 'awaiting_human' ? await gateOfRecord(run, undefined) : null;
2851
+ const prompt = open !== null && !('refusal' in open) ? open.prompt : undefined;
2852
+ const draft = await draftedSteeringRules(adapter, run, prompt);
2853
+ if (draft !== null && 'unreadable' in draft)
2854
+ return reply.code(500).send({ error: draft.unreadable });
2855
+ const waiting = open !== null && !('refusal' in open) && atProposeGate(run);
2856
+ return {
2857
+ runId,
2858
+ ready: draft !== null && waiting,
2859
+ rules: draft?.rules ?? [],
2860
+ ...(draft !== null ? { source: draft.source } : {}),
2861
+ };
2862
+ });
2657
2863
  // ── Inventory claims (crew#721) — whether each enumerating step's list is complete ──
2658
2864
  // A step that lists things a later step acts on ends its reply with a `wicked-inventory` block
2659
2865
  // (wicked-garden core/refs/inventory-report.md). This reads them off every unit's captured
@@ -3062,7 +3268,7 @@ runtime = {}) {
3062
3268
  manifest: {
3063
3269
  requestType: 'ChatOpenBody',
3064
3270
  responseType: 'ChatOpenResponse',
3065
- statusCodes: [201, 400, 404, 409, 500, 501],
3271
+ statusCodes: [201, 400, 403, 404, 409, 500, 501],
3066
3272
  },
3067
3273
  },
3068
3274
  }, async (req, reply) => {
@@ -3073,11 +3279,87 @@ runtime = {}) {
3073
3279
  const b = parsed.data;
3074
3280
  const chatId = b.chatId ?? randomUUID();
3075
3281
  const requestedRefs = [...(b.repoRef !== undefined ? [b.repoRef] : []), ...(b.repoRefs ?? [])];
3282
+ // The scope kind as the open resolves it: the request's, unless an Ask-about-this seed names the repositories.
3283
+ let scopeKind = b.scopeKind;
3076
3284
  // A named scope's SHAPE first (studio#323 R4, codex on #664): a body whose kind refuses its own
3077
- // projectId/repoRefs is a 400 — never masked by the project lookup's 404/409 below.
3078
- const shape = namedKindRefusal(b.scopeKind, b.projectId, [...new Set(requestedRefs)]);
3285
+ // projectId/repoRefs is a 400 — never masked by the project lookup's 404/409 below, nor by a seed's reads.
3286
+ // (A seed asked to fill an empty `repo`/`repos` list is checked once it has filled it, below.)
3287
+ const seedFills = b.seedFromArtifact !== undefined && requestedRefs.length === 0 && (scopeKind === 'repo' || scopeKind === 'repos');
3288
+ const shape = seedFills ? null : namedKindRefusal(scopeKind, b.projectId, [...new Set(requestedRefs)]);
3079
3289
  if (shape !== null)
3080
3290
  return reply.code(400).send({ error: shape.error });
3291
+ // DES-CHAT-ARTIFACTS-001 §6.3 (A1): "Ask about this" on a document no open chat holds. Access first (a person,
3292
+ // a document that exists), then the scope rule: a chat in no project, or a project chat whose project is the
3293
+ // document's mount and holds every repository the document was made about. Only then are the chat's
3294
+ // repositories seeded from that binding — and only when the request names none and no kind that refuses them.
3295
+ if (b.seedFromArtifact !== undefined) {
3296
+ const seed = parseArtifactRef(b.seedFromArtifact);
3297
+ if (seed === null)
3298
+ return reply.code(400).send({ code: 'seed_invalid', error: 'seedFromArtifact must be <mount>/<doc>' });
3299
+ if (actorOf(req).kind !== 'human') {
3300
+ return reply.code(403).send({ code: 'seed_refused', error: 'only a person can open a chat about a document' });
3301
+ }
3302
+ // Scope before any read: a project chat may be seeded only from a document in that project.
3303
+ const chatMountOf = chatMount(b.projectId);
3304
+ if (chatMountOf !== null && seed.mount !== chatMountOf) {
3305
+ return reply.code(400).send({
3306
+ code: 'seed_out_of_scope',
3307
+ error: `${seed.ref} is ${seed.mount === UNFILED_MOUNT ? 'Unfiled' : `in project ${seed.mount}`}, not in project ${b.projectId}; open the chat in its project, or in no project`,
3308
+ });
3309
+ }
3310
+ let root;
3311
+ try {
3312
+ root = await projectDocsRoot(adapter, projectSettings, seed.projectId);
3313
+ }
3314
+ catch (err) {
3315
+ return reply.code(500).send({ error: `cannot read ${seed.ref}: ${message(err)}` });
3316
+ }
3317
+ const status = root === null ? 'missing' : artifactStatus(root, seed.doc);
3318
+ if (status === 'unsupported') {
3319
+ return reply.code(501).send({ code: 'seed_unsupported', error: 'this platform cannot read a document without following links in its folders' });
3320
+ }
3321
+ if (root === null || status === 'missing') {
3322
+ return reply.code(404).send({ code: 'seed_not_found', error: `no document ${seed.ref} on this daemon` });
3323
+ }
3324
+ // The repositories the document was grounded on, as its draft launch resolved them (inferred from the brief or
3325
+ // the project's one repository too); the create-time request's list only for a binding that predates that.
3326
+ // Read beneath the docs root like every A1 read (crew#1274).
3327
+ const grounding = readGroundingBeneath(root, seed.doc);
3328
+ if (grounding !== null && 'unreadable' in grounding) {
3329
+ return reply.code(409).send({ code: 'seed_binding_unreadable', error: `what ${seed.ref} was made about cannot be read; open the chat without it` });
3330
+ }
3331
+ const bound = grounding?.repoRefs ?? [];
3332
+ if (b.projectId !== undefined && bound.length > 0) {
3333
+ const members = new Set((await projectRepoCandidates(adapter, b.projectId)).map((r) => r.repoRef));
3334
+ const outside = bound.filter((r) => !members.has(r));
3335
+ if (outside.length > 0) {
3336
+ return reply.code(400).send({
3337
+ code: 'seed_out_of_scope',
3338
+ error: `${seed.ref} was made about ${outside.join(', ')}, which project ${b.projectId} does not hold; open the chat in no project`,
3339
+ });
3340
+ }
3341
+ }
3342
+ // Seeded: the chat reads the repositories the document was made about, unless the request named its own or a
3343
+ // kind that reads every repository (`everything`) or names its repositories by project (`project`). A chat in
3344
+ // no project that asked for no repository (`system`, `none`) becomes the document's repository scope; a
3345
+ // project chat keeps the kind it asked for.
3346
+ const seedable = b.projectId === undefined
3347
+ ? scopeKind === undefined || scopeKind === 'repo' || scopeKind === 'repos' || scopeKind === 'system' || scopeKind === 'none'
3348
+ : scopeKind === undefined || scopeKind === 'repo' || scopeKind === 'repos';
3349
+ if (requestedRefs.length === 0 && bound.length > 0 && seedable) {
3350
+ // Only where the binding becomes the chat's repository list does a chat's limit apply (an `everything` or
3351
+ // `project` chat, or one that names its own repositories, reads its own scope whatever the binding holds).
3352
+ if (bound.length > 32) {
3353
+ return reply.code(400).send({ code: 'seed_too_many_repos', error: `${seed.ref} was made about ${bound.length} repositories; a chat reads at most 32 — open it in its project or across all repositories` });
3354
+ }
3355
+ requestedRefs.push(...bound);
3356
+ if (scopeKind === 'system' || scopeKind === 'none')
3357
+ scopeKind = undefined;
3358
+ }
3359
+ const reshaped = namedKindRefusal(scopeKind, b.projectId, [...new Set(requestedRefs)]);
3360
+ if (reshaped !== null)
3361
+ return reply.code(400).send({ error: reshaped.error });
3362
+ }
3081
3363
  // Validate the project BEFORE opening seats: a chat has no launch record for the engine to
3082
3364
  // attach against atomically (chats are an in-memory seat pool), so the route validates
3083
3365
  // up-front and attaches right after open — the one non-atomic attach, documented in the ADR
@@ -3114,7 +3396,7 @@ runtime = {}) {
3114
3396
  return reply.code(resolved.status).send({ error: resolved.error });
3115
3397
  workflow = resolved.workflow;
3116
3398
  }
3117
- const statementOpts = { analysis: workflow !== undefined };
3399
+ const statementOpts = { analysis: workflow !== undefined, make: chatProposals.digest() };
3118
3400
  // A chat id this daemon already holds a scope for is LIVE: re-preparing its scratch root and
3119
3401
  // re-opening it would overwrite the statement its seats are reading and (on a changed scope)
3120
3402
  // evict them mid-conversation (Copilot, #518). Close it first, or let the daemon mint the id.
@@ -3140,7 +3422,7 @@ runtime = {}) {
3140
3422
  chatId,
3141
3423
  ...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
3142
3424
  repoRefs: requestedRefs,
3143
- ...(b.scopeKind !== undefined ? { kind: b.scopeKind } : {}),
3425
+ ...(scopeKind !== undefined ? { kind: scopeKind } : {}),
3144
3426
  };
3145
3427
  const resolution = await resolveScope(scopeRequest, req);
3146
3428
  if (!resolution.ok) {
@@ -3148,6 +3430,7 @@ runtime = {}) {
3148
3430
  return reply.code(resolution.status).send({
3149
3431
  error: resolution.error,
3150
3432
  ...(resolution.missing !== undefined ? { missing: resolution.missing } : {}),
3433
+ ...(resolution.code !== undefined ? { code: resolution.code, path: resolution.path ?? null } : {}),
3151
3434
  });
3152
3435
  }
3153
3436
  const { scope, engine } = resolution;
@@ -3162,7 +3445,7 @@ runtime = {}) {
3162
3445
  if (!chatScopes.owns(chatId, token)) {
3163
3446
  return reply.code(409).send({ error: `chat ${chatId} was closed while it was being opened; open it again` });
3164
3447
  }
3165
- prepareChatScratch(chatId, scope, inForceAtOpen, statementOpts);
3448
+ prepareChatScratch(chatId, scope, inForceAtOpen, { ...statementOpts, ...voiceFor(scope.projectId) });
3166
3449
  }
3167
3450
  catch (err) {
3168
3451
  // The helper removes only what it created itself (a pre-existing root is preserved) —
@@ -3322,6 +3605,8 @@ runtime = {}) {
3322
3605
  startedOn: b.startedOn,
3323
3606
  cli: b.primary ?? clis[0] ?? null,
3324
3607
  actorId: security.authMode === 'required' ? actorOf(req).id : null,
3608
+ // A2: an ended chat's proposal frames still name its project.
3609
+ projectId: scope.projectId ?? null,
3325
3610
  });
3326
3611
  }
3327
3612
  catch (err) {
@@ -3387,7 +3672,78 @@ runtime = {}) {
3387
3672
  targets: z.array(z.string().min(1)).min(1).max(8).optional(),
3388
3673
  /** (api-types 0.137.0) Attachment ids from `POST /chats/:id/attachments`. */
3389
3674
  attachments: z.array(z.string().min(1).max(64)).optional(),
3675
+ /** (api-types 0.169.0, DES-CHAT-ARTIFACTS-001 §5.1) What studio shows beside the message. */
3676
+ context: ChatMessageContextSchema.optional(),
3390
3677
  }).strict();
3678
+ // DES-CHAT-ARTIFACTS-001 §5.1 (A1): what a turn's block reads — the docs roots, the decorated runs (project,
3679
+ // document and chat ids), unit outputs and the acceptance line. One set of deps, so the block and the run pages
3680
+ // read the same records.
3681
+ const chatContextDeps = {
3682
+ docsRoot: (projectId) => projectDocsRoot(adapter, projectSettings, projectId),
3683
+ runs: async () => (await adapter.sessionsDetail()).map((v) => decorateRun(v)),
3684
+ workOutput: async (unitId) => (typeof adapter.workOutput === 'function' ? adapter.workOutput(unitId) : null),
3685
+ acceptanceLine: async (view) => (await acceptanceViewOf(view, {})).summary.line,
3686
+ docStyle: (root, doc) => {
3687
+ const g = readGroundingBeneath(root, doc);
3688
+ return g !== null && 'style' in g ? g.style : undefined;
3689
+ },
3690
+ };
3691
+ // DES-CHAT-ARTIFACTS-001 §4 (A2): proposals. Resolved from crew's own records (the same reads the turn block uses);
3692
+ // records go to the live transcript, or the kept history copy once the chat has ended.
3693
+ // A dollar figure only from a run whose EVERY dispatched attempt of every agent step reported a price (DES §4.2).
3694
+ const proposalCosts = async (view) => runCostFromEvents(view, (await adapter.runEvents(view.session.id)));
3695
+ const chatProposals = new ChatProposals({
3696
+ flags: () => chatArtifactFlags,
3697
+ catalog: CATALOG_SEEDS,
3698
+ records: (chatId) => chatHistory?.messages(chatId) ?? chatTranscripts?.read(chatId) ?? [],
3699
+ append: (chatId, record) => {
3700
+ if (chatHistory?.stateOf(chatId)?.status === 'closed')
3701
+ return chatHistory.appendKept(chatId, record);
3702
+ return chatTranscripts?.appendProposalRecord(chatId, record) ?? false;
3703
+ },
3704
+ instanceOf: (chatId) => chatHistory?.stateOf(chatId)?.instance ?? null,
3705
+ projectOf: (chatId) => chatScopes.get(chatId)?.projectId ?? chatHistory?.stateOf(chatId)?.projectId ?? null,
3706
+ chat: (chatId) => {
3707
+ const scope = chatScopes.get(chatId);
3708
+ if (scope === undefined)
3709
+ return null;
3710
+ const path = askPaths.get(chatId);
3711
+ return {
3712
+ projectId: scope.projectId ?? null,
3713
+ repos: scope.repos.map((r) => ({ id: r.id, name: r.name })),
3714
+ lead: path?.pa ?? path?.primary ?? null,
3715
+ seats: path?.eligible ?? [],
3716
+ analysis: path?.workflow !== undefined,
3717
+ };
3718
+ },
3719
+ rosterEligible: () => rosterWithStanding()
3720
+ .filter((s) => s.enabled_for_council && s.seat_eligible_for_work !== false && s.auth !== 'signed_out' && s.optIn !== true)
3721
+ .map((s) => ({ key: String(s.key), name: s.display_name || String(s.key) })),
3722
+ runs: async () => (await adapter.sessionsDetail()).map((v) => decorateRun(v)),
3723
+ chatOfRun: (runId) => runTimingIndex.chatIdFor(runId),
3724
+ isAskPath: (runId) => runTimingIndex.isAskPath(runId),
3725
+ docsRoot: (projectId) => projectDocsRoot(adapter, projectSettings, projectId),
3726
+ projectName: async (projectId) => (await adapter.projectGet(projectId).catch(() => null))?.name ?? null,
3727
+ projectRepos: async (projectId) => (await projectRepoCandidates(adapter, projectId, (m) => app.log.warn(m))).map((r) => ({ id: r.repoRef, name: r.name })),
3728
+ attachments: (chatId) => chatAttachments.list(chatId).map((a) => ({ id: a.id, name: a.name, bytes: a.bytes, mediaType: a.mediaType })),
3729
+ repoIds: async () => new Set((await adapter.listRepos()).map((r) => r.id)),
3730
+ workflowSteps: async (name, projectId) => {
3731
+ // The preset a launch in this project resolves first (a project row wins over a global one), then the def.
3732
+ const presets = await adapter.listPresets(projectId ?? undefined).catch(() => []);
3733
+ const preset = presets.find((p) => p.name === name && p.scope !== 'global') ?? presets.find((p) => p.name === name);
3734
+ if (preset !== undefined && preset.steps.length > 0)
3735
+ return preset.steps.length;
3736
+ const phases = adapter.getWorkflow(name)?.phases;
3737
+ return phases !== undefined && phases.length > 0 ? phases.length : null;
3738
+ },
3739
+ runCostUsd: proposalCosts,
3740
+ registry: proposalLaunchers,
3741
+ broadcast: (frame) => runtime.broadcast?.(frame),
3742
+ audit: (event, actor, detail) => {
3743
+ audit.record(event, actor, { detail });
3744
+ },
3745
+ log: (m) => app.log.warn(m),
3746
+ });
3391
3747
  // F-RECON-017: a message sent while a targeted seat is still answering the previous one is
3392
3748
  // REFUSED, not queued. The engine queues silently (the worker gets the text the instant the
3393
3749
  // previous turn's final block lands) and its reply frames carry no message correlation, so the
@@ -3416,6 +3772,22 @@ runtime = {}) {
3416
3772
  return reply.code(409).send({ error: `chat '${id}' has no eligible seat — sign a seat in and re-seat it (POST /chats/:id/seats)` });
3417
3773
  }
3418
3774
  const text = parsed.data.text;
3775
+ // DES-CHAT-ARTIFACTS-001 §5.1 (A1): the per-turn block reads crew's records (documents, runs) for the chat's own
3776
+ // person. A message that carries `context` from anyone else (another actor under `auth: required`, or an agent,
3777
+ // system or plugin actor) is refused before anything is reserved or read; one without `context` from them still
3778
+ // goes through, with no block.
3779
+ const msgContext = parsed.data.context;
3780
+ const sender = actorOf(req);
3781
+ const ownsChat = chatHistory === null || chatHistory.visibleTo(id, security.authMode === 'required' ? sender.id : null);
3782
+ const contextAllowed = sender.kind === 'human' && ownsChat;
3783
+ if (msgContext !== undefined && !contextAllowed) {
3784
+ return reply.code(403).send({
3785
+ code: 'chat_context_refused',
3786
+ error: sender.kind !== 'human'
3787
+ ? `only a person can send what they are looking at with a chat message (you are a ${sender.kind} actor). Nothing was sent.`
3788
+ : `chat '${id}' was opened by someone else; only its owner can send what they are looking at into it. Nothing was sent.`,
3789
+ });
3790
+ }
3419
3791
  // DES-SYSTEM-CHAT: the attachments this message carries — every id this chat's, within the caps,
3420
3792
  // refused before anything is reserved or sent.
3421
3793
  let attached = [];
@@ -3463,6 +3835,8 @@ runtime = {}) {
3463
3835
  // crew#826: the turn lives as long as its answer step may (`budget_secs`), not 3× the pool knob.
3464
3836
  let turn = null;
3465
3837
  let turnId = randomUUID();
3838
+ // A1: what this turn's seats were given beside the words (recorded with the turn).
3839
+ let turnBinding = null;
3466
3840
  // What an ACCEPTED message records — the engine has it, so a persistence failure here is
3467
3841
  // logged, never a retraction of a turn that is already being answered (codex on #808 r2, 9).
3468
3842
  const persistAccepted = (preface, after) => {
@@ -3470,7 +3844,9 @@ runtime = {}) {
3470
3844
  after();
3471
3845
  if (preface !== null)
3472
3846
  chatTranscripts?.appendSystem(id, turnId, preface);
3473
- chatTranscripts?.appendUser(id, turnId, text, voice, attached.map(attachmentWire));
3847
+ chatTranscripts?.appendUser(id, turnId, text, voice, attached.map(attachmentWire), contextAllowed);
3848
+ if (turnBinding !== null)
3849
+ chatTranscripts?.appendContext(id, turnId, turnBinding);
3474
3850
  // The recorder's audience is the turn's AS THE TURN INDEX HOLDS IT NOW: the eligible roster
3475
3851
  // for a random pick, or the PA the relay narrowed it to during the engine awaits above
3476
3852
  // (`path.started` / `path.repicked` may land before this record exists — ASK-C2, codex on
@@ -3567,6 +3943,31 @@ runtime = {}) {
3567
3943
  });
3568
3944
  }
3569
3945
  const pending = await considerations.pendingPreface(id, { inForce: rules });
3946
+ // DES-CHAT-ARTIFACTS-001 §5.1 (A1): the turn's block — scope checked before anything is read or copied; the
3947
+ // copies and briefs land in the scratch root's `open/` and `context/`, read roots of the chat's run.
3948
+ // Built in memory here (it awaits); its files are written only after the last check below, with no await between.
3949
+ let contextBlock = '';
3950
+ let publishContext = () => undefined;
3951
+ if (scope !== undefined && contextAllowed) {
3952
+ try {
3953
+ const built = await buildTurnContext({
3954
+ chatId: id,
3955
+ scratchRoot: scope.cwd,
3956
+ projectId: scope.projectId ?? null,
3957
+ text,
3958
+ context: msgContext,
3959
+ records: chatTranscripts?.read(id) ?? [],
3960
+ attachmentIds: chatAttachments.list(id).map((a) => a.id),
3961
+ }, chatContextDeps);
3962
+ contextBlock = built.block;
3963
+ turnBinding = built.binding;
3964
+ publishContext = built.publish;
3965
+ }
3966
+ catch (err) {
3967
+ askPaths.release(id, lease);
3968
+ return reply.code(500).send({ error: `could not prepare what this message is about: ${message(err)}. Nothing was sent.` });
3969
+ }
3970
+ }
3570
3971
  // The last await before delivery may also have closed/reopened the chat. From this check
3571
3972
  // through publication and the send there is no await, so no executing turn's files change.
3572
3973
  if (askPaths.get(id)?.generation !== lease || chatScopes.get(id) !== storedScope) {
@@ -3574,7 +3975,7 @@ runtime = {}) {
3574
3975
  }
3575
3976
  if (storedScope !== undefined && scope !== undefined && engine !== undefined &&
3576
3977
  (JSON.stringify(scope) !== JSON.stringify(storedScope) || JSON.stringify(engine) !== JSON.stringify(chatScopes.engineOf(id)))) {
3577
- prepareChatScratch(id, scope, rules, { analysis: path.workflow !== undefined });
3978
+ prepareChatScratch(id, scope, rules, { analysis: path.workflow !== undefined, make: chatProposals.digest(), ...voiceFor(scope.projectId) });
3578
3979
  if (chatScopes.refresh(id, storedScope, scope, engine)) {
3579
3980
  chatTranscripts?.registerRoots(id, scope.repos.map((r) => ({ absRoot: resolve(r.rootPath), name: r.name })));
3580
3981
  }
@@ -3595,6 +3996,22 @@ runtime = {}) {
3595
3996
  return reply.code(500).send({ error: `could not stage the attachments for the seat: ${message(err)}` });
3596
3997
  }
3597
3998
  }
3999
+ // A1: the copies and briefs land now — after the last await and the generation check, before the send — and the
4000
+ // block rides after the person's words (and their files), labelled as data.
4001
+ if (contextBlock !== '') {
4002
+ try {
4003
+ publishContext();
4004
+ }
4005
+ catch (err) {
4006
+ askPaths.release(id, lease);
4007
+ return reply.code(500).send({ error: `could not hand the seat what this message is about: ${message(err)}. Nothing was sent.` });
4008
+ }
4009
+ seatText = `${seatText}\n\n${contextBlock}`;
4010
+ }
4011
+ // A2: the per-message digest of what the chat can make now (advertised operations only; never in an analysis).
4012
+ const makeDigest = path.workflow === undefined ? chatMakeDigest(chatProposals.digest()) : '';
4013
+ if (makeDigest !== '')
4014
+ seatText = `${seatText}\n\n${makeDigest}`;
3598
4015
  voice = path.pa !== null ? [path.pa] : [];
3599
4016
  // A random pick reserves every eligible seat until the relay names the PA.
3600
4017
  audience = voice.length > 0 ? voice : path.eligible;
@@ -3604,7 +4021,8 @@ runtime = {}) {
3604
4021
  // chat (crew#1157) never delivers: it is bound to its repository read-only, with no deliver.
3605
4022
  const repoRef = scope?.kind === 'repos' && scope.repos.length === 1 ? scope.repos[0].id : undefined;
3606
4023
  const delivers = repoRef !== undefined && path.workflow === undefined;
3607
- const statementOpts = { analysis: path.workflow !== undefined };
4024
+ // A2: the per-message digest of what the chat can make now (advertised operations only).
4025
+ const statementOpts = { analysis: path.workflow !== undefined, make: chatProposals.digest(), ...voiceFor(scope?.projectId) };
3608
4026
  const projectId = scope?.projectId;
3609
4027
  if (path.runId === undefined) {
3610
4028
  const stepId = 'answer-1';
@@ -3631,8 +4049,20 @@ runtime = {}) {
3631
4049
  // statement tells the seats to read them at their paths, so the boundary must let them.
3632
4050
  // DES-SYSTEM-CHAT: plus the chat's attachments directory, from launch (a run cannot gain a
3633
4051
  // read root later), so a file attached on any later turn is readable too.
3634
- ...(readRoots.length > 0 || attachRoot !== null
3635
- ? { extraReadRoots: [...readRoots, ...(attachRoot !== null ? [ensureDir(attachRoot)] : [])] }
4052
+ // DES-CHAT-ARTIFACTS-001 §5.1 (A1): and the directories the turn blocks copy artifacts (`open/`) and write run
4053
+ // briefs (`context/`) into, from launch, so a later turn's copies and briefs are readable too.
4054
+ ...(readRoots.length > 0 || attachRoot !== null || scope !== undefined
4055
+ ? {
4056
+ extraReadRoots: [
4057
+ ...readRoots,
4058
+ ...(attachRoot !== null ? [ensureDir(attachRoot)] : []),
4059
+ // At their real paths: a seat in a container sees a read root only there (see `seatPathOf`). Named from the
4060
+ // trusted scratch root's real path, never a child's (a child is the seat's to change; and no seat of this
4061
+ // chat exists yet: this is its first launch). Left out where the platform cannot write them safely, and
4062
+ // when they cannot be made: a plain chat still launches.
4063
+ ...(scope !== undefined ? turnReadRoots(scope.cwd) : []),
4064
+ ],
4065
+ }
3636
4066
  : {}),
3637
4067
  };
3638
4068
  const runId = await adapter.launchRun(input);
@@ -3715,6 +4145,56 @@ runtime = {}) {
3715
4145
  return reply.code(400).send({ error: message(err) });
3716
4146
  }
3717
4147
  });
4148
+ // DES-CHAT-ARTIFACTS-001 §4.3 (A2): the ONLY way a proposal starts work. Capability, person and owner are checked
4149
+ // first on every call (a worker or system actor is refused even on an `accepting` proposal); the service then takes
4150
+ // the proposal's lock and looks for what already happened before any state check.
4151
+ const ProposalPatchValue = z.union([z.string().max(200), z.number(), z.boolean()]);
4152
+ const ProposalDecisionSchema = z.object({
4153
+ decision: z.enum(['go', 'not_now']),
4154
+ edits: z.object({
4155
+ title: z.string().max(400).optional(),
4156
+ brief: z.string().max(16000).optional(),
4157
+ sources: z.array(z.string().min(1).max(300)).max(32).optional(),
4158
+ repos: z.array(z.string().min(1).max(200)).max(16).optional(),
4159
+ project: z.string().max(200).nullable().optional(),
4160
+ ask: z.string().max(8000).optional(),
4161
+ allow: z.array(z.string().min(1).max(128)).max(64).optional(),
4162
+ name: z.string().max(200).optional(),
4163
+ patch: z.record(z.string().max(40), ProposalPatchValue).optional(),
4164
+ url: z.string().max(2048).optional(),
4165
+ audience: z.string().max(800).optional(),
4166
+ show: z.string().max(4000).optional(),
4167
+ helpers: z.array(z.string().min(1).max(64)).max(8).optional(),
4168
+ }).strict().optional(),
4169
+ }).strict();
4170
+ app.post(`${V}/chats/:id/proposals/:pid`, { config: { manifest: { requestType: 'ChatProposalDecisionBody', responseType: 'ChatProposalDecisionResponse', statusCodes: [200, 400, 403, 404, 409, 502] } } }, async (req, reply) => {
4171
+ const { id, pid } = req.params;
4172
+ if (!chatArtifactFlags.proposals)
4173
+ return reply.code(404).send({ error: 'this daemon does not answer chat proposals' });
4174
+ const actor = actorOf(req);
4175
+ if (!isHumanOperator(actor)) {
4176
+ audit.record('chat.proposal.refused', actor, { detail: { chatId: id, pid, reason: 'not_a_person' } });
4177
+ return reply.code(403).send({
4178
+ code: 'proposal_refused',
4179
+ error: `only a person answers a proposal (you are ${actor.kind}/${actor.trust}). Nothing was started.`,
4180
+ });
4181
+ }
4182
+ const viewer = security.authMode === 'required' ? actor.id : null;
4183
+ if (!CHAT_ID.test(id) || chatHistory === null || !chatHistory.visibleTo(id, viewer)) {
4184
+ return reply.code(404).send({ error: `no chat ${id} on this daemon` });
4185
+ }
4186
+ // The instance these checks passed against: nothing is recorded on a chat that reuses the id meanwhile.
4187
+ const instance = chatHistory.stateOf(id)?.instance ?? null;
4188
+ const parsed = ProposalDecisionSchema.safeParse(req.body);
4189
+ if (!parsed.success)
4190
+ return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
4191
+ // Optional fields arrive as absent, never as `undefined` (the contract's exact optional shape).
4192
+ const edits = parsed.data.edits === undefined
4193
+ ? undefined
4194
+ : Object.fromEntries(Object.entries(parsed.data.edits).filter(([, v]) => v !== undefined));
4195
+ const out = await chatProposals.decide(id, pid, { decision: parsed.data.decision, ...(edits !== undefined ? { edits } : {}) }, actor, instance);
4196
+ return reply.code(out.code).send(out.body);
4197
+ });
3718
4198
  const ChatSeatsSchema = z.object({
3719
4199
  clis: z.array(z.string().min(1)).min(1).max(8),
3720
4200
  }).strict();
@@ -4617,6 +5097,21 @@ runtime = {}) {
4617
5097
  // holds nothing after a restart and another gate's words after a late frame.
4618
5098
  const steeringPropose = parsed.data.approve && isSteeringAuthorRun(run);
4619
5099
  const gatePrompt = steeringPropose ? decided.prompt : undefined;
5100
+ // DES-LEARN §3.1: `keepRules` names drafted rules: on a steering-author propose gate only, and only
5101
+ // ids of THIS draft. A draft that changed since the person read it is a 409, the gate left open.
5102
+ let keptSnapshot;
5103
+ if (parsed.data.keepRules !== undefined) {
5104
+ const draft = steeringPropose && atProposeGate(run) ? await draftedSteeringRules(adapter, run, gatePrompt) : null;
5105
+ if (draft === null || 'unreadable' in draft) {
5106
+ return { code: 409, body: { error: 'keepRules answers a steering-author propose gate with a readable draft; this gate has none', code: 'no_draft' } };
5107
+ }
5108
+ const ids = new Set(draft.rules.map((r) => r.id));
5109
+ const unknown = parsed.data.keepRules.filter((k) => !ids.has(k));
5110
+ if (unknown.length > 0) {
5111
+ return { code: 409, body: { error: `the draft changed since it was read (no drafted rule ${unknown.join(', ')}): read it again`, code: 'draft_changed' } };
5112
+ }
5113
+ keptSnapshot = draft.snapshot;
5114
+ }
4620
5115
  try {
4621
5116
  // All five positionals, explicit `undefined` for the absent ones (DES-L1 PR-2; the
4622
5117
  // gate-arms tripwire pins the arity). An engine refusal (no creator to send back to,
@@ -4646,6 +5141,7 @@ runtime = {}) {
4646
5141
  ...(parsed.data.amendScope !== undefined ? { amendScope: parsed.data.amendScope } : {}),
4647
5142
  ...(parsed.data.plan !== undefined ? { planSteps: parsed.data.plan.steps.length } : {}),
4648
5143
  ...(parsed.data.ord !== undefined ? { ord: parsed.data.ord } : {}),
5144
+ ...(parsed.data.input !== undefined ? { input: parsed.data.input } : {}),
4649
5145
  ...extraDetail,
4650
5146
  status,
4651
5147
  },
@@ -4672,7 +5168,10 @@ runtime = {}) {
4672
5168
  // text (an operator who wants different rules rejects and re-authors). A reject lands
4673
5169
  // nothing — the run cancels and the proposal stays an artifact.
4674
5170
  const landing = steeringPropose
4675
- ? await landSteeringProposal({ adapter, audit, actor }, run, gatePrompt)
5171
+ ? await landSteeringProposal({ adapter, audit, actor }, run, gatePrompt, {
5172
+ ...(parsed.data.keepRules !== undefined ? { keep: parsed.data.keepRules } : {}),
5173
+ ...(keptSnapshot !== undefined ? { snapshot: keptSnapshot } : {}),
5174
+ })
4676
5175
  : undefined;
4677
5176
  return {
4678
5177
  code: 200,
@@ -4709,15 +5208,25 @@ runtime = {}) {
4709
5208
  return reply.code(out.code).send(out.body);
4710
5209
  });
4711
5210
  // Cancel a running or paused run (distinct third action, §11.1).
5211
+ // (api-types 0.161.0, DES-VOICE) An optional body: the person's `reason` (said or typed) and `input: "voice"`, both
5212
+ // kept on the `run.cancelled` audit line. A bodiless cancel is unchanged; voice is a person's only (403).
4712
5213
  app.post(`${V}/runs/:id/cancel`, async (req, reply) => {
4713
5214
  const { id } = req.params;
5215
+ // A bodiless cancel arrives as undefined, null or '' (an empty text/plain body): all three are no body.
5216
+ const parsed = CancelSchema.safeParse(req.body === undefined || req.body === null || req.body === '' ? {} : req.body);
5217
+ if (!parsed.success)
5218
+ return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
5219
+ const reason = parsed.data.reason !== undefined && parsed.data.reason !== '' ? parsed.data.reason : null;
5220
+ const spoken = parsed.data.input === 'voice';
5221
+ if (spoken && !isHumanOperator(actorOf(req)))
5222
+ return reply.code(403).send({ error: 'Only a person speaks: input "voice" is a person\'s stop' });
4714
5223
  const views = await adapter.sessionsDetail();
4715
5224
  const run = views.find((v) => v.session.id === id);
4716
5225
  if (!run)
4717
5226
  return reply.code(404).send({ error: 'Run not found' });
4718
5227
  try {
4719
5228
  const status = await adapter.cancelRun(id);
4720
- audit.record('run.cancelled', actorOf(req), { runId: id, detail: { status } });
5229
+ audit.record('run.cancelled', actorOf(req), { runId: id, detail: { status, ...(reason !== null ? { reason } : {}), ...(spoken ? { input: 'voice' } : {}) } });
4721
5230
  return reply.send({ status });
4722
5231
  }
4723
5232
  catch (err) {
@@ -4738,6 +5247,15 @@ runtime = {}) {
4738
5247
  // but its work is complete and liftable — normalize to `completed` so the terminal branch
4739
5248
  // below refuses resume with the DELIVER recovery (POST /runs/:id/deliver), not a re-entry.
4740
5249
  const conflictStrand = normalizeStranded(run);
5250
+ // DES-CHAT-ARTIFACTS-001 §6.2 (A3): a chat proposal's FAILED attempt is never resumed from its cursor: its seam flight
5251
+ // is gone (a resume would finish without landing anything) and its retry may already run. Try again is the path.
5252
+ // A still-live bound run's own gate is approved as any run's.
5253
+ if (run.session.status === 'failed' && runtime.docBindings?.isBound(id) === true) {
5254
+ return reply.code(409).send({
5255
+ error: `run ${id} answered a chat proposal: it cannot be resumed. Use Try again on its card in the chat (a new attempt at the same document).`,
5256
+ recovery: 'retry',
5257
+ });
5258
+ }
4741
5259
  // A TERMINAL run is refused LOUDLY, with the actual recovery named (crew#311 defect 2).
4742
5260
  // The engine's `resume_run` no-ops on completed/cancelled and answers the status token, so
4743
5261
  // this route used to reply 200 {"status":"cancelled"} — on the exact runs an operator was
@@ -4818,12 +5336,27 @@ runtime = {}) {
4818
5336
  if (!parsed.success) {
4819
5337
  return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
4820
5338
  }
5339
+ const spoken = parsed.data.input === 'voice';
5340
+ if (spoken && !isHumanOperator(actorOf(req)))
5341
+ return reply.code(403).send({ error: 'Only a person speaks: input "voice" is a person\'s note' });
4821
5342
  const ids = await adapter.sessions();
4822
5343
  if (!ids.includes(id))
4823
5344
  return reply.code(404).send({ error: 'Run not found' });
4824
5345
  try {
4825
- await adapter.injectWorkerMessage(id, parsed.data.message, parsed.data.target);
4826
- audit.record('run.injected', actorOf(req), { runId: id, detail: { target: parsed.data.target } });
5346
+ // Remembered BEFORE the engine call (its queued frame can reach /ws before the call returns), and forgotten if
5347
+ // the engine refuses it: a send that never happened labels nothing and evicts nothing.
5348
+ const token = spoken ? voiceInputs.remember(id, parsed.data.message, parsed.data.target) : null;
5349
+ try {
5350
+ await adapter.injectWorkerMessage(id, parsed.data.message, parsed.data.target);
5351
+ }
5352
+ catch (err) {
5353
+ if (token !== null)
5354
+ voiceInputs.forget(id, token);
5355
+ throw err;
5356
+ }
5357
+ if (token !== null)
5358
+ voiceInputs.settle(id);
5359
+ audit.record('run.injected', actorOf(req), { runId: id, detail: { target: parsed.data.target, ...(spoken ? { input: 'voice' } : {}) } });
4827
5360
  // DC-S4a host: the operator's mid-run steering text (review S9).
4828
5361
  if (decisions !== null) {
4829
5362
  decisions.track(ingestDecision(decisions, {
@@ -5399,8 +5932,12 @@ runtime = {}) {
5399
5932
  /** Guards against stale-tab submissions: must match the current elicitation's id. */
5400
5933
  elicitationId: z.string().min(1),
5401
5934
  action: z.enum(['accept', 'decline', 'cancel']),
5402
- /** Required when action is "accept" (response must be non-empty); must be absent for decline/cancel. */
5403
- content: z.object({ response: z.string().min(1) }).optional(),
5935
+ /** Required when action is "accept"; must be absent for decline/cancel. `response` answers a free-text or
5936
+ * single-choice question; `values` (api-types 0.158.0) a multi-select one. */
5937
+ content: z.union([
5938
+ z.object({ response: z.string().min(1) }).strict(),
5939
+ z.object({ values: z.array(z.string().min(1)).min(1).max(100) }).strict(),
5940
+ ]).optional(),
5404
5941
  })
5405
5942
  .strict()
5406
5943
  .superRefine((val, ctx) => {
@@ -5420,9 +5957,9 @@ runtime = {}) {
5420
5957
  return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
5421
5958
  }
5422
5959
  const body = parsed.data;
5423
- // 1b. Accept requires content.response — validated here, before any state query, so
5424
- // the 400 is deterministic regardless of run-existence / cache state (P2).
5425
- if (body.action === 'accept' && !body.content?.response) {
5960
+ // 1b. Accept requires content (a response, or a multi-select's values) — validated here, before any state
5961
+ // query, so the 400 is deterministic regardless of run-existence / cache state (P2).
5962
+ if (body.action === 'accept' && body.content === undefined) {
5426
5963
  return reply.code(400).send({ error: 'action:accept requires content.response' });
5427
5964
  }
5428
5965
  // 2. Existence check (IDs only — cheaper than sessionsDetail).
@@ -5441,20 +5978,40 @@ runtime = {}) {
5441
5978
  .send({ error: 'Elicitation superseded; fetch the current prompt and resubmit' });
5442
5979
  }
5443
5980
  // 5. Accept-specific validation.
5981
+ let response = null;
5982
+ let picked = null;
5444
5983
  if (body.action === 'accept') {
5445
- // 5a. content.response must be present.
5446
- if (typeof body.content?.response !== 'string') {
5984
+ const refuse = (error) => {
5447
5985
  elicitationCache.restoreIfUnchanged(id, taken.entry, taken.gen);
5448
- return reply.code(400).send({ error: 'action:accept requires content.response' });
5986
+ return reply.code(400).send({ error });
5987
+ };
5988
+ const multi = taken.entry.multi;
5989
+ if (multi !== undefined) {
5990
+ // 5a. A multi-select is answered with `values`: distinct options, min..=max of them, sent in option order.
5991
+ if (body.content === undefined || !('values' in body.content))
5992
+ return refuse('this question takes content.values (pick several)');
5993
+ const values = body.content.values;
5994
+ const options = taken.entry.options ?? [];
5995
+ if (values.some((v) => !options.includes(v)))
5996
+ return refuse('values must be among the allowed options');
5997
+ if (new Set(values).size !== values.length)
5998
+ return refuse('values must name each option once');
5999
+ if (values.length < multi.min || values.length > multi.max)
6000
+ return refuse(`pick ${multi.min} to ${multi.max} of the options`);
6001
+ picked = options.filter((o) => values.includes(o));
6002
+ response = JSON.stringify(picked);
5449
6003
  }
5450
- // 5b. Enum check: if the schema constrained the response, honour it.
5451
- if (taken.entry.options !== null &&
5452
- !taken.entry.options.includes(body.content.response)) {
5453
- elicitationCache.restoreIfUnchanged(id, taken.entry, taken.gen);
5454
- return reply.code(400).send({ error: 'response must be one of the allowed options' });
6004
+ else {
6005
+ // 5b. content.response must be present; `values` answers only a multi-select.
6006
+ if (body.content === undefined || !('response' in body.content))
6007
+ return refuse('action:accept requires content.response');
6008
+ // 5c. Enum check: if the schema constrained the response, honour it.
6009
+ if (taken.entry.options !== null && !taken.entry.options.includes(body.content.response)) {
6010
+ return refuse('response must be one of the allowed options');
6011
+ }
6012
+ response = body.content.response;
5455
6013
  }
5456
6014
  }
5457
- const response = body.action === 'accept' ? body.content.response : null;
5458
6015
  // 6. Forward to the actor.
5459
6016
  try {
5460
6017
  await adapter.resolveElicitation(id, taken.entry.elicitationId, body.action, response);
@@ -5471,12 +6028,12 @@ runtime = {}) {
5471
6028
  });
5472
6029
  // DC-S4a host: a free-text answer is words; a picked option (or a decline) is a `choice`.
5473
6030
  if (decisions !== null) {
5474
- const picked = taken.entry.options !== null;
6031
+ const hasOptions = taken.entry.options !== null;
5475
6032
  decisions.track(ingestDecision(decisions, {
5476
6033
  host: 'elicitation',
5477
6034
  actor: actorOf(req),
5478
- words: response !== null && !picked ? response : '',
5479
- ...(response !== null && picked ? { choice: response } : body.action !== 'accept' ? { choice: body.action } : {}),
6035
+ words: response !== null && !hasOptions ? response : '',
6036
+ ...(picked !== null ? { choice: picked.join(', ') } : response !== null && hasOptions ? { choice: response } : body.action !== 'accept' ? { choice: body.action } : {}),
5480
6037
  projectId: projects.index.projectOf(id) ?? null,
5481
6038
  runId: id,
5482
6039
  elicitationId: taken.entry.elicitationId,
@@ -5536,7 +6093,9 @@ runtime = {}) {
5536
6093
  .map((t) => t.trim())
5537
6094
  .filter(Boolean);
5538
6095
  const wanted = names.length > 0 ? new Set(names) : null;
5539
- const filtered = wanted ? events.filter((e) => wanted.has(e.type)) : events;
6096
+ // DES-VOICE: a note said by voice reads "You (by voice)" on a reloaded thread too (this daemon's memory).
6097
+ const stamped = voiceInputs.stampHistory(id, events);
6098
+ const filtered = wanted ? stamped.filter((e) => wanted.has(e.type)) : stamped;
5540
6099
  return { runId: id, total: events.length, returned: filtered.length, events: filtered };
5541
6100
  });
5542
6101
  // ── Governance reads (crew#40) ──────────────────────────────────────────────
@@ -5929,6 +6488,46 @@ runtime = {}) {
5929
6488
  const { workflows, unavailable } = await adapter.workflowCatalog();
5930
6489
  return { workflows, unavailable };
5931
6490
  });
6491
+ // wf-catalog (api-types 0.168.0): the workflows as a categorised catalog with the requesting
6492
+ // operator's usage. Metadata is the ONE data module (`workflow-catalog/catalog-data.ts`); what a
6493
+ // workflow is and does stays on its def; usage is derived from the runs and the launch trail.
6494
+ app.get(`${V}/workflows/catalog`, { config: { manifest: { responseType: 'WorkflowCatalogResponse', statusCodes: [200, 400] } } }, async (req, reply) => {
6495
+ const raw = req.query?.projectId;
6496
+ if (raw !== undefined && (typeof raw !== 'string' || raw.length > 200)) {
6497
+ return reply.code(400).send({ error: '`projectId` must be a string' });
6498
+ }
6499
+ const projectId = typeof raw === 'string' && raw !== '' ? raw : null;
6500
+ const { workflows } = await adapter.workflowCatalog();
6501
+ let presets = [];
6502
+ try {
6503
+ presets = await adapter.listPresets(projectId ?? undefined);
6504
+ }
6505
+ catch (err) {
6506
+ // An unknown project: the global presets (built-in attribution and saved ones still hold, agy r1).
6507
+ // An engine without presets lists the served defs only. Either way, said once in the log.
6508
+ app.log.warn(`workflows/catalog: the presets could not be read${projectId !== null ? ` for project ${projectId}` : ''} (${message(err)}); listing ${projectId !== null ? 'the global ones' : 'the served workflows only'}`);
6509
+ if (projectId !== null)
6510
+ presets = await adapter.listPresets().catch(() => []);
6511
+ }
6512
+ const views = await adapter.sessionsDetail();
6513
+ // codex r1: date terminal runs that have no `run.ended` entry from their event log, as GET /runs
6514
+ // does, so the median does not depend on which read came first. Only THIS principal's completed
6515
+ // runs can reach a median, so only they are read (Copilot).
6516
+ const me = actorOf(req).id;
6517
+ await backfillEndedAt(views.filter((v) => v.session.status === 'completed' && runTimingIndex.principalFor(v.session.id) === me));
6518
+ const runs = views.map((view) => ({
6519
+ id: view.session.id,
6520
+ workflow: runIdentityOf(view).name,
6521
+ projectId: projects.index.projectOf(view.session.id) ?? null,
6522
+ status: view.session.status,
6523
+ createdAt: runTimingIndex.createdAtFor(view.session.id),
6524
+ endedAt: runTimingIndex.endedAtFor(view.session.id),
6525
+ retryOf: retryIndex.retryOfFor(view.session.id),
6526
+ principal: runTimingIndex.principalFor(view.session.id),
6527
+ }));
6528
+ const body = buildWorkflowCatalog({ defs: workflows, presets, runs, actorId: me, projectId });
6529
+ return body;
6530
+ });
5932
6531
  app.get(`${V}/workflows/:id`, async (req, reply) => {
5933
6532
  const { id } = req.params;
5934
6533
  const workflow = adapter.getWorkflow(id);
@@ -5970,31 +6569,27 @@ runtime = {}) {
5970
6569
  return reply.code(500).send({ error: message(err) });
5971
6570
  }
5972
6571
  });
5973
- // ── Domain-model browser (crew#44) ────────────────────────────────────────
5974
- // Reads the `requirements_graph.json` artifact produced by `wicked-core domain-graph`.
5975
- // Path: `.wicked-estate/requirements/requirements_graph.json` relative to cwd.
5976
- app.get(`${V}/domain-graph`, async (_req, reply) => {
5977
- const path = join(process.cwd(), '.wicked-estate', 'requirements', 'requirements_graph.json');
5978
- try {
5979
- const content = await fsp.readFile(path, 'utf8');
5980
- return { graph: JSON.parse(content) };
5981
- }
5982
- catch (err) {
5983
- if (err.code === 'ENOENT')
5984
- return { graph: null };
5985
- return reply.code(500).send({ error: message(err) });
5986
- }
5987
- });
6572
+ // ── Domain-model browser (crew#44) — RETIRED ──────────────────────────────
6573
+ // This read `<daemon cwd>/.wicked-estate/requirements/requirements_graph.json`. A requirements
6574
+ // graph lives in its repository's estate store now (operator ruling 2026-10-31) and has a
6575
+ // repository: `GET /repos/:id/domain-graph` and `GET /repos/:id/requirements` read it. Answering
6576
+ // from a file here would serve a stale graph forever, so the route says where to go instead.
6577
+ app.get(`${V}/domain-graph`, async (_req, reply) => reply.code(410).send({
6578
+ error: 'The daemon-wide domain graph is retired: a requirements graph belongs to a repository and lives in its estate store. Read GET /api/v1/repos/:id/domain-graph or GET /api/v1/repos/:id/requirements.',
6579
+ code: 'movedToRepository',
6580
+ }));
5988
6581
  // ── Per-repo domain graph ─────────────────────────────────────────────────
5989
- // Reads requirements_graph.json from the repo root. Coverage stats come from
5990
- // the live estate store via `wicked-core coverage --json` (not a cached file).
6582
+ // The graph is read from the repo's estate store through the engine (operator ruling 2026-10-31:
6583
+ // never a JSON file in the repo). Coverage stats come from the live estate store via
6584
+ // `wicked-core coverage --json` (not a cached file).
5991
6585
  app.get(`${V}/repos/:id/domain-graph`, async (req, reply) => {
5992
6586
  const { id } = req.params;
5993
6587
  const repos = await adapter.listRepos();
5994
6588
  const repo = repos.find((r) => r.id === id);
5995
6589
  if (!repo)
5996
6590
  return reply.code(404).send({ error: `Repo ${id} not found` });
5997
- const graphPath = requirementsGraph(repo);
6591
+ if (refusedMissingFolder(repo, reply))
6592
+ return reply;
5998
6593
  const dbPath = codeGraphDbOr503(repo, reply);
5999
6594
  if (dbPath === null)
6000
6595
  return reply;
@@ -6023,13 +6618,12 @@ runtime = {}) {
6023
6618
  }
6024
6619
  }
6025
6620
  try {
6026
- const content = await fsp.readFile(graphPath, 'utf8');
6027
- return { graph: JSON.parse(content), coverage };
6621
+ const read = await readRequirementsGraph(repo, adapter);
6622
+ return { graph: read.graph, coverage };
6028
6623
  }
6029
6624
  catch (err) {
6030
- if (err.code === 'ENOENT')
6031
- return { graph: null, coverage };
6032
- return reply.code(500).send({ error: message(err) });
6625
+ const status = codeGraphErrorStatus(err);
6626
+ return reply.code(status ?? 500).send({ error: message(err) });
6033
6627
  }
6034
6628
  });
6035
6629
  // ── Repo code graph (via wicked-estate graph-view) ──────────────────────────
@@ -6056,8 +6650,9 @@ runtime = {}) {
6056
6650
  return reply.code(400).send(invalidBody(parsed.error, 'Invalid query'));
6057
6651
  }
6058
6652
  let page;
6653
+ let coverage = null;
6059
6654
  try {
6060
- page = await listRequirements(repo, parsed.data);
6655
+ ({ page, coverage } = await listRequirementsRead(repo, parsed.data, adapter));
6061
6656
  }
6062
6657
  catch (err) {
6063
6658
  // wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
@@ -6067,7 +6662,12 @@ runtime = {}) {
6067
6662
  return reply.code(status).send({ error: message(err) });
6068
6663
  }
6069
6664
  if (page === null) {
6070
- return reply.code(404).send({ error: 'requirements_graph.json not generated for this repo yet' });
6665
+ // Not generated yet is a state, typed by `code`, not a refusal (lane repo-page). It says how
6666
+ // far the repository is ("23% real requirements · 4397 placeholders · target 80%";
6667
+ // wicked-core#1233), from the very engine read the page used, when it has a store to count.
6668
+ return reply
6669
+ .code(404)
6670
+ .send(coverage === null ? REQUIREMENTS_NOT_GENERATED : { ...REQUIREMENTS_NOT_GENERATED, coverage });
6071
6671
  }
6072
6672
  // Overrides keyed by ids the corpus no longer mints (an estate id-scheme migration re-keys
6073
6673
  // method/field SymbolIds) would otherwise vanish without a trace — the count is on the page
@@ -6092,7 +6692,7 @@ runtime = {}) {
6092
6692
  }
6093
6693
  let detail;
6094
6694
  try {
6095
- detail = await getRequirement(repo, decoded);
6695
+ detail = await getRequirement(repo, decoded, adapter);
6096
6696
  }
6097
6697
  catch (err) {
6098
6698
  // wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
@@ -6137,7 +6737,7 @@ runtime = {}) {
6137
6737
  }
6138
6738
  let detail;
6139
6739
  try {
6140
- detail = await patchRequirement(repo, decodedKey, parsed.data);
6740
+ detail = await patchRequirement(repo, decodedKey, parsed.data, adapter);
6141
6741
  }
6142
6742
  catch (err) {
6143
6743
  // wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
@@ -6150,12 +6750,14 @@ runtime = {}) {
6150
6750
  return reply.code(404).send({ error: 'Requirement not found' });
6151
6751
  return { requirement: detail };
6152
6752
  });
6153
- app.get(`${V}/repos/:id/graph`, { config: { manifest: { responseType: 'RepoGraphResponse', statusCodes: [200, 404, 500, 503] } } }, async (req, reply) => {
6753
+ app.get(`${V}/repos/:id/graph`, { config: { manifest: { responseType: 'RepoGraphResponse', statusCodes: [200, 404, 409, 500, 503] } } }, async (req, reply) => {
6154
6754
  const { id } = req.params;
6155
6755
  const repos = await adapter.listRepos();
6156
6756
  const repo = repos.find((r) => r.id === id);
6157
6757
  if (!repo)
6158
6758
  return reply.code(404).send({ error: `Repo ${id} not found` });
6759
+ if (refusedMissingFolder(repo, reply))
6760
+ return reply;
6159
6761
  const dbPath = codeGraphDbOr503(repo, reply);
6160
6762
  if (dbPath === null)
6161
6763
  return reply;
@@ -6231,6 +6833,8 @@ runtime = {}) {
6231
6833
  const repo = repos.find((r) => r.id === id);
6232
6834
  if (!repo)
6233
6835
  return reply.code(404).send({ error: `Repo ${id} not found` });
6836
+ if (refusedMissingFolder(repo, reply))
6837
+ return reply;
6234
6838
  const dbPath = codeGraphDbOr503(repo, reply);
6235
6839
  if (dbPath === null)
6236
6840
  return reply;
@@ -6269,14 +6873,24 @@ runtime = {}) {
6269
6873
  const credentials = await deliverCredentialsProbe(originUrl, process.env, undefined, identity);
6270
6874
  return reply.send({ repo: repo.id, ...deliverTargetView(originUrl), credentials });
6271
6875
  });
6876
+ // `?limit=` (1..500, default 20): the repo page shows the latest 20; its "Show all" asks for more.
6877
+ const GitHistoryQuerySchema = z.object({
6878
+ limit: z.coerce.number().int().min(1).max(500).default(20),
6879
+ }).strict();
6272
6880
  app.get(`${V}/repos/:id/git-history`, async (req, reply) => {
6273
6881
  const { id } = req.params;
6274
6882
  const repos = await adapter.listRepos();
6275
6883
  const repo = repos.find((r) => r.id === id);
6276
6884
  if (!repo)
6277
6885
  return reply.code(404).send({ error: `Repo ${id} not found` });
6886
+ if (refusedMissingFolder(repo, reply))
6887
+ return reply;
6888
+ const query = GitHistoryQuerySchema.safeParse(req.query);
6889
+ if (!query.success) {
6890
+ return reply.code(400).send(invalidBody(query.error, 'Invalid query'));
6891
+ }
6278
6892
  try {
6279
- const { stdout } = await execCapped('git', ['log', '--pretty=format:%H\x1f%h\x1f%s\x1f%an\x1f%ar', '-n', '20'], { timeout: 10_000, cwd: repo.root_path });
6893
+ const { stdout } = await execCapped('git', ['log', '--pretty=format:%H\x1f%h\x1f%s\x1f%an\x1f%ar', '-n', String(query.data.limit)], { timeout: 10_000, cwd: repo.root_path });
6280
6894
  const commits = stdout.trim().split('\n').filter(Boolean).map((line) => {
6281
6895
  const parts = line.split('\x1f');
6282
6896
  return {
@@ -6307,6 +6921,8 @@ runtime = {}) {
6307
6921
  const repo = repos.find((r) => r.id === id);
6308
6922
  if (!repo)
6309
6923
  return reply.code(404).send({ error: `Repo ${id} not found` });
6924
+ if (refusedMissingFolder(repo, reply))
6925
+ return reply;
6310
6926
  try {
6311
6927
  const { stdout } = await execCapped('git', ['shortlog', '-sne', '-n', '--no-merges', 'HEAD'], { timeout: 10_000, cwd: repo.root_path });
6312
6928
  // Output: " 42\tFull Name <email@example.com>"
@@ -6620,6 +7236,12 @@ runtime = {}) {
6620
7236
  .code(400)
6621
7237
  .send({ error: 'councilSpawnStaggerMs must be an integer between 0 and 30000' });
6622
7238
  }
7239
+ // Requirements-graph minimum coverage (operator ruling 2026-10-31): `{ minCoveragePct: 1..100 }`.
7240
+ if (Object.hasOwn(patch, 'requirementsGraph') && !isRequirementsGraphSettings(patch.requirementsGraph)) {
7241
+ return reply
7242
+ .code(400)
7243
+ .send({ error: REQUIREMENTS_GRAPH_SETTINGS_RULE });
7244
+ }
6623
7245
  if (Object.hasOwn(patch, 'worker_config_root')) {
6624
7246
  const root = patch.worker_config_root;
6625
7247
  if (typeof root !== 'string' || (root !== '' && !isAbsolute(root))) {
@@ -6841,6 +7463,7 @@ runtime = {}) {
6841
7463
  'baseSkillPolicy',
6842
7464
  'councilSize',
6843
7465
  'councilSpawnStaggerMs',
7466
+ 'requirementsGraph',
6844
7467
  ];
6845
7468
  const safe = {};
6846
7469
  for (const key of allowed) {
@@ -6872,6 +7495,8 @@ runtime = {}) {
6872
7495
  applyWorkerConfigRoot(settings.worker_config_root);
6873
7496
  // …and the council size + stagger, read by the engine per council (wicked-core#888).
6874
7497
  applyCouncilSettings(settings);
7498
+ // …and the requirements-graph minimum, read by the engine's coverage check and domain-graph step.
7499
+ applyRequirementsGraphSettings(settings);
6875
7500
  // Re-judge and re-export the BASE skill (crew#554) — the engine reads WICKED_BASE_SKILL_REF at
6876
7501
  // intake, per launch, so this alone makes a changed name or policy live at the next launch.
6877
7502
  // Only when the patch named either key: an unrelated settings write must not re-log the seam.
@@ -6904,6 +7529,11 @@ runtime = {}) {
6904
7529
  if (err instanceof EstateMcpError && err.code === -32602) {
6905
7530
  return reply.code(400).send({ error: err.message });
6906
7531
  }
7532
+ // A spawn that failed with ENOENT (the OS code, not text in the message) means estate is not
7533
+ // installed; every other error — EACCES on a path that names ENOENT included — keeps its own message.
7534
+ if (err instanceof EstateMcpError && err.spawnCode === 'ENOENT') {
7535
+ return reply.code(502).send({ error: `wicked-estate is not installed: ${ESTATE_INSTALL_HINT}` });
7536
+ }
6907
7537
  return reply.code(502).send({ error: message(err) });
6908
7538
  };
6909
7539
  // GET /proposals?kind_type=&state= → proposal.list → { proposals: Proposal[] }.
@@ -7730,6 +8360,71 @@ runtime = {}) {
7730
8360
  // F-043: the bridge emits where this daemon's interactive seams read — one bus per daemon.
7731
8361
  busDataDir: runtime.interactiveBridgeBusDataDir ?? null,
7732
8362
  });
8363
+ // The seat images row (`seat_images`) and its fix: the engine's `seat-image status --json`, read
8364
+ // per request, and one background `seat-image build` at a time.
8365
+ const coreExe = () => process.env['WICKED_CORE_EXE'] || null;
8366
+ const seatImageRebuilder = runtime.seatImageRebuilder ?? new SeatImageRebuilder(coreExe);
8367
+ const seatImageStatus = runtime.seatImageStatus ??
8368
+ (async () => {
8369
+ const exe = coreExe();
8370
+ if (exe === null)
8371
+ throw new Error('WICKED_CORE_EXE is not set');
8372
+ try {
8373
+ const out = await execCapped(exe, ['seat-image', 'status', '--json'], { timeout: PROBE_TIMEOUT_MS });
8374
+ return parseSeatImageStatus(out.stdout);
8375
+ }
8376
+ catch (err) {
8377
+ const e = err;
8378
+ throw new Error(loginDetail(typeof e.stderr === 'string' && e.stderr !== '' ? e.stderr : (e.message ?? 'failed')));
8379
+ }
8380
+ });
8381
+ // The roster's seats (on the container floor every seat runs in an image); `null` when unreadable.
8382
+ const rosterSeatClis = () => {
8383
+ try {
8384
+ return rosterWithStanding().map((r) => String(r.key));
8385
+ }
8386
+ catch {
8387
+ return null;
8388
+ }
8389
+ };
8390
+ // ── POST /system/seat-images/rebuild — the seat images row's fix (api-types 0.162.0) ──
8391
+ // A person's only: it builds images in the container runtime for minutes. One at a time; the
8392
+ // progress reads on the `seat_images` row.
8393
+ app.post(`${V}/system/seat-images/rebuild`, { config: { manifest: { responseType: 'RebuildSeatImagesResponse', statusCodes: [202, 403, 409, 503] } } }, async (req, reply) => {
8394
+ if (!isHumanOperator(actorOf(req)))
8395
+ return reply.code(403).send({ error: 'Only a person can rebuild the seat images' });
8396
+ if (!isContainerSeatFloor()) {
8397
+ const out = { started: false, reason: 'seats run on the host floor: there are no seat images to rebuild', rebuild: null };
8398
+ return reply.code(503).send(out);
8399
+ }
8400
+ // A rebuild already running is 409 before anything else is asked of a busy runtime.
8401
+ const active = seatImageRebuilder.current();
8402
+ if (active?.running === true) {
8403
+ const out = { started: false, reason: 'a rebuild is already running', rebuild: active };
8404
+ return reply.code(409).send(out);
8405
+ }
8406
+ // The roster's seats the engine has a seat image for (it refuses a name it does not know).
8407
+ const roster = rosterSeatClis();
8408
+ if (roster === null) {
8409
+ const out = { started: false, reason: 'the roster could not be read', rebuild: active };
8410
+ return reply.code(503).send(out);
8411
+ }
8412
+ const st = await settleWithin(seatImageStatus, PROBE_TIMEOUT_MS);
8413
+ if (st.kind !== 'ok' || st.value === null) {
8414
+ const why = st.kind === 'timeout' ? 'the engine did not answer its seat image status in time' : st.kind === 'error' ? homeless(st.error) : 'the engine gave no seat image status';
8415
+ const out = { started: false, reason: why, rebuild: seatImageRebuilder.current() };
8416
+ return reply.code(503).send(out);
8417
+ }
8418
+ const known = new Set(st.value.seats.map((x) => x.cli));
8419
+ const r = await seatImageRebuilder.start(roster.filter((c) => known.has(c)));
8420
+ if (r.started) {
8421
+ audit.record('system.seat_images.rebuild', actorOf(req), { detail: { clis: r.state.clis } });
8422
+ const out = { started: true, rebuild: r.state };
8423
+ return reply.code(202).send(out);
8424
+ }
8425
+ const out = { started: false, reason: r.reason, rebuild: r.state };
8426
+ return reply.code(r.state?.running === true ? 409 : 503).send(out);
8427
+ });
7733
8428
  // ── GET /system — the Nerve Center snapshot (DES-SYSTEM-VIEW, api-types 0.136.0) ──
7734
8429
  // One derived, READ-ONLY answer: the CLIs with their 7-day record, every unfinished run with a
7735
8430
  // COMPUTED liveness, the infrastructure as real bounded probes, the governance instruments and
@@ -7785,6 +8480,8 @@ runtime = {}) {
7785
8480
  toReviewCache = { at: Date.now(), value }; // a failed read is never kept
7786
8481
  return value;
7787
8482
  }, PROBE_TIMEOUT_MS);
8483
+ // The registered repositories' folders (repo_folder_missing): a missing one is a fixable item.
8484
+ const reposP = settleWithin(() => adapter.listRepos(), PROBE_TIMEOUT_MS);
7788
8485
  const gatesByYouP = settleWithin(async () => {
7789
8486
  const since = Date.now() - SYSTEM_STATS_DAYS * 86_400_000;
7790
8487
  const entries = await audit.readAll({ action: 'gate.decided', since });
@@ -7822,9 +8519,10 @@ runtime = {}) {
7822
8519
  }),
7823
8520
  probeVault({ vault: adapter.vaultBin != null ? vaultDiagnostics(adapter.vaultBin) : null, exec: systemExec }),
7824
8521
  diskRow(stateHome),
8522
+ probeSeatImages({ containerFloor, clis: rosterSeatClis(), status: seatImageStatus, rebuild: () => seatImageRebuilder.current() }),
7825
8523
  ]);
7826
- const [views, settings, openGates, diag, ping, rules, gatesByYou, infraProbed] = await Promise.all([
7827
- viewsP, settingsP, gatesP, diagP, pingP, rulesP, gatesByYouP, infraP,
8524
+ const [views, settings, openGates, diag, ping, rules, gatesByYou, infraProbed, reposRead] = await Promise.all([
8525
+ viewsP, settingsP, gatesP, diagP, pingP, rulesP, gatesByYouP, infraP, reposP,
7828
8526
  ]);
7829
8527
  // The broker probe refreshed each container seat's sign-in reading: the CLI cards read the
7830
8528
  // roster AFTER it, so one snapshot never says "broker: signed in" beside "claude: signed out".
@@ -7952,6 +8650,8 @@ runtime = {}) {
7952
8650
  stateHomeRow(d === null ? null : (d.stateHome ?? null)),
7953
8651
  storesRow,
7954
8652
  seatFloorRow,
8653
+ infraProbed[7],
8654
+ reposRow(reposRead),
7955
8655
  infraProbed[6],
7956
8656
  loadRow(),
7957
8657
  ];
@@ -8013,14 +8713,80 @@ runtime = {}) {
8013
8713
  toReview: toReview.kind === 'ok' ? toReview.value : null,
8014
8714
  });
8015
8715
  });
8716
+ // DES-CHAT-ARTIFACTS-001 §5.2 (A3): a create's `chat_id` + `proposal_id` counts only through the caller's accepted record.
8717
+ const proposalGrant = (chatId, pid, actorId) => (CHAT_ID.test(chatId) ? chatProposals.grantFor(chatId, pid, actorId) : null);
8016
8718
  registerInteractiveProxy(app, adapter, {
8017
8719
  settings: projectSettings,
8018
8720
  pool: interactiveBridges,
8019
8721
  ...(runtime.docGrounding !== undefined ? { grounding: runtime.docGrounding } : {}),
8020
8722
  // crew#631: supply the standing roster so doc/demo creates can refuse an unavailable seat.
8021
8723
  roster: () => rosterWithStanding(),
8724
+ proposalGrant,
8725
+ actorOf: (req) => actorOf(req).id,
8022
8726
  log: (m) => app.log.warn(m),
8023
8727
  });
8728
+ // A3: the real `create` launcher (§4.3 steps 2-3): one document and one run per proposal identity.
8729
+ if (realCreate) {
8730
+ const bindings = runtime.docBindings;
8731
+ proposalLaunchers.register('create', new CreateLauncher({
8732
+ bindings,
8733
+ runExists: async (runId) => (await adapter.sessions()).includes(runId),
8734
+ docsRoot: (projectId) => projectDocsRoot(adapter, projectSettings, projectId),
8735
+ createDoc: (projectId, body, actorId) => createDocInProcess(adapter, {
8736
+ settings: projectSettings,
8737
+ pool: interactiveBridges,
8738
+ ...(runtime.docGrounding !== undefined ? { grounding: runtime.docGrounding } : {}),
8739
+ roster: () => rosterWithStanding(),
8740
+ proposalGrant,
8741
+ log: (m) => app.log.warn(m),
8742
+ }, projectId, body, actorId),
8743
+ launchBound: runtime.launchBoundDraft,
8744
+ ...(runtime.fenceBoundDraft !== undefined ? { fence: runtime.fenceBoundDraft } : {}),
8745
+ records: (chatId) => chatHistory?.messages(chatId) ?? chatTranscripts?.read(chatId) ?? [],
8746
+ instanceOf: (chatId) => chatHistory?.stateOf(chatId)?.instance ?? null,
8747
+ chatIdOf: (chatId, instance) => (chatHistory === null ? chatId : chatHistory.idOfInstance(chatId, instance)),
8748
+ repos: async () => (await adapter.listRepos()).map((r) => ({ id: r.id, name: typeof r.name === 'string' && r.name.trim() !== '' ? r.name.trim() : basename(r.root_path), rootPath: r.root_path })),
8749
+ graphFor: async (projectId, chatId) => {
8750
+ if (projectId === null)
8751
+ return chatScopes.engineOf(chatId)?.codeGraphDb ?? null;
8752
+ const decision = await resolveProjectGraphBinding(adapter, projectId, undefined).catch(() => ({ binding: null }));
8753
+ return decision.binding?.dbPath ?? null;
8754
+ },
8755
+ clisJsonFor: (seats) => {
8756
+ const roster = rosterWithStanding();
8757
+ const picked = seats.map((k) => roster.find((s) => s.key === k));
8758
+ return picked.some((s) => s === undefined) ? null : JSON.stringify(picked);
8759
+ },
8760
+ styleOf: (entry) => {
8761
+ const seed = CATALOG_SEEDS.find((c) => c.id === entry);
8762
+ return seed !== undefined && seed.launch.kind === 'document' ? seed.launch.style : 'web';
8763
+ },
8764
+ packetDeps: {
8765
+ runs: chatContextDeps.runs,
8766
+ brief: (view) => buildRunBrief(view, { workOutput: chatContextDeps.workOutput, ...(chatContextDeps.acceptanceLine !== undefined ? { acceptanceLine: chatContextDeps.acceptanceLine } : {}) }),
8767
+ docsRoot: chatContextDeps.docsRoot,
8768
+ attachment: (chatId, id) => {
8769
+ const hit = chatAttachments.resolve(chatId, [id])[0];
8770
+ return hit === undefined ? null : { name: hit.name, path: hit.path };
8771
+ },
8772
+ // The exact brief a turn was given, from the live chat's scratch root, beneath it (no link followed), only when
8773
+ // its bytes still hash to what the turn recorded (A1's binding). Gone after the chat ends or the daemon restarts.
8774
+ contextBrief: (chatId, relPath, sha) => {
8775
+ const cwd = chatScopes.get(chatId)?.cwd;
8776
+ if (cwd === undefined)
8777
+ return null;
8778
+ try {
8779
+ const data = readBeneath(realpathSync(cwd), relPath, BRIEF_COPY_MAX).data;
8780
+ return sha256Hex(data) === sha ? data : null;
8781
+ }
8782
+ catch {
8783
+ return null;
8784
+ }
8785
+ },
8786
+ },
8787
+ log: (m) => app.log.warn(m),
8788
+ }));
8789
+ }
8024
8790
  // ── Governed doc delete (crew#338) — the one door that changes BOTH stores ──
8025
8791
  // `DELETE /projects/:id/interactive/docs/:doc` retires the doc on the bridge AND drops crew's
8026
8792
  // handoff-ledger rows for it (the draft leg keys by document id, so a stale row claims the
@@ -8035,6 +8801,13 @@ runtime = {}) {
8035
8801
  // the operator's real ~/.wicked-crew ledgers. The real sweep always arrives from
8036
8802
  // `createServer`.
8037
8803
  dropDocLedgerRows: runtime.dropDocLedgerRows ?? (() => ({ ok: true, removed_keys: [] })),
8804
+ // A3: a document a chat proposal is still drafting (or landing) is not retired under it.
8805
+ busyReason: (projectId, doc) => {
8806
+ const pending = runtime.docBindings?.busy(`${projectId === 'default' ? UNFILED_MOUNT : projectId}/${doc}`) ?? null;
8807
+ return pending !== null && (pending.launchingAt !== undefined || pending.launchedAt !== undefined || pending.emittedAt !== undefined)
8808
+ ? `a chat proposal is still drafting this document (run ${pending.runId.slice(0, 8)}); wait for it to land, or stop that run first`
8809
+ : null;
8810
+ },
8038
8811
  log: (m) => app.log.warn(m),
8039
8812
  });
8040
8813
  // ── A demo run's video as an editable document (the video editor re-cuts it through E1's per-doc API) ──
@@ -8113,6 +8886,6 @@ runtime = {}) {
8113
8886
  const runEvalsPrepare = async (input) => {
8114
8887
  await prepareRunEvalsLaunch(input, () => runAndRecord(defaultEvalDeps, { project: null, baseline: false, id: randomUUID(), now: new Date() }), runtime.evalRunsDir);
8115
8888
  };
8116
- return { decideGate, decisions, chatRecorder, considerations, search, decorateRun, runEvalsPrepare };
8889
+ return { decideGate, decisions, chatRecorder, considerations, search, decorateRun, runEvalsPrepare, chatProposals, proposalLaunchers };
8117
8890
  }
8118
8891
  //# sourceMappingURL=routes.js.map