wicked-crew 0.7.46 → 0.7.48

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 (263) hide show
  1. package/dist/api/capture.d.ts +2 -2
  2. package/dist/api/chat-citations.d.ts +10 -1
  3. package/dist/api/chat-citations.d.ts.map +1 -1
  4. package/dist/api/chat-citations.js +28 -0
  5. package/dist/api/chat-citations.js.map +1 -1
  6. package/dist/api/chat-scope.d.ts +4 -2
  7. package/dist/api/chat-scope.d.ts.map +1 -1
  8. package/dist/api/chat-scope.js +35 -3
  9. package/dist/api/chat-scope.js.map +1 -1
  10. package/dist/api/chat-transcripts.d.ts +23 -1
  11. package/dist/api/chat-transcripts.d.ts.map +1 -1
  12. package/dist/api/chat-transcripts.js +61 -6
  13. package/dist/api/chat-transcripts.js.map +1 -1
  14. package/dist/api/delivery-index.d.ts +21 -0
  15. package/dist/api/delivery-index.d.ts.map +1 -1
  16. package/dist/api/delivery-index.js +36 -0
  17. package/dist/api/delivery-index.js.map +1 -1
  18. package/dist/api/elicitation-cache.d.ts +12 -0
  19. package/dist/api/elicitation-cache.d.ts.map +1 -1
  20. package/dist/api/elicitation-cache.js +17 -1
  21. package/dist/api/elicitation-cache.js.map +1 -1
  22. package/dist/api/eval-sample.d.ts +12 -12
  23. package/dist/api/governance-steering.d.ts.map +1 -1
  24. package/dist/api/governance-steering.js +1 -0
  25. package/dist/api/governance-steering.js.map +1 -1
  26. package/dist/api/mcp.d.ts +4 -4
  27. package/dist/api/policy-landing.d.ts +55 -0
  28. package/dist/api/policy-landing.d.ts.map +1 -0
  29. package/dist/api/policy-landing.js +174 -0
  30. package/dist/api/policy-landing.js.map +1 -0
  31. package/dist/api/recording.d.ts +228 -0
  32. package/dist/api/recording.d.ts.map +1 -0
  33. package/dist/api/recording.js +1140 -0
  34. package/dist/api/recording.js.map +1 -0
  35. package/dist/api/routes.d.ts +42 -39
  36. package/dist/api/routes.d.ts.map +1 -1
  37. package/dist/api/routes.js +362 -107
  38. package/dist/api/routes.js.map +1 -1
  39. package/dist/api/run-timing-index.d.ts +17 -0
  40. package/dist/api/run-timing-index.d.ts.map +1 -1
  41. package/dist/api/run-timing-index.js +43 -1
  42. package/dist/api/run-timing-index.js.map +1 -1
  43. package/dist/api/server.d.ts +27 -0
  44. package/dist/api/server.d.ts.map +1 -1
  45. package/dist/api/server.js +168 -4
  46. package/dist/api/server.js.map +1 -1
  47. package/dist/api/testing.d.ts +4 -4
  48. package/dist/api/testing.d.ts.map +1 -1
  49. package/dist/api/testing.js +3 -0
  50. package/dist/api/testing.js.map +1 -1
  51. package/dist/api/watch-routes.d.ts +30 -0
  52. package/dist/api/watch-routes.d.ts.map +1 -0
  53. package/dist/api/watch-routes.js +104 -0
  54. package/dist/api/watch-routes.js.map +1 -0
  55. package/dist/campaigns/routes.d.ts +2 -2
  56. package/dist/cli/index.d.ts.map +1 -1
  57. package/dist/cli/index.js +20 -2
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/core/adapter.d.ts +52 -1
  60. package/dist/core/adapter.d.ts.map +1 -1
  61. package/dist/core/adapter.js +193 -5
  62. package/dist/core/adapter.js.map +1 -1
  63. package/dist/core/run-identity.js +1 -1
  64. package/dist/core/run-identity.js.map +1 -1
  65. package/dist/core/walkthrough-root.d.ts +21 -0
  66. package/dist/core/walkthrough-root.d.ts.map +1 -0
  67. package/dist/core/walkthrough-root.js +66 -0
  68. package/dist/core/walkthrough-root.js.map +1 -0
  69. package/dist/decisions/chat-recorder.d.ts +129 -0
  70. package/dist/decisions/chat-recorder.d.ts.map +1 -0
  71. package/dist/decisions/chat-recorder.js +367 -0
  72. package/dist/decisions/chat-recorder.js.map +1 -0
  73. package/dist/decisions/consider.d.ts +102 -0
  74. package/dist/decisions/consider.d.ts.map +1 -0
  75. package/dist/decisions/consider.js +207 -0
  76. package/dist/decisions/consider.js.map +1 -0
  77. package/dist/decisions/derive.d.ts +79 -0
  78. package/dist/decisions/derive.d.ts.map +1 -0
  79. package/dist/decisions/derive.js +282 -0
  80. package/dist/decisions/derive.js.map +1 -0
  81. package/dist/decisions/events.d.ts +25 -0
  82. package/dist/decisions/events.d.ts.map +1 -0
  83. package/dist/decisions/events.js +24 -0
  84. package/dist/decisions/events.js.map +1 -0
  85. package/dist/decisions/ingest.d.ts +51 -0
  86. package/dist/decisions/ingest.d.ts.map +1 -0
  87. package/dist/decisions/ingest.js +100 -0
  88. package/dist/decisions/ingest.js.map +1 -0
  89. package/dist/decisions/land.d.ts +100 -0
  90. package/dist/decisions/land.d.ts.map +1 -0
  91. package/dist/decisions/land.js +466 -0
  92. package/dist/decisions/land.js.map +1 -0
  93. package/dist/decisions/ledger.d.ts +60 -0
  94. package/dist/decisions/ledger.d.ts.map +1 -0
  95. package/dist/decisions/ledger.js +246 -0
  96. package/dist/decisions/ledger.js.map +1 -0
  97. package/dist/decisions/routes.d.ts +10 -0
  98. package/dist/decisions/routes.d.ts.map +1 -0
  99. package/dist/decisions/routes.js +141 -0
  100. package/dist/decisions/routes.js.map +1 -0
  101. package/dist/decisions/rules-text.d.ts +21 -0
  102. package/dist/decisions/rules-text.d.ts.map +1 -0
  103. package/dist/decisions/rules-text.js +50 -0
  104. package/dist/decisions/rules-text.js.map +1 -0
  105. package/dist/decisions/types.d.ts +120 -0
  106. package/dist/decisions/types.d.ts.map +1 -0
  107. package/dist/decisions/types.js +19 -0
  108. package/dist/decisions/types.js.map +1 -0
  109. package/dist/editors/csp.d.ts +31 -0
  110. package/dist/editors/csp.d.ts.map +1 -0
  111. package/dist/editors/csp.js +46 -0
  112. package/dist/editors/csp.js.map +1 -0
  113. package/dist/editors/pack-install.d.ts +28 -0
  114. package/dist/editors/pack-install.d.ts.map +1 -0
  115. package/dist/editors/pack-install.js +153 -0
  116. package/dist/editors/pack-install.js.map +1 -0
  117. package/dist/editors/registry.d.ts +142 -0
  118. package/dist/editors/registry.d.ts.map +1 -0
  119. package/dist/editors/registry.js +255 -0
  120. package/dist/editors/registry.js.map +1 -0
  121. package/dist/editors/routes.d.ts +46 -0
  122. package/dist/editors/routes.d.ts.map +1 -0
  123. package/dist/editors/routes.js +166 -0
  124. package/dist/editors/routes.js.map +1 -0
  125. package/dist/interactive/bridge-pool.d.ts +7 -2
  126. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  127. package/dist/interactive/bridge-pool.js +6 -1
  128. package/dist/interactive/bridge-pool.js.map +1 -1
  129. package/dist/interactive/draft-events.d.ts.map +1 -1
  130. package/dist/interactive/draft-events.js +1 -0
  131. package/dist/interactive/draft-events.js.map +1 -1
  132. package/dist/interactive/theme-events.d.ts +119 -0
  133. package/dist/interactive/theme-events.d.ts.map +1 -0
  134. package/dist/interactive/theme-events.js +606 -0
  135. package/dist/interactive/theme-events.js.map +1 -0
  136. package/dist/projects/events.d.ts.map +1 -1
  137. package/dist/projects/events.js +2 -1
  138. package/dist/projects/events.js.map +1 -1
  139. package/dist/projects/routes.d.ts +2 -2
  140. package/dist/projects/state-home-preflight.d.ts.map +1 -1
  141. package/dist/projects/state-home-preflight.js +3 -0
  142. package/dist/projects/state-home-preflight.js.map +1 -1
  143. package/dist/projects/state-home-registry.d.ts.map +1 -1
  144. package/dist/projects/state-home-registry.js +2 -0
  145. package/dist/projects/state-home-registry.js.map +1 -1
  146. package/dist/projects/state-home.d.ts +6 -0
  147. package/dist/projects/state-home.d.ts.map +1 -1
  148. package/dist/projects/state-home.js +28 -1
  149. package/dist/projects/state-home.js.map +1 -1
  150. package/dist/qe/acceptance.d.ts +45 -4
  151. package/dist/qe/acceptance.d.ts.map +1 -1
  152. package/dist/qe/acceptance.js +93 -4
  153. package/dist/qe/acceptance.js.map +1 -1
  154. package/dist/qe/ledger.d.ts +20 -0
  155. package/dist/qe/ledger.d.ts.map +1 -1
  156. package/dist/qe/ledger.js +34 -0
  157. package/dist/qe/ledger.js.map +1 -1
  158. package/dist/qe/walkthrough-acceptance.d.ts +82 -0
  159. package/dist/qe/walkthrough-acceptance.d.ts.map +1 -0
  160. package/dist/qe/walkthrough-acceptance.js +248 -0
  161. package/dist/qe/walkthrough-acceptance.js.map +1 -0
  162. package/dist/skills/base-skill.d.ts.map +1 -1
  163. package/dist/skills/base-skill.js +3 -2
  164. package/dist/skills/base-skill.js.map +1 -1
  165. package/dist/skills/plugin-source.d.ts +19 -1
  166. package/dist/skills/plugin-source.d.ts.map +1 -1
  167. package/dist/skills/plugin-source.js +31 -2
  168. package/dist/skills/plugin-source.js.map +1 -1
  169. package/dist/skills/runtime.d.ts +11 -2
  170. package/dist/skills/runtime.d.ts.map +1 -1
  171. package/dist/skills/runtime.js +29 -1
  172. package/dist/skills/runtime.js.map +1 -1
  173. package/dist/skills/store.d.ts +14 -1
  174. package/dist/skills/store.d.ts.map +1 -1
  175. package/dist/skills/store.js +19 -5
  176. package/dist/skills/store.js.map +1 -1
  177. package/dist/standing-orders/routes.d.ts +22 -22
  178. package/dist/standing-orders/store.d.ts +10 -10
  179. package/dist/watch/checks/check-failed.d.ts +24 -0
  180. package/dist/watch/checks/check-failed.d.ts.map +1 -0
  181. package/dist/watch/checks/check-failed.js +56 -0
  182. package/dist/watch/checks/check-failed.js.map +1 -0
  183. package/dist/watch/checks/claim-vs-evidence.d.ts +30 -0
  184. package/dist/watch/checks/claim-vs-evidence.d.ts.map +1 -0
  185. package/dist/watch/checks/claim-vs-evidence.js +152 -0
  186. package/dist/watch/checks/claim-vs-evidence.js.map +1 -0
  187. package/dist/watch/checks/deliver-audit.d.ts +19 -0
  188. package/dist/watch/checks/deliver-audit.d.ts.map +1 -0
  189. package/dist/watch/checks/deliver-audit.js +129 -0
  190. package/dist/watch/checks/deliver-audit.js.map +1 -0
  191. package/dist/watch/checks/index.d.ts +8 -0
  192. package/dist/watch/checks/index.d.ts.map +1 -0
  193. package/dist/watch/checks/index.js +24 -0
  194. package/dist/watch/checks/index.js.map +1 -0
  195. package/dist/watch/checks/lag.d.ts +24 -0
  196. package/dist/watch/checks/lag.d.ts.map +1 -0
  197. package/dist/watch/checks/lag.js +55 -0
  198. package/dist/watch/checks/lag.js.map +1 -0
  199. package/dist/watch/checks/quiet-after-claim.d.ts +17 -0
  200. package/dist/watch/checks/quiet-after-claim.d.ts.map +1 -0
  201. package/dist/watch/checks/quiet-after-claim.js +89 -0
  202. package/dist/watch/checks/quiet-after-claim.js.map +1 -0
  203. package/dist/watch/checks/ungated.d.ts +12 -0
  204. package/dist/watch/checks/ungated.d.ts.map +1 -0
  205. package/dist/watch/checks/ungated.js +64 -0
  206. package/dist/watch/checks/ungated.js.map +1 -0
  207. package/dist/watch/checks/warned-rule.d.ts +26 -0
  208. package/dist/watch/checks/warned-rule.d.ts.map +1 -0
  209. package/dist/watch/checks/warned-rule.js +126 -0
  210. package/dist/watch/checks/warned-rule.js.map +1 -0
  211. package/dist/watch/emit.d.ts +122 -0
  212. package/dist/watch/emit.d.ts.map +1 -0
  213. package/dist/watch/emit.js +443 -0
  214. package/dist/watch/emit.js.map +1 -0
  215. package/dist/watch/events.d.ts +21 -0
  216. package/dist/watch/events.d.ts.map +1 -0
  217. package/dist/watch/events.js +21 -0
  218. package/dist/watch/events.js.map +1 -0
  219. package/dist/watch/keys.d.ts +15 -0
  220. package/dist/watch/keys.d.ts.map +1 -0
  221. package/dist/watch/keys.js +23 -0
  222. package/dist/watch/keys.js.map +1 -0
  223. package/dist/watch/lanes.d.ts +41 -0
  224. package/dist/watch/lanes.d.ts.map +1 -0
  225. package/dist/watch/lanes.js +73 -0
  226. package/dist/watch/lanes.js.map +1 -0
  227. package/dist/watch/loader.d.ts +47 -0
  228. package/dist/watch/loader.d.ts.map +1 -0
  229. package/dist/watch/loader.js +178 -0
  230. package/dist/watch/loader.js.map +1 -0
  231. package/dist/watch/registry.d.ts +166 -0
  232. package/dist/watch/registry.d.ts.map +1 -0
  233. package/dist/watch/registry.js +589 -0
  234. package/dist/watch/registry.js.map +1 -0
  235. package/dist/watch/ring.d.ts +32 -0
  236. package/dist/watch/ring.d.ts.map +1 -0
  237. package/dist/watch/ring.js +90 -0
  238. package/dist/watch/ring.js.map +1 -0
  239. package/dist/watch/router.d.ts +25 -0
  240. package/dist/watch/router.d.ts.map +1 -0
  241. package/dist/watch/router.js +87 -0
  242. package/dist/watch/router.js.map +1 -0
  243. package/dist/watch/sources.d.ts +42 -0
  244. package/dist/watch/sources.d.ts.map +1 -0
  245. package/dist/watch/sources.js +104 -0
  246. package/dist/watch/sources.js.map +1 -0
  247. package/dist/watch/types.d.ts +129 -0
  248. package/dist/watch/types.d.ts.map +1 -0
  249. package/dist/watch/types.js +6 -0
  250. package/dist/watch/types.js.map +1 -0
  251. package/endpoint-manifest.json +272 -1
  252. package/package.json +3 -2
  253. package/watch/entries/claim-vs-evidence.json +13 -0
  254. package/watch/entries/deliver-audit.json +17 -0
  255. package/watch/entries/quiet-after-claim.json +16 -0
  256. package/watch/entries/registry-check-failed.json +12 -0
  257. package/watch/entries/registry-lagging.json +12 -0
  258. package/watch/entries/risky-call.json +12 -0
  259. package/watch/entries/ungated.json +13 -0
  260. package/dist/api/demo.d.ts +0 -90
  261. package/dist/api/demo.d.ts.map +0 -1
  262. package/dist/api/demo.js +0 -492
  263. package/dist/api/demo.js.map +0 -1
@@ -38,7 +38,7 @@ import { DEFAULT_STEERING_TYPE, STEERING_TYPE_VALUES, STEERING_TYPES, registerGo
38
38
  import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js';
39
39
  import { ApproveEditSchema, approveEdited, captureLaunchRoots, editInFlight, FileProposalSchema, registerCaptureRoutes } from './capture.js';
40
40
  import { registerTestingRoutes } from './testing.js';
41
- import { DEMO_PRESET, demoLaunchRoots, registerDemoRoutes } from './demo.js';
41
+ import { DEMO_PRESET, demoLaunchRoots, registerDemoRoutes, registerWalkthroughRoutes, walkthroughAcceptance } from './recording.js';
42
42
  import { registerSkillsRoutes } from './skills.js';
43
43
  import { registerMcpRoutes } from './mcp.js';
44
44
  import { McpPolicies } from '../mcp/policies.js';
@@ -46,8 +46,10 @@ import { disabledSkillsHealth } from '../skills/runtime.js';
46
46
  import { phaseSkillFindings, withPhaseSkillGaps } from '../skills/phase-skill-gaps.js';
47
47
  import { STATE_HOME_BLOCKER_CODE, STATE_HOME_REMEDY, isEngineStateHomeRefusal, stateHomeBlockerBody, } from '../projects/state-home-preflight.js';
48
48
  import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../skills/base-skill.js';
49
+ import { REQUIRED_GARDEN_VERSION } from '../skills/plugin-source.js';
49
50
  import { noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
50
51
  import { ProjectSettingsStore } from '../projects/settings.js';
52
+ import { isHumanOperator } from './watch-routes.js';
51
53
  import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
52
54
  import { composeDeliverText, configuredPublicOrigin, extractFollowUps, factsFromRun, FOLLOW_UPS_MAX, framedDeliverText, runUrlFor, } from '../core/deliver-text.js';
53
55
  import { deliverRepoFor, deliverTargetView, isGitHubLogin, readDeliverOriginUrl, resolvePullRequest as resolvePullRequestViaGh, } from '../core/deliver.js';
@@ -69,14 +71,25 @@ import { DeliveryFreeze, frozenRefusal, isDeliverUnit, registerDeliveryFreezeRou
69
71
  import { GroupIndex } from './group-index.js';
70
72
  import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
71
73
  import { GuidanceIndex } from './guidance-index.js';
72
- import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, deliveryRecordFrom, pushedState, canDeliverResolver, } from './delivery-index.js';
74
+ import { DELIVER_REQUESTED_ACTION, DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, deliveryRecordFrom, pushedState, canDeliverResolver, FREE_TEXT_NOTICE, freeTextOutcome, } from './delivery-index.js';
73
75
  import { DeliveryDerivationCache } from './delivery-cache.js';
74
76
  import { gitReprovisionWorktree, runDeliverScript, } from './post-hoc-deliver.js';
75
- import { LOCAL_ACTOR } from './auth.js';
77
+ import { LOCAL_ACTOR, trustAtLeast } from './auth.js';
76
78
  // Re-exported so existing `import { API_PREFIX } from './routes.js'` callers keep working; the
77
79
  // value lives in the leaf module api-prefix.ts to keep unit-output.ts out of this file's cycle.
78
80
  export { API_PREFIX } from './api-prefix.js';
79
81
  import { API_PREFIX } from './api-prefix.js';
82
+ import { landPolicyRule, policyProposalToRule } from './policy-landing.js';
83
+ import { DecisionError, DecisionService } from '../decisions/land.js';
84
+ import { ingestDecision } from '../decisions/ingest.js';
85
+ import { registerDecisionRoutes } from '../decisions/routes.js';
86
+ import { ChatDecisionRecorder } from '../decisions/chat-recorder.js';
87
+ import { ConsiderationService } from '../decisions/consider.js';
88
+ import { gardenPackCheck } from '../editors/pack-install.js';
89
+ import { EditorRegistry } from '../editors/registry.js';
90
+ import { registerEditorRoutes } from '../editors/routes.js';
91
+ import { discoverLivePlugin } from '../skills/plugin-source.js';
92
+ import { resolveDecisionsMode } from '../decisions/types.js';
80
93
  const V = API_PREFIX;
81
94
  /** How long `GET /health` waits on the engine's actor before answering `ping: "busy"` (crew#471). */
82
95
  export const HEALTH_PING_WAIT_MS = 1_000;
@@ -185,80 +198,8 @@ function isStringRecord(v) {
185
198
  !Array.isArray(v) &&
186
199
  Object.values(v).every((x) => typeof x === 'string'));
187
200
  }
188
- /**
189
- * Map an APPROVED policy proposal (estate `proposal.approve` → `handed_off`) into a steering
190
- * ConformanceRule (DES-MEM-FACETED-001 §5.2). A policy proposal's `kind_type` is
191
- * `policy:<steering_type>` and its `payload` is `{ rule, severity }`; estate writes NOTHING for it
192
- * (the AW-11 "no rules.write on estate" invariant) and hands the payload back for crew — the ONE
193
- * governed rules-write path — to land. Returns the rule to upsert plus the resolved steering type,
194
- * or a loud `error` string when the proposal cannot be shaped into a valid rule (a malformed
195
- * `kind_type` / missing `rule` / out-of-enum `severity`) — the caller reports it as a failed
196
- * landing, never a silent drop (the crew#388 anti-silent-loss doctrine).
197
- *
198
- * The rule id is DETERMINISTIC (`proposal:<id>`) so a re-driven landing UPSERTS the same rule
199
- * (idempotent) instead of minting a duplicate; it sits OUTSIDE the reserved `PAT-/POL-` namespace,
200
- * which UI/chat/proposal-authored rules are free to do (INV-C1). The rule carries NO `effect`
201
- * (recall-only — exactly what `{rule, severity}` supports: an enforcement rule would need a
202
- * non-blank `applies_to`, which the payload does not carry, and INV-S3 fails such a rule closed).
203
- * `steering_type` is validated against the vocabulary here so the engine's INV-S1 never rejects it
204
- * as an unknown page.
205
- */
206
- export function policyProposalToRule(proposalId, kindType, payload, facets) {
207
- const steeringType = kindType.startsWith('policy:') ? kindType.slice('policy:'.length) : '';
208
- if (!STEERING_TYPES.has(steeringType)) {
209
- return {
210
- error: `the approved proposal's kind_type ${JSON.stringify(kindType)} does not name a steering ` +
211
- `type — expected \`policy:<${STEERING_TYPE_VALUES.join('|')}>\``,
212
- };
213
- }
214
- const body = typeof payload === 'object' && payload !== null && !Array.isArray(payload)
215
- ? payload
216
- : {};
217
- const statement = typeof body['rule'] === 'string' ? body['rule'].trim() : '';
218
- if (statement === '') {
219
- return {
220
- error: 'the approved policy proposal payload has no `rule` string to make a rule statement from',
221
- };
222
- }
223
- const rawSeverity = body['severity'];
224
- // Normalize common LLM drift before the enum check: a model naturally writes the English word
225
- // "warning", but the engine's enum is the short "warn" (info/error/critical are already the
226
- // natural words). Tolerate case/whitespace too. A capture worker that says "warning" must still
227
- // land its policy — otherwise every derived policy at the middle band silently fails to land.
228
- const normSeverity = typeof rawSeverity === 'string'
229
- ? (() => {
230
- const s = rawSeverity.trim().toLowerCase();
231
- return s === 'warning' ? 'warn' : s;
232
- })()
233
- : rawSeverity;
234
- const severity = normSeverity === 'info' || normSeverity === 'warn' || normSeverity === 'error' || normSeverity === 'critical'
235
- ? normSeverity
236
- : undefined;
237
- if (rawSeverity !== undefined && severity === undefined) {
238
- return {
239
- error: `the approved policy proposal payload has an invalid severity ${JSON.stringify(rawSeverity)} — ` +
240
- 'expected info|warn|error|critical (or the natural "warning" for warn)',
241
- };
242
- }
243
- const language = facets?.['language'];
244
- const rule = {
245
- id: `proposal:${proposalId}`,
246
- rule_type: 'policy',
247
- statement,
248
- // A policy proposal SHOULD carry severity; a missing one defaults to `warn` (the middle band)
249
- // rather than failing the landing, but a present-but-garbage one fails loud above.
250
- severity: severity ?? 'warn',
251
- // REQUIRED engine-side (f32, no serde default, INV-C2 `[0,1]`); the payload carries none, so a
252
- // fixed authority — the same 0.8 the steering-author landing defaults to.
253
- confidence: 0.8,
254
- // Only the `language` facet maps onto the engine's Targets facet object; `repo`/`project`
255
- // facets have no ConformanceRule slot and are dropped (see the route's open question).
256
- targets: typeof language === 'string' && language !== '' ? { language } : {},
257
- provenance: { source: 'proposal', source_kinds: [] },
258
- steering_type: steeringType,
259
- };
260
- return { rule, steeringType: steeringType };
261
- }
201
+ // `policyProposalToRule` lives beside THE landing (policy-landing.ts); re-exported for its callers.
202
+ export { policyProposalToRule } from './policy-landing.js';
262
203
  /** A JSON object whose every value is a number (a coverage breakdown map). */
263
204
  function isNumberRecord(v) {
264
205
  return (typeof v === 'object' &&
@@ -741,6 +682,62 @@ runtime = {}) {
741
682
  // default is the real spawn-per-call `wicked-estate-mcp` client; route tests inject a stub so no
742
683
  // process is ever spawned.
743
684
  const estateTool = runtime.callEstateTool ?? callEstateTool;
685
+ // DC-S4a: decision capture over the structured hosts (gate note, elicitation, inject). Words are
686
+ // recorded post-commit, off the request path, and only from a human actor (decisions/ingest.ts).
687
+ const decisions = runtime.decisionLedger !== undefined
688
+ ? new DecisionService({
689
+ ledger: runtime.decisionLedger,
690
+ mode: runtime.decisionsMode ?? resolveDecisionsMode(),
691
+ authMode: security.authMode,
692
+ estateTool,
693
+ adapter,
694
+ audit,
695
+ emit: runtime.decisionsEmit ?? null,
696
+ ...(runtime.broadcast !== undefined
697
+ ? { broadcast: (frame) => runtime.broadcast?.(frame) }
698
+ : {}),
699
+ log: (m) => app.log.warn(m),
700
+ })
701
+ : null;
702
+ if (decisions !== null)
703
+ registerDecisionRoutes(app, decisions);
704
+ // DC-S4b: the studio chat host. The recorder writes the transcript's `decisions` record and the
705
+ // `chatDecisions` frame, so without the store or the /ws fan-out there is no chat host at all.
706
+ // DC-S7: considered · set aside · cited (unchecked) for chat turns and run units, and how the
707
+ // seats learn the rules (the statement at open, a disclosed preface on the next send after a rule
708
+ // lands). Fail-soft on an engine or a stub without the rule reads.
709
+ const considerations = new ConsiderationService({
710
+ adapter,
711
+ ...(runtime.decisionLedger !== undefined ? { ledger: runtime.decisionLedger } : {}),
712
+ ...(chatTranscripts !== undefined ? { transcripts: chatTranscripts } : {}),
713
+ projectOf: (id) => projects.index.projectOf(id),
714
+ emit: runtime.decisionsEmit ?? null,
715
+ log: (m) => app.log.warn(m),
716
+ });
717
+ // EP-C1 (DES-artifact-editor-plugins §9.1): the editor registry — first-party editors discovered
718
+ // in studio's bundle, flags in `daemon-editors.json` — and its routes. Pack installs are refused
719
+ // until EP-C8, after garden's gate has run and the approval list has been read.
720
+ const editorRegistry = new EditorRegistry({
721
+ ...(runtime.editorsStateHome !== undefined ? { stateHome: runtime.editorsStateHome } : {}),
722
+ ...(runtime.studioRoot !== undefined ? { studioRoot: runtime.studioRoot } : {}),
723
+ log: (m) => app.log.warn(m),
724
+ });
725
+ registerEditorRoutes(app, {
726
+ registry: editorRegistry,
727
+ adapter,
728
+ packCheck: runtime.packCheck ?? gardenPackCheck(discoverLivePlugin()?.path ?? null),
729
+ log: (m) => app.log.warn(m),
730
+ });
731
+ const chatRecorder = decisions !== null && chatTranscripts !== undefined && runtime.broadcast !== undefined
732
+ ? new ChatDecisionRecorder({
733
+ service: decisions,
734
+ transcripts: chatTranscripts,
735
+ broadcast: runtime.broadcast,
736
+ projectOf: (chat) => projects.index.projectOf(chat),
737
+ onTurnRecorded: (chat, turnId) => considerations.onChatTurn(chat, turnId),
738
+ log: (m) => app.log.warn(m),
739
+ })
740
+ : null;
744
741
  /** Repo root for a repo ref, from the registry — shared by the reprovision path below. */
745
742
  const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
746
743
  /** DES-L9: PR number → head branch, through `gh pr view` unless the runtime injected an answerer. */
@@ -875,6 +872,20 @@ runtime = {}) {
875
872
  view.session.launch_actor = launchActor;
876
873
  const state = resolveDelivery(view, conflictStrand);
877
874
  view.session.delivery = state.delivery;
875
+ // crew#762: was delivery asked for? The launch's decision (`deliver: 'pr'`), a post-hoc attempt
876
+ // or a delivery on record says yes; a launch that said `deliver: 'none'` with neither says no.
877
+ // ABSENT when the daemon holds no launch record and no attempt (never fabricated).
878
+ const launchDeliver = runTimingIndex.launchDeliverFor(view.session.id);
879
+ if (launchDeliver === 'pr' || deliveryIndex.wasRequested(view.session.id) || deliveryIndex.isDelivered(view.session.id)) {
880
+ view.session.deliver_requested = true;
881
+ }
882
+ else if (launchDeliver === 'none') {
883
+ view.session.deliver_requested = false;
884
+ }
885
+ // crew#755: a completed repo-less free-text run answered in text and changed nothing — say so.
886
+ const outcome = freeTextOutcome(view);
887
+ if (outcome !== undefined)
888
+ view.session.outcome = outcome;
878
889
  if (state.deliverUrl !== undefined)
879
890
  view.session.deliverUrl = state.deliverUrl;
880
891
  if (state.deliverBranch !== undefined)
@@ -888,6 +899,10 @@ runtime = {}) {
888
899
  const chatGrounded = runTimingIndex.chatGroundedFor(view.session.id);
889
900
  if (chatGrounded !== undefined)
890
901
  view.session.chat_grounded = chatGrounded;
902
+ // C1: the chat the run was launched from, on every run (terminal ones too) — ABSENT otherwise.
903
+ const chatId = runTimingIndex.chatIdFor(view.session.id);
904
+ if (chatId !== undefined)
905
+ view.session.chat_id = chatId;
891
906
  return view;
892
907
  };
893
908
  // Resolved ONCE and shared by the project routes (which read/write `interactiveRoot`) and the
@@ -898,6 +913,17 @@ runtime = {}) {
898
913
  // driving this function directly (tests) has no hooks, so downstream code
899
914
  // still gets the ONE actor shape via this accessor.
900
915
  const actorOf = (req) => req.actor ?? LOCAL_ACTOR;
916
+ // crew#753: the blocking `skills.garden` finding (an installed garden older than the minimum), or
917
+ // `null`. Read from the skills runtime's cached health; no seam ⇒ nothing to judge.
918
+ const gardenBlocker = () => {
919
+ const f = runtime.skills?.health().findings.find((x) => x.kind === 'skills.garden' && x.severity === 'error');
920
+ return f === undefined ? null : { kind: f.kind, severity: f.severity, message: f.message };
921
+ };
922
+ /** The typed 422 a launch or onboard answers while {@link gardenBlocker} holds. */
923
+ const gardenRefusalBody = () => {
924
+ const f = gardenBlocker();
925
+ return f === null ? null : { code: 'garden_required', error: f.message, required: REQUIRED_GARDEN_VERSION };
926
+ };
901
927
  // Liveness — also proves the actor + event pump are up.
902
928
  //
903
929
  // crew#471: the engine's ping is a round trip through its single-writer actor, and a launch that
@@ -927,9 +953,15 @@ runtime = {}) {
927
953
  // F-E2E-030: what this deployment can keep. A composer reads `capabilities.deliverGate`
928
954
  // before it promises "pauses at the deliver gate"; a stub-driven route set without the
929
955
  // probe honestly reports no gate.
930
- const capabilities = typeof adapter.engineCapabilities === 'function'
931
- ? adapter.engineCapabilities()
932
- : { deliverGate: false, revisesPr: false, chatIdOnLaunch: false, seatChipOnCreate: false };
956
+ const capabilities = {
957
+ ...(typeof adapter.engineCapabilities === 'function'
958
+ ? adapter.engineCapabilities()
959
+ : { deliverGate: false, revisesPr: false, chatIdOnLaunch: false, seatChipOnCreate: false }),
960
+ // C1: `AgentSession.chat_id` is served from the daemon's own launch index, whatever the engine.
961
+ runChatId: true,
962
+ // WT-W1: repo-bound launches mint an evidence root only on an addon that takes the field.
963
+ walkthroughRoots: typeof adapter.evidenceRootsSupported === 'function' ? adapter.evidenceRootsSupported() : false,
964
+ };
933
965
  // wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
934
966
  // daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
935
967
  // launch while the state home holds an entry the worker Read fence cannot classify. Re-surveyed
@@ -950,6 +982,8 @@ runtime = {}) {
950
982
  const warnings = [
951
983
  ...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
952
984
  ...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
985
+ // crew#753: a garden older than the minimum is a blocking finding on the first screen too.
986
+ ...[gardenBlocker()].filter((f) => f !== null),
953
987
  ...phaseSkillFindings(runtime.phaseSkills?.gaps() ?? []),
954
988
  // DES-TEAMING-002 T0: the daemon's bus did not open at boot, so the engine was handed none
955
989
  // and runs un-teamed. A daemon-level notice (status stays ok: it serves); the per-run
@@ -1015,6 +1049,8 @@ runtime = {}) {
1015
1049
  // Is the governance evidence LANDING (crew#495): the store, the records on it, the dead
1016
1050
  // letters — with a `governance.deadletter` finding the moment the outbox holds one.
1017
1051
  governance,
1052
+ // DC-S4b: the chat recorder's per-CLI `wicked-decisions` compliance (null without the chat host).
1053
+ decisions: chatRecorder?.diagnostics() ?? null,
1018
1054
  // The state-home classification (wicked-core#411 / crew#497): which state home the worker
1019
1055
  // Read fence classifies, every entry it cannot classify there (top level and skills root),
1020
1056
  // who classified (the engine, or crew's registry copy on an older addon) and whether that
@@ -1369,11 +1405,39 @@ runtime = {}) {
1369
1405
  return message(err);
1370
1406
  }
1371
1407
  };
1408
+ /**
1409
+ * crew#753: what refuses an onboarding launch, judged BEFORE anything is registered — so a
1410
+ * refused onboard leaves no repo row (core has no unregister). The same three intake refusals a
1411
+ * launch meets: a garden older than the minimum (422 `garden_required`), the state-home blocker
1412
+ * (409) and a base skill the handed generation lacks, e.g. no garden at all (422
1413
+ * `base_skill_refused`). `null` when the launch would be admitted.
1414
+ */
1415
+ const onboardRefusal = async () => {
1416
+ const garden = gardenRefusalBody();
1417
+ if (garden !== null)
1418
+ return { status: 422, body: garden };
1419
+ if (runtime.stateHome !== undefined) {
1420
+ const stateHome = await runtime.stateHome.refresh();
1421
+ if (stateHome.refusesLaunches)
1422
+ return { status: 409, body: stateHomeBlockerBody(stateHome) };
1423
+ }
1424
+ const posture = runtime.skills?.baseSkill() ?? null;
1425
+ if (posture !== null && !posture.present && posture.finding !== null) {
1426
+ return {
1427
+ status: 422,
1428
+ body: { code: 'base_skill_refused', error: posture.finding.message, baseSkill: posture, remedy: baseSkillRemedy(posture.inCatalog) },
1429
+ };
1430
+ }
1431
+ return null;
1432
+ };
1372
1433
  app.post(`${V}/repos`, async (req, reply) => {
1373
1434
  const parsed = RegisterRepoSchema.safeParse(req.body);
1374
1435
  if (!parsed.success) {
1375
1436
  return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
1376
1437
  }
1438
+ const refused = await onboardRefusal();
1439
+ if (refused !== null)
1440
+ return reply.code(refused.status).send(refused.body);
1377
1441
  const { name, rootPath, gitUrl, projectId, channel, actor } = parsed.data;
1378
1442
  let repo;
1379
1443
  let runId;
@@ -1416,6 +1480,9 @@ runtime = {}) {
1416
1480
  const body = OnboardBodySchema.safeParse(req.body ?? {});
1417
1481
  if (!body.success)
1418
1482
  return reply.code(400).send(invalidBody(body.error, 'Invalid request body'));
1483
+ const refused = await onboardRefusal();
1484
+ if (refused !== null)
1485
+ return reply.code(refused.status).send(refused.body);
1419
1486
  let runId;
1420
1487
  try {
1421
1488
  runId = await adapter.launchOnboardingRun(repo.id, repo.name);
@@ -1449,7 +1516,7 @@ runtime = {}) {
1449
1516
  return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
1450
1517
  }
1451
1518
  const b = parsed.data;
1452
- // The demo preset launches only from POST /projects/:id/demo (api/demo.ts), which mints the run's
1519
+ // The demo preset launches only from POST /projects/:id/demo (api/recording.ts), which mints the run's
1453
1520
  // demo root and brief first: launched here directly it would have neither.
1454
1521
  if (b.workflow === DEMO_PRESET && (b.sessionId === undefined || !demoLaunchRoots.has(b.sessionId))) {
1455
1522
  return reply.code(400).send({
@@ -1468,6 +1535,10 @@ runtime = {}) {
1468
1535
  return reply.code(409).send(stateHomeBlockerBody(stateHome));
1469
1536
  }
1470
1537
  }
1538
+ // crew#753: a garden older than the minimum refuses the launch by name (found vs required).
1539
+ const garden = gardenRefusalBody();
1540
+ if (garden !== null)
1541
+ return reply.code(422).send(garden);
1471
1542
  // crew#645: an expired login reads signed out BEFORE routing — wait (bounded) for every seat's
1472
1543
  // missing or stale login check, so the standing below is the seats' own answer.
1473
1544
  if (b.clisJson === undefined)
@@ -1485,7 +1556,7 @@ runtime = {}) {
1485
1556
  const captureRoot = captureLaunchRoots.get(input.sessionId);
1486
1557
  if (captureRoot !== undefined)
1487
1558
  input.extraWriteRoots = [captureRoot];
1488
- // A demo launch (api/demo.ts) writes every file into the demo root the demo route minted for it.
1559
+ // A demo launch (api/recording.ts) writes every file into the demo root the demo route minted for it.
1489
1560
  const demoRoot = demoLaunchRoots.get(input.sessionId);
1490
1561
  if (demoRoot !== undefined)
1491
1562
  input.extraWriteRoots = [demoRoot];
@@ -1730,7 +1801,11 @@ runtime = {}) {
1730
1801
  // is always reproducible even if the chat is idle-reclaimed before the run finishes.
1731
1802
  if (b.chatId !== undefined)
1732
1803
  runtime.linkChatRun?.(b.chatId, runId);
1733
- return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}) });
1804
+ // crew#755: a launch that named neither a workflow nor a plan runs the engine's free-text
1805
+ // planner (one unit, the brief verbatim). Say so in the answer, with how to get work done,
1806
+ // instead of letting it pass for a launch of real work.
1807
+ const freeText = input.workflow === undefined && input.plan === undefined ? { freeText: { notice: FREE_TEXT_NOTICE } } : {};
1808
+ return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}), ...freeText });
1734
1809
  }
1735
1810
  catch (err) {
1736
1811
  const msg = message(err);
@@ -2024,6 +2099,10 @@ runtime = {}) {
2024
2099
  });
2025
2100
  }
2026
2101
  deliverInFlight.add(id);
2102
+ // crew#762: the attempt is the durable fact "delivery was requested" — recorded BEFORE the
2103
+ // script runs, so a lift that then fails still reads requested after a reload or restart.
2104
+ audit.record(DELIVER_REQUESTED_ACTION, actorOf(req), { runId: id, detail: { via: 'post-hoc' } });
2105
+ deliveryIndex.markRequested(id);
2027
2106
  // The worktree the script runs in. Usually the run's own; but the engine REAPS a
2028
2107
  // failed-deliver run's worktree once its work is committed (crew#418/#432) — the work then lives
2029
2108
  // on the `wicked/<id>` branch. For such a strand, stand a throwaway worktree back up from
@@ -2254,6 +2333,8 @@ runtime = {}) {
2254
2333
  : `chat ${chatId} is already open on this daemon; DELETE /chats/${chatId} first, or omit chatId to mint a fresh one`,
2255
2334
  });
2256
2335
  }
2336
+ // DC-S7: the in-force rules the statement lists; recorded as "told" only once the open succeeds.
2337
+ let inForceAtOpen = [];
2257
2338
  // Everything below either ends in `chatScopes.set(chatId, …, token)` or releases the reservation.
2258
2339
  try {
2259
2340
  // The scope (crew#502): explicit repos (`repoRefs`, the legacy `repoRef` merged in) or the
@@ -2280,7 +2361,10 @@ runtime = {}) {
2280
2361
  const { scope, engine } = resolution;
2281
2362
  try {
2282
2363
  // Cleans up only what it created itself on failure (Copilot, #518).
2283
- prepareChatScratch(chatId, scope);
2364
+ // DC-S7: the seats learn the project's in-force rules from the statement (cap 20, severity
2365
+ // order); the service remembers what they were told so a later landing can be prefaced.
2366
+ inForceAtOpen = (await considerations.inForceFor(scope.projectId ?? null)).rules;
2367
+ prepareChatScratch(chatId, scope, inForceAtOpen);
2284
2368
  }
2285
2369
  catch (err) {
2286
2370
  chatScopes.release(chatId, token);
@@ -2459,6 +2543,9 @@ runtime = {}) {
2459
2543
  const roots = scope.repos.map((r) => ({ absRoot: resolve(r.rootPath), name: r.name }));
2460
2544
  chatTranscripts.registerRoots(chatId, roots);
2461
2545
  }
2546
+ // DC-S7: only a chat that actually OPENED is preface-eligible — recorded here, after the scope
2547
+ // is published, so a refused open leaves no per-chat state behind (codex r1).
2548
+ considerations.noteChatOpen(chatId, inForceAtOpen);
2462
2549
  // The thread learns of every refused seat the way it learns of everything else — a frame
2463
2550
  // on /ws — AFTER the scope is published, so a reader never sees a refusal for a chat it
2464
2551
  // cannot yet look up.
@@ -2549,12 +2636,20 @@ runtime = {}) {
2549
2636
  // Sync, same tick as `inFlight`: the window is closed before anything yields.
2550
2637
  const turn = chatTurns.begin(id, audience, parsed.data.text);
2551
2638
  try {
2552
- const seats = await adapter.chatSend(id, parsed.data.text, parsed.data.targets);
2639
+ // DC-S7: a rule remembered since the seats last heard from crew rides this send as a
2640
+ // crew-authored preface — disclosed below as a `system` transcript record; the `user` record
2641
+ // keeps the operator's own words.
2642
+ const preface = await considerations.prefaceForSend(id);
2643
+ const seats = await adapter.chatSend(id, preface !== null ? `${preface}\n\n${parsed.data.text}` : parsed.data.text, parsed.data.targets);
2553
2644
  if (turn !== null) {
2554
2645
  // The engine's answer is the truth: the reserved audience is squared with the seats it
2555
2646
  // reached, and the operator's message joins the transcript with exactly those seats.
2556
2647
  chatTurns.reconcile(id, turn.turnId, seats);
2557
2648
  chatTranscripts?.appendUser(id, turn.turnId, parsed.data.text, seats);
2649
+ // DC-S4b: who said it, to whom — the recorder matches the seats' quotes against THIS message.
2650
+ chatRecorder?.noteSend(id, turn.turnId, actorOf(req), parsed.data.text, seats);
2651
+ if (preface !== null)
2652
+ chatTranscripts?.appendSystem(id, turn.turnId, preface);
2558
2653
  }
2559
2654
  // crew#641: re-state single-seat degradation on every turn so it is visible in the transcript.
2560
2655
  // Decided from `warmRoster` — the seats that are WARM — never from `seats`, which is only the
@@ -2744,6 +2839,40 @@ runtime = {}) {
2744
2839
  return reply.send({ output });
2745
2840
  return reply.send({ output: null, outputUnavailable: outputUnavailableReason(unit) });
2746
2841
  });
2842
+ // DC-S7 (§4.6): what the seats considered, set aside and cited for one chat turn or one unit
2843
+ // attempt. Operator+ (the rules are the project's doctrine). A cited rule is "unchecked", never
2844
+ // "followed" (B4). 404 when the transcript holds no such turn, or the run has no such unit.
2845
+ const operatorOnlyRead = (req, reply) => {
2846
+ const actor = actorOf(req);
2847
+ if (trustAtLeast(actor, 'operator'))
2848
+ return true;
2849
+ void reply.code(403).send({ error: `Insufficient trust: considered needs 'operator' (you are '${actor.trust}')` });
2850
+ return false;
2851
+ };
2852
+ app.get(`${V}/chats/:id/turns/:turnId/considered`, { config: { manifest: { responseType: 'Consideration', statusCodes: [200, 403, 404] } } }, async (req, reply) => {
2853
+ if (!operatorOnlyRead(req, reply))
2854
+ return reply;
2855
+ const { id, turnId } = req.params;
2856
+ const c = await considerations.forChatTurn(id, turnId);
2857
+ if (c === null)
2858
+ return reply.code(404).send({ error: `chat ${id} holds no turn ${turnId}` });
2859
+ return c;
2860
+ });
2861
+ app.get(`${V}/runs/:id/units/:unitKey/considered`, { config: { manifest: { responseType: 'Consideration', statusCodes: [200, 400, 403, 404] } } }, async (req, reply) => {
2862
+ if (!operatorOnlyRead(req, reply))
2863
+ return reply;
2864
+ const { id, unitKey } = req.params;
2865
+ const raw = req.query.attempt;
2866
+ const attemptRaw = Array.isArray(raw) ? raw[0] : raw;
2867
+ const attempt = attemptRaw === undefined || attemptRaw === '' ? 0 : Number(attemptRaw);
2868
+ if (!Number.isInteger(attempt) || attempt < 0) {
2869
+ return reply.code(400).send({ error: '`attempt` must be a non-negative integer' });
2870
+ }
2871
+ const c = await considerations.forUnit(id, unitKey, attempt);
2872
+ if (c === null)
2873
+ return reply.code(404).send({ error: `run ${id} has no unit '${unitKey}'` });
2874
+ return c;
2875
+ });
2747
2876
  // The whole run as one auditable JSON attachment: the run, its units (each with
2748
2877
  // the captured transcript), and the decision trail read back from core's durable
2749
2878
  // per-run event log — what actually happened, not a re-derivation of it.
@@ -2782,7 +2911,10 @@ runtime = {}) {
2782
2911
  // catalog entry re-verifies evidence, a registered workflow's def, nothing for free text — and
2783
2912
  // an unknown run (or one the daemon cannot read) is declared and denied with the reason.
2784
2913
  const verifiedCatalog = typeof adapter.verifiedEvidenceCatalog === 'function' ? await adapter.verifiedEvidenceCatalog() : null;
2785
- const requirement = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog);
2914
+ const knownCatalog = typeof adapter.catalogIds === 'function' ? await adapter.catalogIds() : null;
2915
+ const requirement = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog, knownCatalog);
2916
+ // WT-W2: every walkthrough step the requirement names is resolved from its proof root and seal.
2917
+ const walkthroughs = await walkthroughAcceptance(adapter, run, requirement.phases);
2786
2918
  let repo = null;
2787
2919
  if (run.session.repo_ref !== null) {
2788
2920
  const repos = await adapter.listRepos();
@@ -2799,6 +2931,7 @@ runtime = {}) {
2799
2931
  // unit both appear on the page humans actually look at.
2800
2932
  claims: () => adapter.listConformanceClaims(),
2801
2933
  events: (rid) => adapter.runEvents(rid),
2934
+ ...(walkthroughs.gates.length > 0 ? { walkthroughs } : {}),
2802
2935
  });
2803
2936
  });
2804
2937
  /**
@@ -2920,6 +3053,19 @@ runtime = {}) {
2920
3053
  status,
2921
3054
  },
2922
3055
  });
3056
+ // DC-S4a host: the gate note (amend / request_changes text) — a bare approve or reject is a
3057
+ // `choice`. Post-commit, off the request path; a non-human actor (a standing order) records nothing.
3058
+ if (decisions !== null) {
3059
+ decisions.track(ingestDecision(decisions, {
3060
+ host: 'gate',
3061
+ actor,
3062
+ words: parsed.data.amend ?? '',
3063
+ choice: parsed.data.action ?? (parsed.data.approve ? 'approve' : 'reject'),
3064
+ projectId: projects.index.projectOf(id) ?? null,
3065
+ runId: id,
3066
+ ...(parsed.data.ord !== undefined ? { ord: parsed.data.ord } : {}),
3067
+ }));
3068
+ }
2923
3069
  // APPROVE of the steering-author propose gate = the doctrine's landing moment: the
2924
3070
  // approved proposal is written to the governance store with `provenance.source: "chat"`,
2925
3071
  // audited per rule, idempotent on replay, and LOUD on failure — the response carries the
@@ -3046,6 +3192,16 @@ runtime = {}) {
3046
3192
  try {
3047
3193
  await adapter.injectWorkerMessage(id, parsed.data.message, parsed.data.target);
3048
3194
  audit.record('run.injected', actorOf(req), { runId: id, detail: { target: parsed.data.target } });
3195
+ // DC-S4a host: the operator's mid-run steering text (review S9).
3196
+ if (decisions !== null) {
3197
+ decisions.track(ingestDecision(decisions, {
3198
+ host: 'inject',
3199
+ actor: actorOf(req),
3200
+ words: parsed.data.message,
3201
+ projectId: projects.index.projectOf(id) ?? null,
3202
+ runId: id,
3203
+ }));
3204
+ }
3049
3205
  return reply.send({ status: 'ok' });
3050
3206
  }
3051
3207
  catch (err) {
@@ -3298,6 +3454,19 @@ runtime = {}) {
3298
3454
  runId: id,
3299
3455
  detail: { elicitationId: taken.entry.elicitationId, action: body.action },
3300
3456
  });
3457
+ // DC-S4a host: a free-text answer is words; a picked option (or a decline) is a `choice`.
3458
+ if (decisions !== null) {
3459
+ const picked = taken.entry.options !== null;
3460
+ decisions.track(ingestDecision(decisions, {
3461
+ host: 'elicitation',
3462
+ actor: actorOf(req),
3463
+ words: response !== null && !picked ? response : '',
3464
+ ...(response !== null && picked ? { choice: response } : body.action !== 'accept' ? { choice: body.action } : {}),
3465
+ projectId: projects.index.projectOf(id) ?? null,
3466
+ runId: id,
3467
+ elicitationId: taken.entry.elicitationId,
3468
+ }));
3469
+ }
3301
3470
  return { status: 'resolved' };
3302
3471
  });
3303
3472
  // The durable history of one run (FINDING-057).
@@ -3656,6 +3825,21 @@ runtime = {}) {
3656
3825
  });
3657
3826
  }
3658
3827
  }
3828
+ // DC-S3: `targets.project` and `supersedes` shipped with `considerRules` (wicked-core-ts
3829
+ // 0.7.35). An older engine would accept and drop them, so a project rule would land
3830
+ // everywhere: refuse, naming the fields (codex on DC-S3).
3831
+ if (!adapter.projectRulesSupported() && rule !== null && typeof rule === 'object') {
3832
+ const carried = [
3833
+ ...(rule.targets?.project !== undefined ? ['targets.project'] : []),
3834
+ ...(rule.supersedes !== undefined ? ['supersedes'] : []),
3835
+ ];
3836
+ if (carried.length > 0) {
3837
+ return reply.code(501).send({
3838
+ error: `the installed engine would silently drop ${carried.map((f) => `\`${f}\``).join(', ')} ` +
3839
+ 'and land the rule everywhere; upgrade wicked-core-ts (>= 0.7.35)',
3840
+ });
3841
+ }
3842
+ }
3659
3843
  await adapter.upsertConformanceRule(rule);
3660
3844
  audit.record('governance.rule.upserted', actorOf(req), { detail: { id: rule?.id } });
3661
3845
  return { status: 'ok' };
@@ -4332,6 +4516,26 @@ runtime = {}) {
4332
4516
  });
4333
4517
  }
4334
4518
  }
4519
+ // The watch registry's switches (DES-TRIGGER-REGISTRY-001 §4.2, TR-W5a): only `enabled` and
4520
+ // `threshold` of a shipped entry, and `llm`. Turning a watcher off is the one way to make the
4521
+ // platform stop telling the operator something, so only a PERSON may do it — the "human"
4522
+ // predicate decision capture uses; a worker's bearer gets 403. A bad patch is a 400 naming the
4523
+ // key (never a silent drop), and the daemon stamps who changed what and when.
4524
+ let watchNext;
4525
+ if (Object.hasOwn(patch, 'watch')) {
4526
+ const actor = actorOf(req);
4527
+ if (!isHumanOperator(actor)) {
4528
+ return reply.code(403).send({ error: 'Only a person can change what is watched (watch.* settings); a worker or system actor cannot' });
4529
+ }
4530
+ const registry = runtime.watchRegistry?.() ?? null;
4531
+ if (registry === null) {
4532
+ return reply.code(400).send({ error: 'watch: watching is not configured on this daemon' });
4533
+ }
4534
+ const problem = registry.validateSettingsPatch(patch.watch);
4535
+ if (problem !== null)
4536
+ return reply.code(400).send({ error: problem });
4537
+ watchNext = registry.mergeSettings((await adapter.getSettings()).watch, patch.watch, actor.id, Date.now());
4538
+ }
4335
4539
  // Only known engine keys and validated `studio.*` keys are persisted. A key that is
4336
4540
  // NEITHER is dropped, not refused: request bodies are forward-additive too (DES-STUDIO-001
4337
4541
  // §5.1), so an older daemon meeting a newer client's engine key must not fail the whole
@@ -4364,11 +4568,15 @@ runtime = {}) {
4364
4568
  for (const key of studioKeys) {
4365
4569
  safe[key] = patch[key];
4366
4570
  }
4571
+ if (watchNext !== undefined)
4572
+ safe.watch = watchNext;
4367
4573
  // `Object.hasOwn`, NOT `k in safe`: `in` walks the prototype chain, so a dropped key named
4368
4574
  // `toString` / `valueOf` / `constructor` would test as "kept" and vanish from `ignored` —
4369
4575
  // the exact silent drop this route exists to end.
4370
4576
  const ignored = Object.keys(patch).filter((k) => !Object.hasOwn(safe, k));
4371
4577
  const settings = await adapter.updateSettings(safe);
4578
+ if (watchNext !== undefined)
4579
+ runtime.watchRegistry?.()?.applySettings(settings.watch);
4372
4580
  // Re-apply the worker-config root to this process's env (seat sign-in). The engine reads
4373
4581
  // WICKED_WORKER_HOME per worker spawn — never cached — so this alone makes the change live
4374
4582
  // at the next spawn: no daemon restart, no engine restart.
@@ -4383,7 +4591,12 @@ runtime = {}) {
4383
4591
  // `changed` names every persisted key, engine and `studio.*` alike; `ignored` (present only
4384
4592
  // when there is one) is where a dropped unknown key stops being invisible.
4385
4593
  audit.record('settings.updated', actorOf(req), {
4386
- detail: { changed: Object.keys(safe), ...(ignored.length > 0 ? { ignored } : {}) },
4594
+ detail: {
4595
+ changed: Object.keys(safe),
4596
+ ...(ignored.length > 0 ? { ignored } : {}),
4597
+ // Which watchers were switched or re-thresholded (health reads the stamps; this is the trail).
4598
+ ...(watchNext !== undefined ? { watch: patch.watch } : {}),
4599
+ },
4387
4600
  });
4388
4601
  return { settings };
4389
4602
  });
@@ -4514,6 +4727,46 @@ runtime = {}) {
4514
4727
  if (editInFlight(id)) {
4515
4728
  return reply.code(409).send({ error: `proposal ${id} is already being accepted` });
4516
4729
  }
4730
+ // DC-S4a: a proposal THIS ledger filed for a decision lands through `remember()` — the one
4731
+ // landing path, with the human check inside it (§4.5). A payload naming an id the ledger does
4732
+ // not know is an ordinary policy proposal (no ORIGIN claim) and takes the path below.
4733
+ let decisionId = decisions?.deps.ledger.decisionForProposal(id) ?? null;
4734
+ if (decisions !== null && decisionId === null) {
4735
+ // The ledger links a proposal to its decision only once the `offered` outcome is written; a
4736
+ // proposal crew filed but has not linked yet (the publish window, or a crash in it) names a
4737
+ // decision this ledger HOLDS — it must still land through `remember()` (codex on DC-S4a).
4738
+ try {
4739
+ const pending = (await estateTool('proposal.list', { state: 'pending' }));
4740
+ const named = pending.proposals.find((p) => p.id === id)?.payload?.decision?.id;
4741
+ if (typeof named === 'string' && decisions.deps.ledger.record(named) !== null)
4742
+ decisionId = named;
4743
+ }
4744
+ catch (err) {
4745
+ return estateUpstreamError(reply, err);
4746
+ }
4747
+ }
4748
+ if (decisions !== null && decisionId !== null) {
4749
+ try {
4750
+ const landed = await decisions.remember(decisionId, actorOf(req), { how: 'needs-you' });
4751
+ const view = decisions.deps.ledger.view(decisionId);
4752
+ const landing = {
4753
+ outcome: 'landed',
4754
+ ruleId: landed.rule_id,
4755
+ ...(view !== null ? { steering_type: view.edits?.steering_type ?? view.derived.steering_type } : {}),
4756
+ ...(landed.project !== undefined ? { project: landed.project } : {}),
4757
+ };
4758
+ return {
4759
+ outcome: 'handed_off',
4760
+ payload: { rule: view?.edits?.statement ?? view?.derived.statement ?? '', severity: 'warn', capture: 'decision', decision: { id: decisionId } },
4761
+ landing,
4762
+ };
4763
+ }
4764
+ catch (err) {
4765
+ if (err instanceof DecisionError)
4766
+ return reply.code(err.status).send({ error: err.message });
4767
+ throw err;
4768
+ }
4769
+ }
4517
4770
  let approved;
4518
4771
  try {
4519
4772
  approved = (await estateTool('proposal.approve', { id }));
@@ -4561,26 +4814,13 @@ runtime = {}) {
4561
4814
  if ('error' in built) {
4562
4815
  return landFailed(built.error);
4563
4816
  }
4564
- try {
4565
- await adapter.upsertConformanceRule(built.rule);
4566
- }
4567
- catch (err) {
4568
- return landFailed(`the store refused the steering rule derived from the approved policy proposal: ${message(err)}`);
4569
- }
4570
- audit.record('governance.rule.upserted', actorOf(req), {
4571
- detail: {
4572
- id: built.rule.id,
4573
- source: 'proposal',
4574
- via: 'proposal-approve',
4575
- steeringType: built.steeringType,
4576
- proposalId: id,
4577
- },
4817
+ // THE landing (probe, upsert, read-back, audit) — shared with the decision landing (DC-S4a).
4818
+ const landing = await landPolicyRule({ adapter, audit }, built, actorOf(req), {
4819
+ via: 'proposal-approve',
4820
+ proposalId: id,
4578
4821
  });
4579
- const landing = {
4580
- outcome: 'landed',
4581
- ruleId: built.rule.id,
4582
- steering_type: built.steeringType,
4583
- };
4822
+ if (landing.outcome === 'failed')
4823
+ return landFailed(landing.error ?? 'the landing failed');
4584
4824
  return { outcome: 'handed_off', payload: approved.payload, landing };
4585
4825
  });
4586
4826
  // POST /proposals/:id/reject → proposal.reject → { ok: true }.
@@ -4594,6 +4834,19 @@ runtime = {}) {
4594
4834
  if (editInFlight(id.trim())) {
4595
4835
  return reply.code(409).send({ error: `proposal ${id.trim()} is being accepted` });
4596
4836
  }
4837
+ // DC-S4a B12: rejecting a decision's proposal dismisses the decision too (one object, two places).
4838
+ const rejectedDecision = decisions?.deps.ledger.decisionForProposal(id.trim()) ?? null;
4839
+ if (decisions !== null && rejectedDecision !== null) {
4840
+ try {
4841
+ await decisions.dismiss(rejectedDecision, actorOf(req), 'not-a-rule');
4842
+ return { ok: true };
4843
+ }
4844
+ catch (err) {
4845
+ if (err instanceof DecisionError)
4846
+ return reply.code(err.status).send({ error: err.message });
4847
+ throw err;
4848
+ }
4849
+ }
4597
4850
  try {
4598
4851
  return (await estateTool('proposal.reject', { id }));
4599
4852
  }
@@ -4814,6 +5067,8 @@ runtime = {}) {
4814
5067
  registerCaptureRoutes(app);
4815
5068
  // The Demo experience (studio#373): the demo preset's launch, view, files and script edit.
4816
5069
  registerDemoRoutes(app, adapter);
5070
+ // WT-W3: the storyline PUT checks the open gate through the same resolution the gate routes use.
5071
+ registerWalkthroughRoutes(app, adapter, { resolveOpenGate, audit });
4817
5072
  // ── Presets (DES-TEAMING-002 §8.4, seam C2) — saved phase selections in the engine's store;
4818
5073
  // a launch names one via `workflow`, and the ENGINE resolves it.
4819
5074
  registerPresetRoutes(app, adapter, security);
@@ -4959,6 +5214,6 @@ runtime = {}) {
4959
5214
  pool: interactiveBridges,
4960
5215
  log: (m) => app.log.warn(m),
4961
5216
  });
4962
- return { decideGate };
5217
+ return { decideGate, decisions, chatRecorder, considerations };
4963
5218
  }
4964
5219
  //# sourceMappingURL=routes.js.map