wicked-crew 0.8.10 → 0.9.1

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 (124) hide show
  1. package/README.md +11 -8
  2. package/dist/api/auth.d.ts +6 -5
  3. package/dist/api/auth.d.ts.map +1 -1
  4. package/dist/api/auth.js +7 -5
  5. package/dist/api/auth.js.map +1 -1
  6. package/dist/api/capture.d.ts +10 -6
  7. package/dist/api/capture.d.ts.map +1 -1
  8. package/dist/api/capture.js +6 -1
  9. package/dist/api/capture.js.map +1 -1
  10. package/dist/api/chat-freshness.d.ts +37 -0
  11. package/dist/api/chat-freshness.d.ts.map +1 -0
  12. package/dist/api/chat-freshness.js +130 -0
  13. package/dist/api/chat-freshness.js.map +1 -0
  14. package/dist/api/chat-scope.d.ts +15 -0
  15. package/dist/api/chat-scope.d.ts.map +1 -1
  16. package/dist/api/chat-scope.js +55 -1
  17. package/dist/api/chat-scope.js.map +1 -1
  18. package/dist/api/codebase-archive.d.ts +88 -0
  19. package/dist/api/codebase-archive.d.ts.map +1 -0
  20. package/dist/api/codebase-archive.js +282 -0
  21. package/dist/api/codebase-archive.js.map +1 -0
  22. package/dist/api/delivery-assurance.d.ts +48 -0
  23. package/dist/api/delivery-assurance.d.ts.map +1 -0
  24. package/dist/api/delivery-assurance.js +118 -0
  25. package/dist/api/delivery-assurance.js.map +1 -0
  26. package/dist/api/delivery-index.d.ts +14 -6
  27. package/dist/api/delivery-index.d.ts.map +1 -1
  28. package/dist/api/delivery-index.js +42 -9
  29. package/dist/api/delivery-index.js.map +1 -1
  30. package/dist/api/mcp.d.ts +8 -8
  31. package/dist/api/product.d.ts +67 -0
  32. package/dist/api/product.d.ts.map +1 -0
  33. package/dist/api/product.js +169 -0
  34. package/dist/api/product.js.map +1 -0
  35. package/dist/api/project-aggregates.d.ts +61 -0
  36. package/dist/api/project-aggregates.d.ts.map +1 -0
  37. package/dist/api/project-aggregates.js +296 -0
  38. package/dist/api/project-aggregates.js.map +1 -0
  39. package/dist/api/recording.d.ts +4 -0
  40. package/dist/api/recording.d.ts.map +1 -1
  41. package/dist/api/recording.js +6 -1
  42. package/dist/api/recording.js.map +1 -1
  43. package/dist/api/routes.d.ts +50 -19
  44. package/dist/api/routes.d.ts.map +1 -1
  45. package/dist/api/routes.js +295 -42
  46. package/dist/api/routes.js.map +1 -1
  47. package/dist/api/run-files.d.ts.map +1 -1
  48. package/dist/api/run-files.js +11 -2
  49. package/dist/api/run-files.js.map +1 -1
  50. package/dist/api/server.d.ts +2 -0
  51. package/dist/api/server.d.ts.map +1 -1
  52. package/dist/api/server.js +42 -1
  53. package/dist/api/server.js.map +1 -1
  54. package/dist/api/stall-watchdog.d.ts.map +1 -1
  55. package/dist/api/stall-watchdog.js +6 -1
  56. package/dist/api/stall-watchdog.js.map +1 -1
  57. package/dist/api/steering-landing.d.ts +1 -1
  58. package/dist/api/steering-landing.d.ts.map +1 -1
  59. package/dist/api/steering-landing.js +65 -29
  60. package/dist/api/steering-landing.js.map +1 -1
  61. package/dist/cli/index.d.ts.map +1 -1
  62. package/dist/cli/index.js +22 -1
  63. package/dist/cli/index.js.map +1 -1
  64. package/dist/core/adapter.d.ts +3 -0
  65. package/dist/core/adapter.d.ts.map +1 -1
  66. package/dist/core/adapter.js +36 -6
  67. package/dist/core/adapter.js.map +1 -1
  68. package/dist/core/ado-deliver-helper.d.ts +22 -0
  69. package/dist/core/ado-deliver-helper.d.ts.map +1 -0
  70. package/dist/core/ado-deliver-helper.js +139 -0
  71. package/dist/core/ado-deliver-helper.js.map +1 -0
  72. package/dist/core/assurance.d.ts +60 -0
  73. package/dist/core/assurance.d.ts.map +1 -0
  74. package/dist/core/assurance.js +88 -0
  75. package/dist/core/assurance.js.map +1 -0
  76. package/dist/core/deliver-credentials.d.ts +88 -0
  77. package/dist/core/deliver-credentials.d.ts.map +1 -0
  78. package/dist/core/deliver-credentials.js +231 -0
  79. package/dist/core/deliver-credentials.js.map +1 -0
  80. package/dist/core/deliver-exclusions.d.ts +9 -1
  81. package/dist/core/deliver-exclusions.d.ts.map +1 -1
  82. package/dist/core/deliver-exclusions.js +37 -2
  83. package/dist/core/deliver-exclusions.js.map +1 -1
  84. package/dist/core/deliver.d.ts +36 -1
  85. package/dist/core/deliver.d.ts.map +1 -1
  86. package/dist/core/deliver.js +185 -13
  87. package/dist/core/deliver.js.map +1 -1
  88. package/dist/core/deliverable-floor.d.ts.map +1 -1
  89. package/dist/core/deliverable-floor.js +6 -1
  90. package/dist/core/deliverable-floor.js.map +1 -1
  91. package/dist/core/inventory-claim.d.ts +28 -0
  92. package/dist/core/inventory-claim.d.ts.map +1 -0
  93. package/dist/core/inventory-claim.js +138 -0
  94. package/dist/core/inventory-claim.js.map +1 -0
  95. package/dist/core/types.d.ts +7 -0
  96. package/dist/core/types.d.ts.map +1 -1
  97. package/dist/core/types.js.map +1 -1
  98. package/dist/editors/registry.d.ts +3 -3
  99. package/dist/interactive/bridge-pool.d.ts +8 -2
  100. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  101. package/dist/interactive/bridge-pool.js +7 -1
  102. package/dist/interactive/bridge-pool.js.map +1 -1
  103. package/dist/interactive/docs-index.d.ts +8 -0
  104. package/dist/interactive/docs-index.d.ts.map +1 -1
  105. package/dist/interactive/docs-index.js +5 -2
  106. package/dist/interactive/docs-index.js.map +1 -1
  107. package/dist/projects/routes.d.ts +2 -2
  108. package/dist/projects/state-home-registry.d.ts.map +1 -1
  109. package/dist/projects/state-home-registry.js +2 -0
  110. package/dist/projects/state-home-registry.js.map +1 -1
  111. package/dist/qe/acceptance.d.ts.map +1 -1
  112. package/dist/qe/acceptance.js +49 -1
  113. package/dist/qe/acceptance.js.map +1 -1
  114. package/dist/studio/assets/index-BLYKfepE.css +32 -0
  115. package/dist/studio/assets/index-BbXtfDnB.js +567 -0
  116. package/dist/studio/editors/wicked-page/editor.json +1 -1
  117. package/dist/studio/editors/wicked-page/index.html +1 -1
  118. package/dist/studio/index.html +2 -2
  119. package/dist/studio/testid-inventory.json +566 -6
  120. package/endpoint-manifest.json +73 -2
  121. package/package.json +8 -8
  122. package/skills/wicked-crew/SKILL.md +2 -1
  123. package/dist/studio/assets/index-Djg8MmkU.js +0 -563
  124. package/dist/studio/assets/index-gyu6tvrW.css +0 -32
@@ -8,7 +8,7 @@ import { readFileSync, existsSync } from 'node:fs';
8
8
  import { promises as fsp } from 'node:fs';
9
9
  import { isAbsolute, join, relative, resolve } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
- import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, GovernanceReplayUnsupportedError, PlanLaunchUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
11
+ import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, GovernanceReplayUnsupportedError, PlanLaunchUnsupportedError, SteeringUnsupportedError, addonSupportsReducedAssurance, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
12
12
  import { codeGraphDb, codeGraphErrorStatus, requirementsGraph } from '../core/repoPaths.js';
13
13
  import { resolveCursorUnit } from '../core/cursor.js';
14
14
  import { detectRefusal } from './gate-cache.js';
@@ -33,12 +33,15 @@ import { listInteractiveDocs } from '../interactive/docs-index.js';
33
33
  import { TestSetIndex } from '../qe/test-sets.js';
34
34
  import { estateExe, parseEstateTotals, resolveProjectGraphBinding } from '../projects/graph.js';
35
35
  import { registerProjectRoutes } from '../projects/routes.js';
36
+ import { registerProjectAggregateRoutes } from './project-aggregates.js';
37
+ import { registerProductRoutes } from './product.js';
36
38
  import { registerPresetRoutes } from '../presets/routes.js';
37
39
  import { registerTeamRoutes } from '../team/routes.js';
38
40
  import { registerCampaignRoutes } from '../campaigns/routes.js';
39
41
  import { registerGovernanceWikiRoutes } from './governance-wiki.js';
40
42
  import { DEFAULT_STEERING_TYPE, STEERING_TYPE_VALUES, STEERING_TYPES, registerGovernanceSteeringRoutes, } from './governance-steering.js';
41
43
  import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js';
44
+ import { inventoryComplete, parseInventoryClaims } from '../core/inventory-claim.js';
42
45
  import { ApproveEditSchema, approveEdited, captureLaunchRoots, editInFlight, FileProposalSchema, registerCaptureRoutes } from './capture.js';
43
46
  import { registerTestingRoutes } from './testing.js';
44
47
  import { DEMO_PRESET, demoLaunchRoots, registerDemoRoutes, registerWalkthroughRoutes, walkthroughAcceptance } from './recording.js';
@@ -50,12 +53,16 @@ import { phaseSkillFindings, withPhaseSkillGaps } from '../skills/phase-skill-ga
50
53
  import { STATE_HOME_BLOCKER_CODE, STATE_HOME_REMEDY, isEngineStateHomeRefusal, stateHomeBlockerBody, } from '../projects/state-home-preflight.js';
51
54
  import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../skills/base-skill.js';
52
55
  import { REQUIRED_GARDEN_VERSION } from '../skills/plugin-source.js';
53
- import { noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
56
+ import { engineRosterJson, noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
57
+ import { launchAssuranceNotice } from '../core/assurance.js';
58
+ import { QE_ACCEPTANCE, aggregateReceipt, postHocDeliveryAssurance, qeAcceptanceFromView, qeAcceptanceRefusal, receiptOf, requiresQeAcceptance, unreadableQeAcceptance, } from './delivery-assurance.js';
54
59
  import { ProjectSettingsStore } from '../projects/settings.js';
55
60
  import { isHumanOperator } from './watch-routes.js';
56
61
  import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
57
62
  import { composeDeliverText, configuredPublicOrigin, extractFollowUps, factsFromRun, reportsFromOutputs, FOLLOW_UPS_MAX, framedDeliverText, runUrlFor, } from '../core/deliver-text.js';
58
- import { deliverRepoFor, deliverTargetView, isGitHubLogin, readDeliverOriginUrl, resolvePullRequest as resolvePullRequestViaGh, } from '../core/deliver.js';
63
+ import { deliverRepoFor, deliverTargetView, isGitHubLogin, readDeliverOriginUrl, resolvePullRequest as resolvePullRequestViaGh, unverifiedTreesFrom, } from '../core/deliver.js';
64
+ import { credentialsMissingIn, deliverCredentialsProbe, missingCredentials } from '../core/deliver-credentials.js';
65
+ import { codebaseArchiveUrl } from './codebase-archive.js';
59
66
  import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
60
67
  import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
61
68
  import { registerInteractiveDocChecks } from '../interactive/doc-checks-routes.js';
@@ -77,7 +84,7 @@ import { DeliveryFreeze, frozenRefusal, isDeliverUnit, registerDeliveryFreezeRou
77
84
  import { GroupIndex } from './group-index.js';
78
85
  import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
79
86
  import { GuidanceIndex } from './guidance-index.js';
80
- import { DELIVER_REQUESTED_ACTION, DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, deliveryRecordFrom, pushedState, canDeliverResolver, FREE_TEXT_NOTICE, freeTextOutcome, } from './delivery-index.js';
87
+ import { DELIVER_REQUESTED_ACTION, DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, deliveryRecordFrom, deliverUnitOf, pushedState, canDeliverResolver, FREE_TEXT_NOTICE, freeTextOutcome, } from './delivery-index.js';
81
88
  import { DeliveryDerivationCache } from './delivery-cache.js';
82
89
  import { gitReprovisionWorktree, runDeliverScript, } from './post-hoc-deliver.js';
83
90
  import { LOCAL_ACTOR, trustAtLeast } from './auth.js';
@@ -399,6 +406,11 @@ export const LaunchSchema = z.object({
399
406
  * the issue. They are answered as `linkedIssues[].excluded`. Send only when
400
407
  * `GET /health.capabilities.linkedIssuesExclude === true`. */
401
408
  excludeLinkedIssues: ExcludeLinkedIssuesSchema,
409
+ /** wicked-core#850 (api-types 0.102.0) — the EXPLICIT reduced-assurance opt-in: the creator's seat
410
+ * may evaluate and a judge-less gate may pass, disclosed on every receipt. `false` is an explicit
411
+ * "full assurance", the same as omitting it: the launcher never chooses for the caller, and a
412
+ * full-assurance launch on a one-CLI roster is answered with `assuranceNotice` (`core/assurance.ts`). */
413
+ reducedAssurance: z.boolean().optional(),
402
414
  }).strict().refine((b) => b.plan === undefined || b.workflow === undefined, {
403
415
  message: 'plan and workflow are mutually exclusive — a launch carries a plan or names a preset, not both',
404
416
  path: ['plan'],
@@ -428,7 +440,10 @@ export const LaunchSchema = z.object({
428
440
  * evaluator's discarded, pinned edit as the creator's amendment. The ENGINE refuses each at any
429
441
  * other gate (a 409 naming the gate it answers). */
430
442
  export const ESCALATION_ACTIONS = ['extend', 'targeted', 'accept_partial', 'accept_suggestion'];
431
- const isEscalationAction = (a) => a !== undefined && ESCALATION_ACTIONS.includes(a);
443
+ /** (core#820) A consent gate's answer token: `consent:<choice id>` approves that row of the dry-run
444
+ * plan (the engine refuses an id its plan does not offer); `reject` stays the decline. */
445
+ export const CONSENT_CHOICE_ACTION = /^consent:[A-Za-z0-9_-]{1,32}$/u; // core `parse_plan` caps a choice id at 32
446
+ const isEscalationAction = (a) => a !== undefined && (ESCALATION_ACTIONS.includes(a) || CONSENT_CHOICE_ACTION.test(a));
432
447
  /** `POST /runs/:id/gate` (api-types 0.38.0 `GateDecision`; DES-L1 PR-2). Additive arms: `action`
433
448
  * names the arm (`approve` | `request_changes` | `reject`; absent = today's two-arm mapping of
434
449
  * `approve`), `amendScope` says where an approve's `amend` lands (`cursor` = the gated unit, today's
@@ -445,7 +460,12 @@ const isEscalationAction = (a) => a !== undefined && ESCALATION_ACTIONS.includes
445
460
  export const GateSchema = z.object({
446
461
  approve: z.boolean(),
447
462
  amend: z.string().optional(),
448
- action: z.enum(['approve', 'request_changes', 'reject', 'edit_plan', 'amend_intent', ...ESCALATION_ACTIONS]).optional(),
463
+ action: z
464
+ .union([
465
+ z.enum(['approve', 'request_changes', 'reject', 'edit_plan', 'amend_intent', ...ESCALATION_ACTIONS]),
466
+ z.string().regex(CONSENT_CHOICE_ACTION, 'a consent choice is `consent:<choice id>`'),
467
+ ])
468
+ .optional(),
449
469
  amendScope: z.enum(['cursor', 'creator']).optional(),
450
470
  /** DES-TEAMING-002 T3 — approve a `plan_approval` gate WITH AN EDIT (`GateDecision.plan`). */
451
471
  plan: PlanSchema.optional(),
@@ -453,11 +473,11 @@ export const GateSchema = z.object({
453
473
  * open gate is a 409 `gate_changed`: the decision was made on a gate that is no longer open. */
454
474
  ord: z.number().int().nonnegative().optional(),
455
475
  }).strict().refine((b) => b.action === undefined
456
- || (b.action === 'approve' || b.action === 'edit_plan' || b.action === 'amend_intent' || isEscalationAction(b.action)) === b.approve, { message: '`action` disagrees with `approve`: request_changes and reject require approve: false; approve, edit_plan, amend_intent, extend, targeted, accept_partial and accept_suggestion require approve: true', path: ['action'] }).refine(
476
+ || (b.action === 'approve' || b.action === 'edit_plan' || b.action === 'amend_intent' || isEscalationAction(b.action)) === b.approve, { message: '`action` disagrees with `approve`: request_changes and reject require approve: false; approve, edit_plan, amend_intent, extend, targeted, accept_partial, accept_suggestion and consent:<id> require approve: true', path: ['action'] }).refine(
457
477
  // (wicked-core#555) An intent amendment IS its text: an empty one amends nothing, and the arm's
458
478
  // scope is every unit at or after the cursor — which is what makes it reach the evaluator — so
459
479
  // `amendScope` cannot narrow it. Both are 400s here rather than a 409 from the engine.
460
- (b) => b.action !== 'amend_intent' || (b.amend !== undefined && b.amend.trim() !== ''), { message: 'amend_intent needs `amend`: what the run\'s acceptance list now says (approve without an action to proceed unamended)', path: ['amend'] }).refine((b) => b.action !== 'amend_intent' || (b.amendScope === undefined && b.plan === undefined), { message: 'amend_intent takes no amendScope or plan — an intent amendment reaches every unit at or after the cursor, which is what makes it reach the evaluator', path: ['amendScope'] }).refine((b) => !isEscalationAction(b.action) || (b.amend === undefined && b.amendScope === undefined && b.plan === undefined), { message: 'extend, targeted, accept_partial and accept_suggestion take no amend, amendScope or plan — the engine re-runs the floor or adopts the pinned edit as it stands', path: ['action'] }).refine((b) => (b.action === 'edit_plan') === (b.plan !== undefined) || (b.action === undefined && b.plan !== undefined), { message: 'an edited plan answers a plan_approval gate: `plan` needs approve: true with `action` omitted or edit_plan, and edit_plan needs `plan`', path: ['plan'] }).refine((b) => b.plan === undefined || (b.approve && b.amend === undefined && b.amendScope === undefined), { message: '`plan` is an approve WITH an edited plan: approve: true, and no amend / amendScope (the plan is the edit)', path: ['plan'] }).refine((b) => b.amendScope === undefined || b.approve, { message: '`amendScope` applies to an approve only (approve: true)', path: ['amendScope'] });
480
+ (b) => b.action !== 'amend_intent' || (b.amend !== undefined && b.amend.trim() !== ''), { message: 'amend_intent needs `amend`: what the run\'s acceptance list now says (approve without an action to proceed unamended)', path: ['amend'] }).refine((b) => b.action !== 'amend_intent' || (b.amendScope === undefined && b.plan === undefined), { message: 'amend_intent takes no amendScope or plan — an intent amendment reaches every unit at or after the cursor, which is what makes it reach the evaluator', path: ['amendScope'] }).refine((b) => !isEscalationAction(b.action) || (b.amend === undefined && b.amendScope === undefined && b.plan === undefined), { message: 'extend, targeted, accept_partial, accept_suggestion and consent:<id> take no amend, amendScope or plan — the engine re-runs the floor, adopts the pinned edit, or runs the chosen install as it stands', path: ['action'] }).refine((b) => (b.action === 'edit_plan') === (b.plan !== undefined) || (b.action === undefined && b.plan !== undefined), { message: 'an edited plan answers a plan_approval gate: `plan` needs approve: true with `action` omitted or edit_plan, and edit_plan needs `plan`', path: ['plan'] }).refine((b) => b.plan === undefined || (b.approve && b.amend === undefined && b.amendScope === undefined), { message: '`plan` is an approve WITH an edited plan: approve: true, and no amend / amendScope (the plan is the edit)', path: ['plan'] }).refine((b) => b.amendScope === undefined || b.approve, { message: '`amendScope` applies to an approve only (approve: true)', path: ['amendScope'] });
461
481
  /** `PUT /runs/:id/guidance` (DES-UX-002 §7.2, CREW-UX-7) — the durable pre-gate note body.
462
482
  * The empty string is a legal body: it CLEARS the note. The byte cap is checked in the route
463
483
  * (not zod's char-counting `max`) so the 400 names the actual limit. */
@@ -932,6 +952,10 @@ runtime = {}) {
932
952
  view.session.launch_actor = launchActor;
933
953
  const state = resolveDelivery(view, conflictStrand);
934
954
  view.session.delivery = state.delivery;
955
+ // wicked-core#850 EX-04: a post-hoc delivery reads unverified, with its trees. ABSENT otherwise.
956
+ const deliveryAssurance = deliveryIndex.assuranceFor(view.session.id);
957
+ if (deliveryAssurance !== undefined)
958
+ view.session.delivery_assurance = deliveryAssurance;
935
959
  // crew#762: was delivery asked for? The launch's decision (`deliver: 'pr'`), a post-hoc attempt
936
960
  // or a delivery on record says yes; a launch that said `deliver: 'none'` with neither says no.
937
961
  // ABSENT when the daemon holds no launch record and no attempt (never fabricated).
@@ -952,6 +976,15 @@ runtime = {}) {
952
976
  view.session.deliverBranch = state.deliverBranch;
953
977
  if (state.deliverRemote !== undefined)
954
978
  view.session.deliverRemote = state.deliverRemote;
979
+ // crew#720: the final-codebase zip (every outcome), and a deliver phase that refused for want of
980
+ // the provider credential — read from the deliver unit's own refusal line.
981
+ const archive = runtime.codebaseArchives?.get(view.session.id);
982
+ if (archive !== undefined)
983
+ view.session.codebase_archive = archive;
984
+ const deliverUnit = deliverUnitOf(view);
985
+ const credMissing = deliverUnit !== null && deliverUnit.status !== 'done' ? credentialsMissingIn(deliverUnit.denial_reason ?? '') : null;
986
+ if (credMissing !== null)
987
+ view.session.deliver_credentials = missingCredentials(credMissing);
955
988
  // crew#641/#642 (item 5): chat promotion provenance — ABSENT when not a chat-promoted run.
956
989
  const chatSeatCount = runTimingIndex.chatSeatCountFor(view.session.id);
957
990
  if (chatSeatCount !== undefined)
@@ -1038,6 +1071,10 @@ runtime = {}) {
1038
1071
  askPath: typeof adapter.engineCapabilities === 'function' ? adapter.engineCapabilities().askPath : false,
1039
1072
  // WT-W1: repo-bound launches mint an evidence root only on an addon that takes the field.
1040
1073
  walkthroughRoots: typeof adapter.evidenceRootsSupported === 'function' ? adapter.evidenceRootsSupported() : false,
1074
+ // wicked-core#850: `LaunchRunBody.reducedAssurance` is accepted (the installed addon carries the
1075
+ // assurance contract); a one-CLI full-assurance launch answers `assuranceNotice`. A route set
1076
+ // that cannot probe the addon reports no capability — never an invented one.
1077
+ reducedAssurance: typeof adapter.engineCapabilities === 'function' && addonSupportsReducedAssurance(),
1041
1078
  };
1042
1079
  // wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
1043
1080
  // daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
@@ -1719,8 +1756,18 @@ runtime = {}) {
1719
1756
  // F-E2E-030: only the explicit `'auto'` opts out of the engine's deliver gate. `'human'` and
1720
1757
  // an omitted field both leave `autoDeliver` off the input — the gate is the engine's default,
1721
1758
  // so the wire never has to say "gate me" to be gated.
1722
- if (b.deliverGate === 'auto')
1759
+ if (b.deliverGate === 'auto') {
1760
+ // wicked-core#850 EX-03: a workflow that requires QE acceptance delivers only through the deliver
1761
+ // gate, where crew checks the verdict. Unattended delivery would push before anyone could.
1762
+ const required = b.workflow !== undefined ? adapter.getWorkflow(b.workflow)?.required_instruments : undefined;
1763
+ if (required?.includes(QE_ACCEPTANCE) === true) {
1764
+ return reply.code(400).send({
1765
+ error: `workflow ${b.workflow} requires QE acceptance (required_instruments: qe_acceptance), so it delivers only ` +
1766
+ 'through the deliver gate, where the verdict is checked — deliverGate: "auto" cannot apply to it',
1767
+ });
1768
+ }
1723
1769
  input.autoDeliver = true;
1770
+ }
1724
1771
  if (b.repoRef !== undefined)
1725
1772
  input.repoRef = b.repoRef;
1726
1773
  if (b.workflow !== undefined)
@@ -1786,6 +1833,10 @@ runtime = {}) {
1786
1833
  }
1787
1834
  if (deliver === 'pr')
1788
1835
  input.deliver = 'pr';
1836
+ // wicked-core#850 EX-01/EX-02: forwarded only when the caller sent it — crew never waives
1837
+ // assurance on anyone's behalf (a one-CLI launch that did not is TOLD so in its answer, below).
1838
+ if (b.reducedAssurance === true)
1839
+ input.reducedAssurance = true;
1789
1840
  // DES-L9 / crew#550 — REVISION: `revisesPr` names an OPEN same-repository pull request whose
1790
1841
  // head branch becomes the run's base (`baseRef`, crew-internal → the engine's
1791
1842
  // `LaunchSpec.base_ref`) and the push target of the composed deliver phase — the PR gains
@@ -1907,6 +1958,9 @@ runtime = {}) {
1907
1958
  // trail says what the run will actually do, and whether the daemon decided it.
1908
1959
  deliver,
1909
1960
  ...(deliverDefaulted ? { deliverDefaulted: true } : {}),
1961
+ // wicked-core#850: the caller's explicit assurance choice (the engine persists the contract
1962
+ // on the session; this records WHO waived it).
1963
+ ...(b.reducedAssurance !== undefined ? { reducedAssurance: b.reducedAssurance } : {}),
1910
1964
  // CREW-UX-3: the trail is the durable record of lineage — the retry index (and a
1911
1965
  // restarted daemon's hydrate) reads it back from exactly this entry.
1912
1966
  ...(b.retryOf !== undefined ? { retryOf: b.retryOf } : {}),
@@ -1963,7 +2017,11 @@ runtime = {}) {
1963
2017
  // was built from, the launch answer says so (a recorded proof can disclose which garden ran).
1964
2018
  const skillsAhead = runtime.skills?.installedAheadFinding({ fresh: true }) ?? null; // fresh: an upgrade just before Send counts (codex r1)
1965
2019
  const skillsWarning = skillsAhead === null ? {} : { skillsWarning: { kind: skillsAhead.kind, severity: skillsAhead.severity, message: skillsAhead.message } };
1966
- return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}), ...freeText, ...skillsWarning });
2020
+ // wicked-core#850: a full-assurance run on a one-CLI roster will wait at a gate for a seat that
2021
+ // can evaluate or judge it; the answer names the explicit opt-in to relaunch with.
2022
+ const notice = launchAssuranceNotice({ runId, reducedAssurance: b.reducedAssurance, engineClisJson: engineRosterJson(input.clisJson) });
2023
+ const assuranceNotice = notice === null ? {} : { assuranceNotice: notice };
2024
+ return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}), ...freeText, ...skillsWarning, ...assuranceNotice });
1967
2025
  }
1968
2026
  catch (err) {
1969
2027
  const msg = message(err);
@@ -2137,6 +2195,61 @@ runtime = {}) {
2137
2195
  await settleDeliveries([run]);
2138
2196
  return { run: decorateRun(run) };
2139
2197
  });
2198
+ // ── Inventory claims (crew#721) — whether each enumerating step's list is complete ──
2199
+ // A step that lists things a later step acts on ends its reply with a `wicked-inventory` block
2200
+ // (wicked-garden core/refs/inventory-report.md). This reads them off every unit's captured
2201
+ // output, so a reviewer sees "partial: 9 of 10, issue #936 unread" without opening transcripts
2202
+ // (crew#648). `complete` is true only when at least one unit claimed an inventory, every claim is
2203
+ // `full` and every unit's output was readable; `readable: false` when the adapter keeps none.
2204
+ app.get(`${V}/runs/:id/inventory`, {
2205
+ config: {
2206
+ manifest: {
2207
+ responseType: 'RunInventoryResponse',
2208
+ statusCodes: [200, 404],
2209
+ },
2210
+ },
2211
+ }, async (req, reply) => {
2212
+ const { id } = req.params;
2213
+ const views = await adapter.sessionsDetail();
2214
+ const run = views.find((v) => v.session.id === id);
2215
+ if (!run)
2216
+ return reply.code(404).send({ error: 'Run not found' });
2217
+ if (typeof adapter.workOutput !== 'function') {
2218
+ return { runId: id, readable: false, complete: false, units: [], unreadUnits: [] };
2219
+ }
2220
+ const units = [];
2221
+ // A unit whose output could not be READ is not a unit that claimed nothing: it may hold the
2222
+ // partial claim, so it is listed and the run is never `complete` while any remain (codex r1).
2223
+ const unreadUnits = [];
2224
+ for (const u of [...run.units].sort((a, b) => a.ord - b.ord)) {
2225
+ let text;
2226
+ try {
2227
+ text = await adapter.workOutput(coreUnitId(id, u));
2228
+ }
2229
+ catch {
2230
+ unreadUnits.push(u.id);
2231
+ continue;
2232
+ }
2233
+ if (text === null) {
2234
+ // No output from a unit that has not run is expected; from one that finished, the
2235
+ // transcript is missing and may have held the claim (codex r3).
2236
+ if (u.status === 'done' || u.status === 'rejected')
2237
+ unreadUnits.push(u.id);
2238
+ continue;
2239
+ }
2240
+ const claims = parseInventoryClaims(text);
2241
+ if (claims.length > 0)
2242
+ units.push({ ord: u.ord, unitId: u.id, claims });
2243
+ }
2244
+ const all = units.flatMap((u) => u.claims);
2245
+ return {
2246
+ runId: id,
2247
+ readable: true,
2248
+ complete: inventoryComplete(all) && unreadUnits.length === 0,
2249
+ units,
2250
+ unreadUnits,
2251
+ };
2252
+ });
2140
2253
  // ── Deliver text (crew#524 / F-3R2-014) — the PR title + body a run's delivery carries ──
2141
2254
  // `gh pr create --fill` gave wicked-studio#249 a mid-word title and an EMPTY body. The deliver
2142
2255
  // script now asks this route for the text composed from the PERSISTED RUN RECORD: the intent,
@@ -2217,8 +2330,12 @@ runtime = {}) {
2217
2330
  // re-run on a delivered run, so a double-click (or a retry after a slow response) cannot
2218
2331
  // double-open. The index only ever holds real runs, so this needs no store round-trip.
2219
2332
  const existing = deliveryIndex.urlFor(id);
2220
- if (existing !== undefined)
2221
- return { prUrl: existing };
2333
+ if (existing !== undefined) {
2334
+ const recorded = deliveryIndex.assuranceFor(id);
2335
+ // crew#720: the run's final-codebase zip rides the replay too.
2336
+ const zip = runtime.codebaseArchives?.get(id);
2337
+ return { prUrl: existing, ...(recorded !== undefined ? { assurance: recorded } : {}), ...(zip !== undefined ? { codebase: zip } : {}) };
2338
+ }
2222
2339
  // N1: a run whose branch is already on a non-GitHub origin has nothing left to lift, and no
2223
2340
  // pull request can be opened from here — say so instead of pushing the branch again.
2224
2341
  const pushedAlready = deliveryIndex.pushedFor(id);
@@ -2252,6 +2369,12 @@ runtime = {}) {
2252
2369
  // back to `| null`.
2253
2370
  const repoRef = s.repo_ref;
2254
2371
  const initialWorkdir = s.workdir;
2372
+ // wicked-core#850 EX-03: a run whose contract requires QE acceptance is lifted only on a PASS
2373
+ // attributed to it — checked BEFORE anything is spawned, so a refusal pushes nothing.
2374
+ const qeAcceptance = await qeAcceptanceCheckFor(run);
2375
+ if (qeAcceptance !== null && !qeAcceptance.satisfied) {
2376
+ return reply.code(409).send(qeAcceptanceRefusal(id, qeAcceptance, aggregateReceipt(run, null), 'post_hoc'));
2377
+ }
2255
2378
  // One delivery per run at a time: the script pushes and opens a PR, so two concurrent
2256
2379
  // spawns could race gh into two PRs — the exact double-open idempotency forbids. The guard
2257
2380
  // wraps the reprovision too, so a reaped run's `wicked/<id>` branch is only ever checked out
@@ -2272,6 +2395,7 @@ runtime = {}) {
2272
2395
  // that branch and lift THAT; the branch (the record) is untouched. `cleanupWorktree` tears
2273
2396
  // the throwaway down after the lift.
2274
2397
  let result;
2398
+ let codebase;
2275
2399
  let worktreeGone = false;
2276
2400
  // DES-L9: a stranded REVISION re-pushes onto its PR's branch (re-checked OPEN via the same
2277
2401
  // resolver), never a new PR; a PR that closed meanwhile is a named 409.
@@ -2330,6 +2454,17 @@ runtime = {}) {
2330
2454
  // remotes), so the PR opens where origin points rather than where gh's default does.
2331
2455
  originUrl: await readDeliverOriginUrl(workdir),
2332
2456
  });
2457
+ // crew#720: the final-codebase zip, taken while this checkout (perhaps a throwaway)
2458
+ // still exists — whatever the script's verdict.
2459
+ const archives = runtime.codebaseArchives;
2460
+ if (archives !== undefined) {
2461
+ try {
2462
+ codebase = (await archives.archive(id, { workdir, repoRoot: (await repoRootOf(repoRef)) ?? null }, 'deliver')) ?? undefined;
2463
+ }
2464
+ catch {
2465
+ // Best-effort: the archive store logs its own failures; a lookup that threw leaves none.
2466
+ }
2467
+ }
2333
2468
  }
2334
2469
  finally {
2335
2470
  if (cw !== null)
@@ -2368,6 +2503,8 @@ runtime = {}) {
2368
2503
  // guarantees nothing was pushed on the refusing paths.
2369
2504
  return reply.code(409).send({
2370
2505
  error: `deliver failed (exit ${result.status}): ${deliverErrorTail(result.output)}`,
2506
+ // crew#720: the final-codebase zip is there whatever the verdict.
2507
+ ...(codebase !== undefined ? { codebase } : {}),
2371
2508
  });
2372
2509
  }
2373
2510
  // The SAME parser the in-run record uses: the script's push-only verdict outranks a URL a
@@ -2375,12 +2512,16 @@ runtime = {}) {
2375
2512
  const record = deliveryRecordFrom(result.output);
2376
2513
  const url = record !== null && 'url' in record ? record.url : null;
2377
2514
  const pushedOnly = record !== null && 'pushed' in record ? record.pushed : null;
2515
+ // wicked-core#850 EX-04: no engine lift re-verified what this pushed — the run record says so,
2516
+ // with the trees the script named (the PR body carries the same label). Built before the
2517
+ // outcome branches, so a push-only delivery records it too (codex r1).
2518
+ const assurance = postHocDeliveryAssurance(run, unverifiedTreesFrom(result.output), qeAcceptance);
2378
2519
  if (pushedOnly !== null) {
2379
2520
  // N1: the script pushed the branch and gh could resolve no GitHub repository for the
2380
2521
  // origin. That IS a delivery — record it, so the run reads `delivery: 'pushed'` — but there
2381
2522
  // is no PR URL to answer with, so the reply says what happened instead of a 200 `prUrl`.
2382
- audit.record('run.delivered', actorOf(req), { runId: id, detail: { pushed: pushedOnly, via: 'post-hoc' } });
2383
- deliveryIndex.setPushed(id, pushedOnly);
2523
+ audit.record('run.delivered', actorOf(req), { runId: id, detail: { pushed: pushedOnly, via: 'post-hoc', assurance } });
2524
+ deliveryIndex.setPushed(id, pushedOnly, assurance);
2384
2525
  return reply.code(409).send({ error: pushedOnlyRefusal(id, pushedOnly) });
2385
2526
  }
2386
2527
  if (url === null) {
@@ -2392,9 +2533,9 @@ runtime = {}) {
2392
2533
  }
2393
2534
  // The durable record first, then the read-side index — the same write order as the
2394
2535
  // deliver-phase resolution in server.ts, so the index can only LAG a crash, never lead it.
2395
- audit.record('run.delivered', actorOf(req), { runId: id, detail: { url, via: 'post-hoc' } });
2396
- deliveryIndex.set(id, url);
2397
- return { prUrl: url };
2536
+ audit.record('run.delivered', actorOf(req), { runId: id, detail: { url, via: 'post-hoc', assurance, ...(codebase !== undefined ? { codebase } : {}) } });
2537
+ deliveryIndex.set(id, url, assurance);
2538
+ return { prUrl: url, assurance, ...(codebase !== undefined ? { codebase } : {}) };
2398
2539
  });
2399
2540
  // Durable pre-gate guidance (DES-UX-002 §7.2 — spec'd there as CREW-UX-4, implemented as
2400
2541
  // CREW-UX-7 because crew#308 already spent that id; see guidance-index.ts). Upserts the ONE
@@ -2517,6 +2658,7 @@ runtime = {}) {
2517
2658
  scratchBase: chatScopes.base,
2518
2659
  log: (m) => req.log.warn(m),
2519
2660
  ...(runtime.entityCount !== undefined ? { entityCount: runtime.entityCount } : {}),
2661
+ ...(runtime.freshenCheckout !== undefined ? { freshen: runtime.freshenCheckout } : {}),
2520
2662
  });
2521
2663
  if (!resolution.ok) {
2522
2664
  chatScopes.release(chatId, token);
@@ -3056,6 +3198,8 @@ runtime = {}) {
3056
3198
  adapter,
3057
3199
  settings: projectSettings,
3058
3200
  docRuns,
3201
+ // crew#896: the index carries the grounding the per-project list carries.
3202
+ ...(runtime.docGrounding !== undefined ? { groundingStore: runtime.docGrounding } : {}),
3059
3203
  includeRetired: flag === '1' || flag === 'true',
3060
3204
  });
3061
3205
  });
@@ -3138,6 +3282,25 @@ runtime = {}) {
3138
3282
  // `outputUnavailable` saying WHICH cause applies — denied, not finished, or a store that
3139
3283
  // disagrees with itself. It previously answered `200 {"output": null}` to all four, so the
3140
3284
  // failed unit an operator opens during triage was the one that told them nothing (FINDING-006).
3285
+ // crew#720: the run's final-codebase zip (every delivery, every outcome). 404 for an unknown run
3286
+ // or one with no archive yet; the record (`AgentSession.codebase_archive`) names the same sha256,
3287
+ // which the answer also carries as `X-Content-SHA256`.
3288
+ app.get(`${V}/runs/:id/artifacts/codebase.zip`, { config: { manifest: { responseType: 'binary (application/zip)', statusCodes: [200, 404] } } }, async (req, reply) => {
3289
+ const { id } = req.params;
3290
+ const opened = runtime.codebaseArchives?.open(id) ?? null;
3291
+ if (opened === null || opened.rec.url !== codebaseArchiveUrl(id)) {
3292
+ opened?.stream.destroy();
3293
+ return reply.code(404).send({ error: `run ${id} has no codebase archive` });
3294
+ }
3295
+ const rec = opened.rec;
3296
+ const safe = id.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80);
3297
+ return reply
3298
+ .header('content-type', 'application/zip')
3299
+ .header('content-disposition', `attachment; filename="${safe}-codebase.zip"`)
3300
+ .header('x-content-sha256', rec.sha256)
3301
+ .header('content-length', String(rec.bytes))
3302
+ .send(opened.stream);
3303
+ });
3141
3304
  app.get(`${V}/runs/:id/units/:unitKey/output`, async (req, reply) => {
3142
3305
  const { id, unitKey } = req.params;
3143
3306
  const views = await adapter.sessionsDetail();
@@ -3262,12 +3425,22 @@ runtime = {}) {
3262
3425
  const run = views.find((v) => v.session.id === id);
3263
3426
  if (!run)
3264
3427
  return reply.code(404).send({ error: 'Run not found' });
3428
+ return acceptanceViewOf(run, { ...(qeRunId !== undefined && qeRunId !== '' ? { qeRunId } : {}) });
3429
+ });
3430
+ /**
3431
+ * One run's acceptance answer — the body `GET /runs/:id/acceptance` serves, and what a delivery
3432
+ * reads (wicked-core#850 EX-03). `forceDeclared`: the run's assurance CONTRACT requires QE
3433
+ * acceptance, so a verdict is owed even when no phase raises `verified_evidence` — missing denies.
3434
+ */
3435
+ async function acceptanceViewOf(run, opts) {
3436
+ const id = run.session.id;
3265
3437
  // What the run must prove, from what it CONTAINS (seam X2): a plan or preset run's steps whose
3266
3438
  // catalog entry re-verifies evidence, a registered workflow's def, nothing for free text — and
3267
3439
  // an unknown run (or one the daemon cannot read) is declared and denied with the reason.
3268
3440
  const verifiedCatalog = typeof adapter.verifiedEvidenceCatalog === 'function' ? await adapter.verifiedEvidenceCatalog() : null;
3269
3441
  const knownCatalog = typeof adapter.catalogIds === 'function' ? await adapter.catalogIds() : null;
3270
- const requirement = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog, knownCatalog);
3442
+ const read = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog, knownCatalog);
3443
+ const requirement = opts.forceDeclared === true ? { ...read, declared: true } : read;
3271
3444
  // WT-W2: every walkthrough step the requirement names is resolved from its proof root and seal.
3272
3445
  const walkthroughs = await walkthroughAcceptance(adapter, run, requirement.phases);
3273
3446
  let repo = null;
@@ -3279,7 +3452,7 @@ runtime = {}) {
3279
3452
  runId: id,
3280
3453
  repo,
3281
3454
  requirement,
3282
- ...(qeRunId !== undefined && qeRunId !== '' ? { qeRunId } : {}),
3455
+ ...(opts.qeRunId !== undefined ? { qeRunId: opts.qeRunId } : {}),
3283
3456
  // AW-14 (arch-R13a + R16): the conformance section reads the same wires the standalone
3284
3457
  // `/governance/claims` and `/runs/:id/events` routes serve, but run-scoped and resolved
3285
3458
  // deny-dominates BESIDE the QE gate — so a wiki-rule violation and an unenforced governed
@@ -3308,7 +3481,19 @@ runtime = {}) {
3308
3481
  })(),
3309
3482
  },
3310
3483
  });
3311
- });
3484
+ }
3485
+ /** EX-03: the run's QE acceptance check when its contract requires `qe_acceptance`; `null` when it
3486
+ * does not. A read that fails is a refusal naming why, never a pass. */
3487
+ async function qeAcceptanceCheckFor(run) {
3488
+ if (!requiresQeAcceptance(run))
3489
+ return null;
3490
+ try {
3491
+ return qeAcceptanceFromView(await acceptanceViewOf(run, { forceDeclared: true }));
3492
+ }
3493
+ catch (err) {
3494
+ return unreadableQeAcceptance(err);
3495
+ }
3496
+ }
3312
3497
  /**
3313
3498
  * The run's OPEN gate, resolved the way `GET /runs/:id/gate` serves it: the cache, then the
3314
3499
  * engine's durable `interaction_requests` row (adopted into the cache), then the event-log replay.
@@ -3343,6 +3528,35 @@ runtime = {}) {
3343
3528
  return 'no-log';
3344
3529
  return gateCache.rebuild(id, events) ?? null;
3345
3530
  }
3531
+ /** The unit an open gate holds: the named `ord`, else the open gate's, else the cursor's. */
3532
+ async function gatedUnitOf(run, ord) {
3533
+ let gateOrd = ord;
3534
+ if (gateOrd === undefined) {
3535
+ // A route set whose adapter cannot read the gate (a partial stub without `runEvents`) falls
3536
+ // back to the cursor unit, exactly as a log that records no open gate does.
3537
+ const open = await resolveOpenGate(run.session.id).catch(() => null);
3538
+ gateOrd = open !== null && open !== 'no-log' ? open.ord : undefined;
3539
+ }
3540
+ const units = run.units ?? [];
3541
+ return gateOrd !== undefined
3542
+ ? units.find((u) => u.ord === gateOrd)
3543
+ : [...units].sort((a, b) => a.ord - b.ord)[run.session.unit_ix];
3544
+ }
3545
+ /**
3546
+ * The checks an APPROVE that runs the deliver unit must pass — the ONE place for both gate paths
3547
+ * (`POST /runs/:id/gate`, standing orders, and a gated `POST /runs/:id/resume`): the delivery
3548
+ * freeze (idea 15), and wicked-core#850 EX-03 — a run whose contract requires QE acceptance
3549
+ * delivers only on an attributed PASS. A refusal leaves the gate open.
3550
+ */
3551
+ async function deliverApprovalRefusal(run, deliverUnit) {
3552
+ if (deliveryFreeze.state().frozen)
3553
+ return { refusal: { code: 409, body: frozenRefusal(deliveryFreeze.state()) } };
3554
+ const qeAcceptance = await qeAcceptanceCheckFor(run);
3555
+ if (qeAcceptance !== null && !qeAcceptance.satisfied) {
3556
+ return { refusal: { code: 409, body: qeAcceptanceRefusal(run.session.id, qeAcceptance, receiptOf(deliverUnit), 'gate') } };
3557
+ }
3558
+ return { qeAcceptance };
3559
+ }
3346
3560
  /**
3347
3561
  * THE gate decision path. `POST /runs/:id/gate` answers with it, and so does a standing order
3348
3562
  * (behaviour 10) — the same awaiting check, the same ord check and 409 `gate_changed` /
@@ -3383,22 +3597,17 @@ runtime = {}) {
3383
3597
  } };
3384
3598
  }
3385
3599
  }
3386
- // Freeze deliveries (idea 15): an APPROVE that would run the run's deliver unit is held while
3387
- // the switch is on — the gate stays open, so unfreezing lets the same approve through. The gate
3388
- // is the named one, else the open one, else the unit the run's cursor points at.
3389
- if (parsed.data.approve && deliveryFreeze.state().frozen) {
3390
- let gateOrd = parsed.data.ord;
3391
- if (gateOrd === undefined) {
3392
- const open = await resolveOpenGate(id);
3393
- gateOrd = open !== null && open !== 'no-log' ? open.ord : undefined;
3394
- }
3395
- const units = run.units ?? [];
3396
- const gated = gateOrd !== undefined
3397
- ? units.find((u) => u.ord === gateOrd)
3398
- : [...units].sort((a, b) => a.ord - b.ord)[run.session.unit_ix];
3399
- if (gated !== undefined && isDeliverUnit(gated)) {
3400
- return { code: 409, body: frozenRefusal(deliveryFreeze.state()) };
3401
- }
3600
+ // The gated unit — the named gate, else the open one, else the unit the run's cursor points at —
3601
+ // and its receipt as it stands when the person decides (wicked-core#850: every gate answer
3602
+ // carries it). An APPROVE that would run the deliver unit passes the delivery checks first.
3603
+ const gated = await gatedUnitOf(run, parsed.data.ord);
3604
+ const assurance = receiptOf(gated);
3605
+ let deliveryCheck;
3606
+ if (parsed.data.approve && gated !== undefined && isDeliverUnit(gated)) {
3607
+ const refused = await deliverApprovalRefusal(run, gated);
3608
+ if ('refusal' in refused)
3609
+ return refused.refusal;
3610
+ deliveryCheck = { qeAcceptance: refused.qeAcceptance };
3402
3611
  }
3403
3612
  // The steering-author landing (crew#388): decided — and the gate prompt captured — BEFORE
3404
3613
  // the confirm, because a terminal-phase approve prunes the gate cache and moves the run out
@@ -3452,7 +3661,15 @@ runtime = {}) {
3452
3661
  const landing = steeringPropose
3453
3662
  ? await landSteeringProposal({ adapter, audit, actor }, run, gatePrompt)
3454
3663
  : undefined;
3455
- return { code: 200, body: { status, ...(landing !== undefined ? { landing } : {}) } };
3664
+ return {
3665
+ code: 200,
3666
+ body: {
3667
+ status,
3668
+ ...(landing !== undefined ? { landing } : {}),
3669
+ assurance,
3670
+ ...(deliveryCheck !== undefined ? { delivery: deliveryCheck } : {}),
3671
+ },
3672
+ };
3456
3673
  }
3457
3674
  catch (err) {
3458
3675
  if (err instanceof PlanLaunchUnsupportedError) {
@@ -3466,7 +3683,7 @@ runtime = {}) {
3466
3683
  config: {
3467
3684
  manifest: {
3468
3685
  requestType: 'GateDecision',
3469
- responseType: '{ status: SessionStatus; landing?: SteeringLandingResult }',
3686
+ responseType: 'GateDecisionResult',
3470
3687
  // 409 twice over: a run not awaiting a human gate, and an engine refusal at confirm.
3471
3688
  // 501: an edited plan on an addon without the plan approval gate (DES-TEAMING-002 T3).
3472
3689
  statusCodes: [200, 400, 404, 409, 501],
@@ -3536,6 +3753,16 @@ runtime = {}) {
3536
3753
  // write too (crew#388). Prompt captured pre-confirm; see the gate route.
3537
3754
  const steeringPropose = gated && isSteeringAuthorRun(run);
3538
3755
  const gatePrompt = steeringPropose ? gateCache.get(id)?.prompt : undefined;
3756
+ // A gated resume IS an approve, so it passes the same delivery checks (freeze, EX-03) — no side
3757
+ // door around the deliver gate — and answers with the gated unit's receipt.
3758
+ const gatedUnit = gated ? await gatedUnitOf(run, undefined) : undefined;
3759
+ let deliveryCheck;
3760
+ if (gatedUnit !== undefined && isDeliverUnit(gatedUnit)) {
3761
+ const refused = await deliverApprovalRefusal(run, gatedUnit);
3762
+ if ('refusal' in refused)
3763
+ return reply.code(refused.refusal.code).send(refused.refusal.body);
3764
+ deliveryCheck = { qeAcceptance: refused.qeAcceptance };
3765
+ }
3539
3766
  const status = gated ? await adapter.confirmGate(id, true) : await adapter.resumeRun(id);
3540
3767
  // A resume of a gated run IS a gate approval — audit it as one, so the
3541
3768
  // "who approved" trail has no side door (task #88).
@@ -3546,7 +3773,12 @@ runtime = {}) {
3546
3773
  const landing = steeringPropose
3547
3774
  ? await landSteeringProposal({ adapter, audit, actor: actorOf(req) }, run, gatePrompt)
3548
3775
  : undefined;
3549
- return reply.send({ status, ...(landing !== undefined ? { landing } : {}) });
3776
+ return reply.send({
3777
+ status,
3778
+ ...(landing !== undefined ? { landing } : {}),
3779
+ ...(gated ? { assurance: receiptOf(gatedUnit) } : {}),
3780
+ ...(deliveryCheck !== undefined ? { delivery: deliveryCheck } : {}),
3781
+ });
3550
3782
  }
3551
3783
  catch (err) {
3552
3784
  return reply.code(409).send({ error: message(err) });
@@ -3640,6 +3872,16 @@ runtime = {}) {
3640
3872
  });
3641
3873
  }
3642
3874
  }
3875
+ // A gated reassign APPROVES the gate first, so it passes the same delivery checks (freeze,
3876
+ // wicked-core#850 EX-03) when the gate holds the deliver unit (codex r1 on the EX-03 PR).
3877
+ if (gated) {
3878
+ const gatedUnit = await gatedUnitOf(run, undefined);
3879
+ if (gatedUnit !== undefined && isDeliverUnit(gatedUnit)) {
3880
+ const refused = await deliverApprovalRefusal(run, gatedUnit);
3881
+ if ('refusal' in refused)
3882
+ return reply.code(refused.refusal.code).send(refused.refusal.body);
3883
+ }
3884
+ }
3643
3885
  const cursor = resolveCursorUnit(run);
3644
3886
  const ord = cursor?.ord ?? run.session.unit_ix;
3645
3887
  try {
@@ -3674,9 +3916,14 @@ runtime = {}) {
3674
3916
  // read it. A restart is routine: deploy, crash, laptop sleep.
3675
3917
  app.get(`${V}/runs/:id/gate`, async (req, reply) => {
3676
3918
  const { id } = req.params;
3919
+ // wicked-core#850: the gate answer carries the gated unit's receipt — what assured the work it asks about.
3920
+ const receiptAt = async (ord) => {
3921
+ const run = (await adapter.sessionsDetail()).find((v) => v.session.id === id);
3922
+ return receiptOf(run?.units?.find((u) => u.ord === ord));
3923
+ };
3677
3924
  const cached = gateCache.get(id);
3678
3925
  if (cached)
3679
- return { runId: id, ...cached };
3926
+ return { runId: id, ...cached, assurance: await receiptAt(cached.ord) };
3680
3927
  // A miss is not evidence of anything on its own, so ask the run what it is doing before paying
3681
3928
  // to replay it. Only `awaiting_human` can have an open gate, and that answer is definitive:
3682
3929
  // every other status is a 404 that needs no log at all — which is also the overwhelmingly
@@ -3712,7 +3959,7 @@ runtime = {}) {
3712
3959
  // genuinely lost), as does a gate whose `awaitingHuman` predates the log's retention.
3713
3960
  return reply.code(404).send({ error: 'No open gate for this run' });
3714
3961
  }
3715
- return { runId: id, ...open };
3962
+ return { runId: id, ...open, assurance: receiptOf(run.units?.find((u) => u.ord === open.ord)) };
3716
3963
  });
3717
3964
  // ── Elicitation (DES-002) ────────────────────────────────────────────────────
3718
3965
  //
@@ -4634,7 +4881,9 @@ runtime = {}) {
4634
4881
  if (repo === undefined)
4635
4882
  return reply.code(404).send({ error: `Repo ${id} not found` });
4636
4883
  const originUrl = repo.root_path === '' ? null : await readDeliverOriginUrl(repo.root_path);
4637
- return reply.send({ repo: repo.id, ...deliverTargetView(originUrl) });
4884
+ // crew#720: the provider credential preflight, so a launch can say "not configured" up front.
4885
+ const credentials = await deliverCredentialsProbe(originUrl, process.env);
4886
+ return reply.send({ repo: repo.id, ...deliverTargetView(originUrl), credentials });
4638
4887
  });
4639
4888
  app.get(`${V}/repos/:id/git-history`, async (req, reply) => {
4640
4889
  const { id } = req.params;
@@ -5513,6 +5762,10 @@ runtime = {}) {
5513
5762
  });
5514
5763
  // ── Projects (DES-PROJECT-001) — the 9-route experience-plane surface ────────
5515
5764
  registerProjectRoutes(app, adapter, { ...projects, settings: projectSettings }, security);
5765
+ // crew#371: a project's requirements / domain / coverage, folded over its repos' own reads.
5766
+ registerProjectAggregateRoutes(app, adapter);
5767
+ // crew#372: draft a product plan from selected requirements, as a governed run via POST /runs.
5768
+ registerProductRoutes(app, adapter);
5516
5769
  // ── Capture (Studio OS behaviour 8) — notes and photos → a project-filed run whose output is
5517
5770
  // proposals in the queue above; the launch is POST /runs itself (api/capture.ts).
5518
5771
  registerCaptureRoutes(app);