wicked-crew 0.7.45 → 0.7.47

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 (215) hide show
  1. package/dist/api/capture.d.ts +2 -2
  2. package/dist/api/delivery-index.d.ts +21 -0
  3. package/dist/api/delivery-index.d.ts.map +1 -1
  4. package/dist/api/delivery-index.js +36 -0
  5. package/dist/api/delivery-index.js.map +1 -1
  6. package/dist/api/elicitation-cache.d.ts +12 -0
  7. package/dist/api/elicitation-cache.d.ts.map +1 -1
  8. package/dist/api/elicitation-cache.js +17 -1
  9. package/dist/api/elicitation-cache.js.map +1 -1
  10. package/dist/api/eval-sample.d.ts +6 -6
  11. package/dist/api/governance-steering.d.ts.map +1 -1
  12. package/dist/api/governance-steering.js +1 -0
  13. package/dist/api/governance-steering.js.map +1 -1
  14. package/dist/api/mcp.d.ts +10 -10
  15. package/dist/api/policy-landing.d.ts +55 -0
  16. package/dist/api/policy-landing.d.ts.map +1 -0
  17. package/dist/api/policy-landing.js +174 -0
  18. package/dist/api/policy-landing.js.map +1 -0
  19. package/dist/api/recording.d.ts +228 -0
  20. package/dist/api/recording.d.ts.map +1 -0
  21. package/dist/api/recording.js +1140 -0
  22. package/dist/api/recording.js.map +1 -0
  23. package/dist/api/roster-standing.d.ts.map +1 -1
  24. package/dist/api/roster-standing.js +5 -1
  25. package/dist/api/roster-standing.js.map +1 -1
  26. package/dist/api/routes.d.ts +31 -39
  27. package/dist/api/routes.d.ts.map +1 -1
  28. package/dist/api/routes.js +264 -104
  29. package/dist/api/routes.js.map +1 -1
  30. package/dist/api/run-timing-index.d.ts +17 -0
  31. package/dist/api/run-timing-index.d.ts.map +1 -1
  32. package/dist/api/run-timing-index.js +43 -1
  33. package/dist/api/run-timing-index.js.map +1 -1
  34. package/dist/api/seat-health.d.ts +35 -0
  35. package/dist/api/seat-health.d.ts.map +1 -1
  36. package/dist/api/seat-health.js +72 -0
  37. package/dist/api/seat-health.js.map +1 -1
  38. package/dist/api/seat-standing.d.ts +2 -2
  39. package/dist/api/seat-standing.d.ts.map +1 -1
  40. package/dist/api/seat-standing.js +27 -7
  41. package/dist/api/seat-standing.js.map +1 -1
  42. package/dist/api/server.d.ts +12 -0
  43. package/dist/api/server.d.ts.map +1 -1
  44. package/dist/api/server.js +74 -1
  45. package/dist/api/server.js.map +1 -1
  46. package/dist/api/testing.d.ts +4 -4
  47. package/dist/api/testing.d.ts.map +1 -1
  48. package/dist/api/testing.js +3 -0
  49. package/dist/api/testing.js.map +1 -1
  50. package/dist/api/watch-routes.d.ts +30 -0
  51. package/dist/api/watch-routes.d.ts.map +1 -0
  52. package/dist/api/watch-routes.js +104 -0
  53. package/dist/api/watch-routes.js.map +1 -0
  54. package/dist/campaigns/routes.d.ts +2 -2
  55. package/dist/cli/index.d.ts.map +1 -1
  56. package/dist/cli/index.js +4 -1
  57. package/dist/cli/index.js.map +1 -1
  58. package/dist/core/adapter.d.ts +36 -0
  59. package/dist/core/adapter.d.ts.map +1 -1
  60. package/dist/core/adapter.js +177 -5
  61. package/dist/core/adapter.js.map +1 -1
  62. package/dist/core/run-identity.js +1 -1
  63. package/dist/core/run-identity.js.map +1 -1
  64. package/dist/core/walkthrough-root.d.ts +21 -0
  65. package/dist/core/walkthrough-root.d.ts.map +1 -0
  66. package/dist/core/walkthrough-root.js +66 -0
  67. package/dist/core/walkthrough-root.js.map +1 -0
  68. package/dist/decisions/derive.d.ts +79 -0
  69. package/dist/decisions/derive.d.ts.map +1 -0
  70. package/dist/decisions/derive.js +282 -0
  71. package/dist/decisions/derive.js.map +1 -0
  72. package/dist/decisions/events.d.ts +23 -0
  73. package/dist/decisions/events.d.ts.map +1 -0
  74. package/dist/decisions/events.js +22 -0
  75. package/dist/decisions/events.js.map +1 -0
  76. package/dist/decisions/ingest.d.ts +28 -0
  77. package/dist/decisions/ingest.d.ts.map +1 -0
  78. package/dist/decisions/ingest.js +92 -0
  79. package/dist/decisions/ingest.js.map +1 -0
  80. package/dist/decisions/land.d.ts +100 -0
  81. package/dist/decisions/land.d.ts.map +1 -0
  82. package/dist/decisions/land.js +466 -0
  83. package/dist/decisions/land.js.map +1 -0
  84. package/dist/decisions/ledger.d.ts +60 -0
  85. package/dist/decisions/ledger.d.ts.map +1 -0
  86. package/dist/decisions/ledger.js +246 -0
  87. package/dist/decisions/ledger.js.map +1 -0
  88. package/dist/decisions/routes.d.ts +10 -0
  89. package/dist/decisions/routes.d.ts.map +1 -0
  90. package/dist/decisions/routes.js +141 -0
  91. package/dist/decisions/routes.js.map +1 -0
  92. package/dist/decisions/types.d.ts +120 -0
  93. package/dist/decisions/types.d.ts.map +1 -0
  94. package/dist/decisions/types.js +19 -0
  95. package/dist/decisions/types.js.map +1 -0
  96. package/dist/interactive/draft-events.d.ts.map +1 -1
  97. package/dist/interactive/draft-events.js +1 -0
  98. package/dist/interactive/draft-events.js.map +1 -1
  99. package/dist/projects/events.d.ts.map +1 -1
  100. package/dist/projects/events.js +2 -1
  101. package/dist/projects/events.js.map +1 -1
  102. package/dist/projects/state-home-preflight.d.ts.map +1 -1
  103. package/dist/projects/state-home-preflight.js +3 -0
  104. package/dist/projects/state-home-preflight.js.map +1 -1
  105. package/dist/projects/state-home-registry.d.ts.map +1 -1
  106. package/dist/projects/state-home-registry.js +2 -0
  107. package/dist/projects/state-home-registry.js.map +1 -1
  108. package/dist/projects/state-home.d.ts +6 -0
  109. package/dist/projects/state-home.d.ts.map +1 -1
  110. package/dist/projects/state-home.js +28 -1
  111. package/dist/projects/state-home.js.map +1 -1
  112. package/dist/qe/acceptance.d.ts +45 -4
  113. package/dist/qe/acceptance.d.ts.map +1 -1
  114. package/dist/qe/acceptance.js +93 -4
  115. package/dist/qe/acceptance.js.map +1 -1
  116. package/dist/qe/ledger.d.ts +20 -0
  117. package/dist/qe/ledger.d.ts.map +1 -1
  118. package/dist/qe/ledger.js +34 -0
  119. package/dist/qe/ledger.js.map +1 -1
  120. package/dist/qe/walkthrough-acceptance.d.ts +82 -0
  121. package/dist/qe/walkthrough-acceptance.d.ts.map +1 -0
  122. package/dist/qe/walkthrough-acceptance.js +248 -0
  123. package/dist/qe/walkthrough-acceptance.js.map +1 -0
  124. package/dist/skills/base-skill.d.ts.map +1 -1
  125. package/dist/skills/base-skill.js +3 -2
  126. package/dist/skills/base-skill.js.map +1 -1
  127. package/dist/skills/plugin-source.d.ts +19 -1
  128. package/dist/skills/plugin-source.d.ts.map +1 -1
  129. package/dist/skills/plugin-source.js +31 -2
  130. package/dist/skills/plugin-source.js.map +1 -1
  131. package/dist/skills/runtime.d.ts +11 -2
  132. package/dist/skills/runtime.d.ts.map +1 -1
  133. package/dist/skills/runtime.js +29 -1
  134. package/dist/skills/runtime.js.map +1 -1
  135. package/dist/skills/store.d.ts +14 -1
  136. package/dist/skills/store.d.ts.map +1 -1
  137. package/dist/skills/store.js +19 -5
  138. package/dist/skills/store.js.map +1 -1
  139. package/dist/standing-orders/routes.d.ts +4 -4
  140. package/dist/standing-orders/store.d.ts +2 -2
  141. package/dist/watch/checks/check-failed.d.ts +24 -0
  142. package/dist/watch/checks/check-failed.d.ts.map +1 -0
  143. package/dist/watch/checks/check-failed.js +56 -0
  144. package/dist/watch/checks/check-failed.js.map +1 -0
  145. package/dist/watch/checks/deliver-audit.d.ts +19 -0
  146. package/dist/watch/checks/deliver-audit.d.ts.map +1 -0
  147. package/dist/watch/checks/deliver-audit.js +129 -0
  148. package/dist/watch/checks/deliver-audit.js.map +1 -0
  149. package/dist/watch/checks/index.d.ts +8 -0
  150. package/dist/watch/checks/index.d.ts.map +1 -0
  151. package/dist/watch/checks/index.js +19 -0
  152. package/dist/watch/checks/index.js.map +1 -0
  153. package/dist/watch/checks/lag.d.ts +24 -0
  154. package/dist/watch/checks/lag.d.ts.map +1 -0
  155. package/dist/watch/checks/lag.js +55 -0
  156. package/dist/watch/checks/lag.js.map +1 -0
  157. package/dist/watch/checks/quiet-after-claim.d.ts +17 -0
  158. package/dist/watch/checks/quiet-after-claim.d.ts.map +1 -0
  159. package/dist/watch/checks/quiet-after-claim.js +89 -0
  160. package/dist/watch/checks/quiet-after-claim.js.map +1 -0
  161. package/dist/watch/checks/ungated.d.ts +12 -0
  162. package/dist/watch/checks/ungated.d.ts.map +1 -0
  163. package/dist/watch/checks/ungated.js +64 -0
  164. package/dist/watch/checks/ungated.js.map +1 -0
  165. package/dist/watch/emit.d.ts +122 -0
  166. package/dist/watch/emit.d.ts.map +1 -0
  167. package/dist/watch/emit.js +443 -0
  168. package/dist/watch/emit.js.map +1 -0
  169. package/dist/watch/events.d.ts +21 -0
  170. package/dist/watch/events.d.ts.map +1 -0
  171. package/dist/watch/events.js +21 -0
  172. package/dist/watch/events.js.map +1 -0
  173. package/dist/watch/keys.d.ts +15 -0
  174. package/dist/watch/keys.d.ts.map +1 -0
  175. package/dist/watch/keys.js +23 -0
  176. package/dist/watch/keys.js.map +1 -0
  177. package/dist/watch/lanes.d.ts +41 -0
  178. package/dist/watch/lanes.d.ts.map +1 -0
  179. package/dist/watch/lanes.js +73 -0
  180. package/dist/watch/lanes.js.map +1 -0
  181. package/dist/watch/loader.d.ts +47 -0
  182. package/dist/watch/loader.d.ts.map +1 -0
  183. package/dist/watch/loader.js +178 -0
  184. package/dist/watch/loader.js.map +1 -0
  185. package/dist/watch/registry.d.ts +137 -0
  186. package/dist/watch/registry.d.ts.map +1 -0
  187. package/dist/watch/registry.js +530 -0
  188. package/dist/watch/registry.js.map +1 -0
  189. package/dist/watch/ring.d.ts +32 -0
  190. package/dist/watch/ring.d.ts.map +1 -0
  191. package/dist/watch/ring.js +90 -0
  192. package/dist/watch/ring.js.map +1 -0
  193. package/dist/watch/router.d.ts +25 -0
  194. package/dist/watch/router.d.ts.map +1 -0
  195. package/dist/watch/router.js +87 -0
  196. package/dist/watch/router.js.map +1 -0
  197. package/dist/watch/sources.d.ts +42 -0
  198. package/dist/watch/sources.d.ts.map +1 -0
  199. package/dist/watch/sources.js +104 -0
  200. package/dist/watch/sources.js.map +1 -0
  201. package/dist/watch/types.d.ts +110 -0
  202. package/dist/watch/types.d.ts.map +1 -0
  203. package/dist/watch/types.js +6 -0
  204. package/dist/watch/types.js.map +1 -0
  205. package/endpoint-manifest.json +178 -1
  206. package/package.json +3 -2
  207. package/watch/entries/deliver-audit.json +17 -0
  208. package/watch/entries/quiet-after-claim.json +16 -0
  209. package/watch/entries/registry-check-failed.json +12 -0
  210. package/watch/entries/registry-lagging.json +12 -0
  211. package/watch/entries/ungated.json +13 -0
  212. package/dist/api/demo.d.ts +0 -90
  213. package/dist/api/demo.d.ts.map +0 -1
  214. package/dist/api/demo.js +0 -492
  215. 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,7 +71,7 @@ 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
77
  import { LOCAL_ACTOR } from './auth.js';
@@ -77,6 +79,11 @@ import { LOCAL_ACTOR } from './auth.js';
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 { resolveDecisionsMode } from '../decisions/types.js';
80
87
  const V = API_PREFIX;
81
88
  /** How long `GET /health` waits on the engine's actor before answering `ping: "busy"` (crew#471). */
82
89
  export const HEALTH_PING_WAIT_MS = 1_000;
@@ -185,80 +192,8 @@ function isStringRecord(v) {
185
192
  !Array.isArray(v) &&
186
193
  Object.values(v).every((x) => typeof x === 'string'));
187
194
  }
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
- }
195
+ // `policyProposalToRule` lives beside THE landing (policy-landing.ts); re-exported for its callers.
196
+ export { policyProposalToRule } from './policy-landing.js';
262
197
  /** A JSON object whose every value is a number (a coverage breakdown map). */
263
198
  function isNumberRecord(v) {
264
199
  return (typeof v === 'object' &&
@@ -741,6 +676,25 @@ runtime = {}) {
741
676
  // default is the real spawn-per-call `wicked-estate-mcp` client; route tests inject a stub so no
742
677
  // process is ever spawned.
743
678
  const estateTool = runtime.callEstateTool ?? callEstateTool;
679
+ // DC-S4a: decision capture over the structured hosts (gate note, elicitation, inject). Words are
680
+ // recorded post-commit, off the request path, and only from a human actor (decisions/ingest.ts).
681
+ const decisions = runtime.decisionLedger !== undefined
682
+ ? new DecisionService({
683
+ ledger: runtime.decisionLedger,
684
+ mode: runtime.decisionsMode ?? resolveDecisionsMode(),
685
+ authMode: security.authMode,
686
+ estateTool,
687
+ adapter,
688
+ audit,
689
+ emit: runtime.decisionsEmit ?? null,
690
+ ...(runtime.broadcast !== undefined
691
+ ? { broadcast: (frame) => runtime.broadcast?.(frame) }
692
+ : {}),
693
+ log: (m) => app.log.warn(m),
694
+ })
695
+ : null;
696
+ if (decisions !== null)
697
+ registerDecisionRoutes(app, decisions);
744
698
  /** Repo root for a repo ref, from the registry — shared by the reprovision path below. */
745
699
  const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
746
700
  /** DES-L9: PR number → head branch, through `gh pr view` unless the runtime injected an answerer. */
@@ -875,6 +829,20 @@ runtime = {}) {
875
829
  view.session.launch_actor = launchActor;
876
830
  const state = resolveDelivery(view, conflictStrand);
877
831
  view.session.delivery = state.delivery;
832
+ // crew#762: was delivery asked for? The launch's decision (`deliver: 'pr'`), a post-hoc attempt
833
+ // or a delivery on record says yes; a launch that said `deliver: 'none'` with neither says no.
834
+ // ABSENT when the daemon holds no launch record and no attempt (never fabricated).
835
+ const launchDeliver = runTimingIndex.launchDeliverFor(view.session.id);
836
+ if (launchDeliver === 'pr' || deliveryIndex.wasRequested(view.session.id) || deliveryIndex.isDelivered(view.session.id)) {
837
+ view.session.deliver_requested = true;
838
+ }
839
+ else if (launchDeliver === 'none') {
840
+ view.session.deliver_requested = false;
841
+ }
842
+ // crew#755: a completed repo-less free-text run answered in text and changed nothing — say so.
843
+ const outcome = freeTextOutcome(view);
844
+ if (outcome !== undefined)
845
+ view.session.outcome = outcome;
878
846
  if (state.deliverUrl !== undefined)
879
847
  view.session.deliverUrl = state.deliverUrl;
880
848
  if (state.deliverBranch !== undefined)
@@ -888,6 +856,10 @@ runtime = {}) {
888
856
  const chatGrounded = runTimingIndex.chatGroundedFor(view.session.id);
889
857
  if (chatGrounded !== undefined)
890
858
  view.session.chat_grounded = chatGrounded;
859
+ // C1: the chat the run was launched from, on every run (terminal ones too) — ABSENT otherwise.
860
+ const chatId = runTimingIndex.chatIdFor(view.session.id);
861
+ if (chatId !== undefined)
862
+ view.session.chat_id = chatId;
891
863
  return view;
892
864
  };
893
865
  // Resolved ONCE and shared by the project routes (which read/write `interactiveRoot`) and the
@@ -898,6 +870,17 @@ runtime = {}) {
898
870
  // driving this function directly (tests) has no hooks, so downstream code
899
871
  // still gets the ONE actor shape via this accessor.
900
872
  const actorOf = (req) => req.actor ?? LOCAL_ACTOR;
873
+ // crew#753: the blocking `skills.garden` finding (an installed garden older than the minimum), or
874
+ // `null`. Read from the skills runtime's cached health; no seam ⇒ nothing to judge.
875
+ const gardenBlocker = () => {
876
+ const f = runtime.skills?.health().findings.find((x) => x.kind === 'skills.garden' && x.severity === 'error');
877
+ return f === undefined ? null : { kind: f.kind, severity: f.severity, message: f.message };
878
+ };
879
+ /** The typed 422 a launch or onboard answers while {@link gardenBlocker} holds. */
880
+ const gardenRefusalBody = () => {
881
+ const f = gardenBlocker();
882
+ return f === null ? null : { code: 'garden_required', error: f.message, required: REQUIRED_GARDEN_VERSION };
883
+ };
901
884
  // Liveness — also proves the actor + event pump are up.
902
885
  //
903
886
  // crew#471: the engine's ping is a round trip through its single-writer actor, and a launch that
@@ -927,9 +910,15 @@ runtime = {}) {
927
910
  // F-E2E-030: what this deployment can keep. A composer reads `capabilities.deliverGate`
928
911
  // before it promises "pauses at the deliver gate"; a stub-driven route set without the
929
912
  // probe honestly reports no gate.
930
- const capabilities = typeof adapter.engineCapabilities === 'function'
931
- ? adapter.engineCapabilities()
932
- : { deliverGate: false, revisesPr: false, chatIdOnLaunch: false, seatChipOnCreate: false };
913
+ const capabilities = {
914
+ ...(typeof adapter.engineCapabilities === 'function'
915
+ ? adapter.engineCapabilities()
916
+ : { deliverGate: false, revisesPr: false, chatIdOnLaunch: false, seatChipOnCreate: false }),
917
+ // C1: `AgentSession.chat_id` is served from the daemon's own launch index, whatever the engine.
918
+ runChatId: true,
919
+ // WT-W1: repo-bound launches mint an evidence root only on an addon that takes the field.
920
+ walkthroughRoots: typeof adapter.evidenceRootsSupported === 'function' ? adapter.evidenceRootsSupported() : false,
921
+ };
933
922
  // wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
934
923
  // daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
935
924
  // launch while the state home holds an entry the worker Read fence cannot classify. Re-surveyed
@@ -950,6 +939,8 @@ runtime = {}) {
950
939
  const warnings = [
951
940
  ...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
952
941
  ...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
942
+ // crew#753: a garden older than the minimum is a blocking finding on the first screen too.
943
+ ...[gardenBlocker()].filter((f) => f !== null),
953
944
  ...phaseSkillFindings(runtime.phaseSkills?.gaps() ?? []),
954
945
  // DES-TEAMING-002 T0: the daemon's bus did not open at boot, so the engine was handed none
955
946
  // and runs un-teamed. A daemon-level notice (status stays ok: it serves); the per-run
@@ -1369,11 +1360,39 @@ runtime = {}) {
1369
1360
  return message(err);
1370
1361
  }
1371
1362
  };
1363
+ /**
1364
+ * crew#753: what refuses an onboarding launch, judged BEFORE anything is registered — so a
1365
+ * refused onboard leaves no repo row (core has no unregister). The same three intake refusals a
1366
+ * launch meets: a garden older than the minimum (422 `garden_required`), the state-home blocker
1367
+ * (409) and a base skill the handed generation lacks, e.g. no garden at all (422
1368
+ * `base_skill_refused`). `null` when the launch would be admitted.
1369
+ */
1370
+ const onboardRefusal = async () => {
1371
+ const garden = gardenRefusalBody();
1372
+ if (garden !== null)
1373
+ return { status: 422, body: garden };
1374
+ if (runtime.stateHome !== undefined) {
1375
+ const stateHome = await runtime.stateHome.refresh();
1376
+ if (stateHome.refusesLaunches)
1377
+ return { status: 409, body: stateHomeBlockerBody(stateHome) };
1378
+ }
1379
+ const posture = runtime.skills?.baseSkill() ?? null;
1380
+ if (posture !== null && !posture.present && posture.finding !== null) {
1381
+ return {
1382
+ status: 422,
1383
+ body: { code: 'base_skill_refused', error: posture.finding.message, baseSkill: posture, remedy: baseSkillRemedy(posture.inCatalog) },
1384
+ };
1385
+ }
1386
+ return null;
1387
+ };
1372
1388
  app.post(`${V}/repos`, async (req, reply) => {
1373
1389
  const parsed = RegisterRepoSchema.safeParse(req.body);
1374
1390
  if (!parsed.success) {
1375
1391
  return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
1376
1392
  }
1393
+ const refused = await onboardRefusal();
1394
+ if (refused !== null)
1395
+ return reply.code(refused.status).send(refused.body);
1377
1396
  const { name, rootPath, gitUrl, projectId, channel, actor } = parsed.data;
1378
1397
  let repo;
1379
1398
  let runId;
@@ -1416,6 +1435,9 @@ runtime = {}) {
1416
1435
  const body = OnboardBodySchema.safeParse(req.body ?? {});
1417
1436
  if (!body.success)
1418
1437
  return reply.code(400).send(invalidBody(body.error, 'Invalid request body'));
1438
+ const refused = await onboardRefusal();
1439
+ if (refused !== null)
1440
+ return reply.code(refused.status).send(refused.body);
1419
1441
  let runId;
1420
1442
  try {
1421
1443
  runId = await adapter.launchOnboardingRun(repo.id, repo.name);
@@ -1449,7 +1471,7 @@ runtime = {}) {
1449
1471
  return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
1450
1472
  }
1451
1473
  const b = parsed.data;
1452
- // The demo preset launches only from POST /projects/:id/demo (api/demo.ts), which mints the run's
1474
+ // The demo preset launches only from POST /projects/:id/demo (api/recording.ts), which mints the run's
1453
1475
  // demo root and brief first: launched here directly it would have neither.
1454
1476
  if (b.workflow === DEMO_PRESET && (b.sessionId === undefined || !demoLaunchRoots.has(b.sessionId))) {
1455
1477
  return reply.code(400).send({
@@ -1468,6 +1490,10 @@ runtime = {}) {
1468
1490
  return reply.code(409).send(stateHomeBlockerBody(stateHome));
1469
1491
  }
1470
1492
  }
1493
+ // crew#753: a garden older than the minimum refuses the launch by name (found vs required).
1494
+ const garden = gardenRefusalBody();
1495
+ if (garden !== null)
1496
+ return reply.code(422).send(garden);
1471
1497
  // crew#645: an expired login reads signed out BEFORE routing — wait (bounded) for every seat's
1472
1498
  // missing or stale login check, so the standing below is the seats' own answer.
1473
1499
  if (b.clisJson === undefined)
@@ -1485,7 +1511,7 @@ runtime = {}) {
1485
1511
  const captureRoot = captureLaunchRoots.get(input.sessionId);
1486
1512
  if (captureRoot !== undefined)
1487
1513
  input.extraWriteRoots = [captureRoot];
1488
- // A demo launch (api/demo.ts) writes every file into the demo root the demo route minted for it.
1514
+ // A demo launch (api/recording.ts) writes every file into the demo root the demo route minted for it.
1489
1515
  const demoRoot = demoLaunchRoots.get(input.sessionId);
1490
1516
  if (demoRoot !== undefined)
1491
1517
  input.extraWriteRoots = [demoRoot];
@@ -1730,7 +1756,11 @@ runtime = {}) {
1730
1756
  // is always reproducible even if the chat is idle-reclaimed before the run finishes.
1731
1757
  if (b.chatId !== undefined)
1732
1758
  runtime.linkChatRun?.(b.chatId, runId);
1733
- return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}) });
1759
+ // crew#755: a launch that named neither a workflow nor a plan runs the engine's free-text
1760
+ // planner (one unit, the brief verbatim). Say so in the answer, with how to get work done,
1761
+ // instead of letting it pass for a launch of real work.
1762
+ const freeText = input.workflow === undefined && input.plan === undefined ? { freeText: { notice: FREE_TEXT_NOTICE } } : {};
1763
+ return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}), ...freeText });
1734
1764
  }
1735
1765
  catch (err) {
1736
1766
  const msg = message(err);
@@ -2024,6 +2054,10 @@ runtime = {}) {
2024
2054
  });
2025
2055
  }
2026
2056
  deliverInFlight.add(id);
2057
+ // crew#762: the attempt is the durable fact "delivery was requested" — recorded BEFORE the
2058
+ // script runs, so a lift that then fails still reads requested after a reload or restart.
2059
+ audit.record(DELIVER_REQUESTED_ACTION, actorOf(req), { runId: id, detail: { via: 'post-hoc' } });
2060
+ deliveryIndex.markRequested(id);
2027
2061
  // The worktree the script runs in. Usually the run's own; but the engine REAPS a
2028
2062
  // failed-deliver run's worktree once its work is committed (crew#418/#432) — the work then lives
2029
2063
  // on the `wicked/<id>` branch. For such a strand, stand a throwaway worktree back up from
@@ -2782,7 +2816,10 @@ runtime = {}) {
2782
2816
  // catalog entry re-verifies evidence, a registered workflow's def, nothing for free text — and
2783
2817
  // an unknown run (or one the daemon cannot read) is declared and denied with the reason.
2784
2818
  const verifiedCatalog = typeof adapter.verifiedEvidenceCatalog === 'function' ? await adapter.verifiedEvidenceCatalog() : null;
2785
- const requirement = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog);
2819
+ const knownCatalog = typeof adapter.catalogIds === 'function' ? await adapter.catalogIds() : null;
2820
+ const requirement = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog, knownCatalog);
2821
+ // WT-W2: every walkthrough step the requirement names is resolved from its proof root and seal.
2822
+ const walkthroughs = await walkthroughAcceptance(adapter, run, requirement.phases);
2786
2823
  let repo = null;
2787
2824
  if (run.session.repo_ref !== null) {
2788
2825
  const repos = await adapter.listRepos();
@@ -2799,6 +2836,7 @@ runtime = {}) {
2799
2836
  // unit both appear on the page humans actually look at.
2800
2837
  claims: () => adapter.listConformanceClaims(),
2801
2838
  events: (rid) => adapter.runEvents(rid),
2839
+ ...(walkthroughs.gates.length > 0 ? { walkthroughs } : {}),
2802
2840
  });
2803
2841
  });
2804
2842
  /**
@@ -2920,6 +2958,19 @@ runtime = {}) {
2920
2958
  status,
2921
2959
  },
2922
2960
  });
2961
+ // DC-S4a host: the gate note (amend / request_changes text) — a bare approve or reject is a
2962
+ // `choice`. Post-commit, off the request path; a non-human actor (a standing order) records nothing.
2963
+ if (decisions !== null) {
2964
+ decisions.track(ingestDecision(decisions, {
2965
+ host: 'gate',
2966
+ actor,
2967
+ words: parsed.data.amend ?? '',
2968
+ choice: parsed.data.action ?? (parsed.data.approve ? 'approve' : 'reject'),
2969
+ projectId: projects.index.projectOf(id) ?? null,
2970
+ runId: id,
2971
+ ...(parsed.data.ord !== undefined ? { ord: parsed.data.ord } : {}),
2972
+ }));
2973
+ }
2923
2974
  // APPROVE of the steering-author propose gate = the doctrine's landing moment: the
2924
2975
  // approved proposal is written to the governance store with `provenance.source: "chat"`,
2925
2976
  // audited per rule, idempotent on replay, and LOUD on failure — the response carries the
@@ -3046,6 +3097,16 @@ runtime = {}) {
3046
3097
  try {
3047
3098
  await adapter.injectWorkerMessage(id, parsed.data.message, parsed.data.target);
3048
3099
  audit.record('run.injected', actorOf(req), { runId: id, detail: { target: parsed.data.target } });
3100
+ // DC-S4a host: the operator's mid-run steering text (review S9).
3101
+ if (decisions !== null) {
3102
+ decisions.track(ingestDecision(decisions, {
3103
+ host: 'inject',
3104
+ actor: actorOf(req),
3105
+ words: parsed.data.message,
3106
+ projectId: projects.index.projectOf(id) ?? null,
3107
+ runId: id,
3108
+ }));
3109
+ }
3049
3110
  return reply.send({ status: 'ok' });
3050
3111
  }
3051
3112
  catch (err) {
@@ -3298,6 +3359,19 @@ runtime = {}) {
3298
3359
  runId: id,
3299
3360
  detail: { elicitationId: taken.entry.elicitationId, action: body.action },
3300
3361
  });
3362
+ // DC-S4a host: a free-text answer is words; a picked option (or a decline) is a `choice`.
3363
+ if (decisions !== null) {
3364
+ const picked = taken.entry.options !== null;
3365
+ decisions.track(ingestDecision(decisions, {
3366
+ host: 'elicitation',
3367
+ actor: actorOf(req),
3368
+ words: response !== null && !picked ? response : '',
3369
+ ...(response !== null && picked ? { choice: response } : body.action !== 'accept' ? { choice: body.action } : {}),
3370
+ projectId: projects.index.projectOf(id) ?? null,
3371
+ runId: id,
3372
+ elicitationId: taken.entry.elicitationId,
3373
+ }));
3374
+ }
3301
3375
  return { status: 'resolved' };
3302
3376
  });
3303
3377
  // The durable history of one run (FINDING-057).
@@ -3656,6 +3730,21 @@ runtime = {}) {
3656
3730
  });
3657
3731
  }
3658
3732
  }
3733
+ // DC-S3: `targets.project` and `supersedes` shipped with `considerRules` (wicked-core-ts
3734
+ // 0.7.35). An older engine would accept and drop them, so a project rule would land
3735
+ // everywhere: refuse, naming the fields (codex on DC-S3).
3736
+ if (!adapter.projectRulesSupported() && rule !== null && typeof rule === 'object') {
3737
+ const carried = [
3738
+ ...(rule.targets?.project !== undefined ? ['targets.project'] : []),
3739
+ ...(rule.supersedes !== undefined ? ['supersedes'] : []),
3740
+ ];
3741
+ if (carried.length > 0) {
3742
+ return reply.code(501).send({
3743
+ error: `the installed engine would silently drop ${carried.map((f) => `\`${f}\``).join(', ')} ` +
3744
+ 'and land the rule everywhere; upgrade wicked-core-ts (>= 0.7.35)',
3745
+ });
3746
+ }
3747
+ }
3659
3748
  await adapter.upsertConformanceRule(rule);
3660
3749
  audit.record('governance.rule.upserted', actorOf(req), { detail: { id: rule?.id } });
3661
3750
  return { status: 'ok' };
@@ -4332,6 +4421,26 @@ runtime = {}) {
4332
4421
  });
4333
4422
  }
4334
4423
  }
4424
+ // The watch registry's switches (DES-TRIGGER-REGISTRY-001 §4.2, TR-W5a): only `enabled` and
4425
+ // `threshold` of a shipped entry, and `llm`. Turning a watcher off is the one way to make the
4426
+ // platform stop telling the operator something, so only a PERSON may do it — the "human"
4427
+ // predicate decision capture uses; a worker's bearer gets 403. A bad patch is a 400 naming the
4428
+ // key (never a silent drop), and the daemon stamps who changed what and when.
4429
+ let watchNext;
4430
+ if (Object.hasOwn(patch, 'watch')) {
4431
+ const actor = actorOf(req);
4432
+ if (!isHumanOperator(actor)) {
4433
+ return reply.code(403).send({ error: 'Only a person can change what is watched (watch.* settings); a worker or system actor cannot' });
4434
+ }
4435
+ const registry = runtime.watchRegistry?.() ?? null;
4436
+ if (registry === null) {
4437
+ return reply.code(400).send({ error: 'watch: watching is not configured on this daemon' });
4438
+ }
4439
+ const problem = registry.validateSettingsPatch(patch.watch);
4440
+ if (problem !== null)
4441
+ return reply.code(400).send({ error: problem });
4442
+ watchNext = registry.mergeSettings((await adapter.getSettings()).watch, patch.watch, actor.id, Date.now());
4443
+ }
4335
4444
  // Only known engine keys and validated `studio.*` keys are persisted. A key that is
4336
4445
  // NEITHER is dropped, not refused: request bodies are forward-additive too (DES-STUDIO-001
4337
4446
  // §5.1), so an older daemon meeting a newer client's engine key must not fail the whole
@@ -4364,11 +4473,15 @@ runtime = {}) {
4364
4473
  for (const key of studioKeys) {
4365
4474
  safe[key] = patch[key];
4366
4475
  }
4476
+ if (watchNext !== undefined)
4477
+ safe.watch = watchNext;
4367
4478
  // `Object.hasOwn`, NOT `k in safe`: `in` walks the prototype chain, so a dropped key named
4368
4479
  // `toString` / `valueOf` / `constructor` would test as "kept" and vanish from `ignored` —
4369
4480
  // the exact silent drop this route exists to end.
4370
4481
  const ignored = Object.keys(patch).filter((k) => !Object.hasOwn(safe, k));
4371
4482
  const settings = await adapter.updateSettings(safe);
4483
+ if (watchNext !== undefined)
4484
+ runtime.watchRegistry?.()?.applySettings(settings.watch);
4372
4485
  // Re-apply the worker-config root to this process's env (seat sign-in). The engine reads
4373
4486
  // WICKED_WORKER_HOME per worker spawn — never cached — so this alone makes the change live
4374
4487
  // at the next spawn: no daemon restart, no engine restart.
@@ -4383,7 +4496,12 @@ runtime = {}) {
4383
4496
  // `changed` names every persisted key, engine and `studio.*` alike; `ignored` (present only
4384
4497
  // when there is one) is where a dropped unknown key stops being invisible.
4385
4498
  audit.record('settings.updated', actorOf(req), {
4386
- detail: { changed: Object.keys(safe), ...(ignored.length > 0 ? { ignored } : {}) },
4499
+ detail: {
4500
+ changed: Object.keys(safe),
4501
+ ...(ignored.length > 0 ? { ignored } : {}),
4502
+ // Which watchers were switched or re-thresholded (health reads the stamps; this is the trail).
4503
+ ...(watchNext !== undefined ? { watch: patch.watch } : {}),
4504
+ },
4387
4505
  });
4388
4506
  return { settings };
4389
4507
  });
@@ -4514,6 +4632,46 @@ runtime = {}) {
4514
4632
  if (editInFlight(id)) {
4515
4633
  return reply.code(409).send({ error: `proposal ${id} is already being accepted` });
4516
4634
  }
4635
+ // DC-S4a: a proposal THIS ledger filed for a decision lands through `remember()` — the one
4636
+ // landing path, with the human check inside it (§4.5). A payload naming an id the ledger does
4637
+ // not know is an ordinary policy proposal (no ORIGIN claim) and takes the path below.
4638
+ let decisionId = decisions?.deps.ledger.decisionForProposal(id) ?? null;
4639
+ if (decisions !== null && decisionId === null) {
4640
+ // The ledger links a proposal to its decision only once the `offered` outcome is written; a
4641
+ // proposal crew filed but has not linked yet (the publish window, or a crash in it) names a
4642
+ // decision this ledger HOLDS — it must still land through `remember()` (codex on DC-S4a).
4643
+ try {
4644
+ const pending = (await estateTool('proposal.list', { state: 'pending' }));
4645
+ const named = pending.proposals.find((p) => p.id === id)?.payload?.decision?.id;
4646
+ if (typeof named === 'string' && decisions.deps.ledger.record(named) !== null)
4647
+ decisionId = named;
4648
+ }
4649
+ catch (err) {
4650
+ return estateUpstreamError(reply, err);
4651
+ }
4652
+ }
4653
+ if (decisions !== null && decisionId !== null) {
4654
+ try {
4655
+ const landed = await decisions.remember(decisionId, actorOf(req), { how: 'needs-you' });
4656
+ const view = decisions.deps.ledger.view(decisionId);
4657
+ const landing = {
4658
+ outcome: 'landed',
4659
+ ruleId: landed.rule_id,
4660
+ ...(view !== null ? { steering_type: view.edits?.steering_type ?? view.derived.steering_type } : {}),
4661
+ ...(landed.project !== undefined ? { project: landed.project } : {}),
4662
+ };
4663
+ return {
4664
+ outcome: 'handed_off',
4665
+ payload: { rule: view?.edits?.statement ?? view?.derived.statement ?? '', severity: 'warn', capture: 'decision', decision: { id: decisionId } },
4666
+ landing,
4667
+ };
4668
+ }
4669
+ catch (err) {
4670
+ if (err instanceof DecisionError)
4671
+ return reply.code(err.status).send({ error: err.message });
4672
+ throw err;
4673
+ }
4674
+ }
4517
4675
  let approved;
4518
4676
  try {
4519
4677
  approved = (await estateTool('proposal.approve', { id }));
@@ -4561,26 +4719,13 @@ runtime = {}) {
4561
4719
  if ('error' in built) {
4562
4720
  return landFailed(built.error);
4563
4721
  }
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
- },
4722
+ // THE landing (probe, upsert, read-back, audit) — shared with the decision landing (DC-S4a).
4723
+ const landing = await landPolicyRule({ adapter, audit }, built, actorOf(req), {
4724
+ via: 'proposal-approve',
4725
+ proposalId: id,
4578
4726
  });
4579
- const landing = {
4580
- outcome: 'landed',
4581
- ruleId: built.rule.id,
4582
- steering_type: built.steeringType,
4583
- };
4727
+ if (landing.outcome === 'failed')
4728
+ return landFailed(landing.error ?? 'the landing failed');
4584
4729
  return { outcome: 'handed_off', payload: approved.payload, landing };
4585
4730
  });
4586
4731
  // POST /proposals/:id/reject → proposal.reject → { ok: true }.
@@ -4594,6 +4739,19 @@ runtime = {}) {
4594
4739
  if (editInFlight(id.trim())) {
4595
4740
  return reply.code(409).send({ error: `proposal ${id.trim()} is being accepted` });
4596
4741
  }
4742
+ // DC-S4a B12: rejecting a decision's proposal dismisses the decision too (one object, two places).
4743
+ const rejectedDecision = decisions?.deps.ledger.decisionForProposal(id.trim()) ?? null;
4744
+ if (decisions !== null && rejectedDecision !== null) {
4745
+ try {
4746
+ await decisions.dismiss(rejectedDecision, actorOf(req), 'not-a-rule');
4747
+ return { ok: true };
4748
+ }
4749
+ catch (err) {
4750
+ if (err instanceof DecisionError)
4751
+ return reply.code(err.status).send({ error: err.message });
4752
+ throw err;
4753
+ }
4754
+ }
4597
4755
  try {
4598
4756
  return (await estateTool('proposal.reject', { id }));
4599
4757
  }
@@ -4814,6 +4972,8 @@ runtime = {}) {
4814
4972
  registerCaptureRoutes(app);
4815
4973
  // The Demo experience (studio#373): the demo preset's launch, view, files and script edit.
4816
4974
  registerDemoRoutes(app, adapter);
4975
+ // WT-W3: the storyline PUT checks the open gate through the same resolution the gate routes use.
4976
+ registerWalkthroughRoutes(app, adapter, { resolveOpenGate, audit });
4817
4977
  // ── Presets (DES-TEAMING-002 §8.4, seam C2) — saved phase selections in the engine's store;
4818
4978
  // a launch names one via `workflow`, and the ENGINE resolves it.
4819
4979
  registerPresetRoutes(app, adapter, security);
@@ -4959,6 +5119,6 @@ runtime = {}) {
4959
5119
  pool: interactiveBridges,
4960
5120
  log: (m) => app.log.warn(m),
4961
5121
  });
4962
- return { decideGate };
5122
+ return { decideGate, decisions };
4963
5123
  }
4964
5124
  //# sourceMappingURL=routes.js.map