wicked-crew 0.8.9 → 0.9.0

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 (110) 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 +4 -0
  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/delivery-assurance.d.ts +48 -0
  19. package/dist/api/delivery-assurance.d.ts.map +1 -0
  20. package/dist/api/delivery-assurance.js +118 -0
  21. package/dist/api/delivery-assurance.js.map +1 -0
  22. package/dist/api/delivery-index.d.ts +11 -5
  23. package/dist/api/delivery-index.d.ts.map +1 -1
  24. package/dist/api/delivery-index.js +38 -7
  25. package/dist/api/delivery-index.js.map +1 -1
  26. package/dist/api/product.d.ts +67 -0
  27. package/dist/api/product.d.ts.map +1 -0
  28. package/dist/api/product.js +169 -0
  29. package/dist/api/product.js.map +1 -0
  30. package/dist/api/project-aggregates.d.ts +61 -0
  31. package/dist/api/project-aggregates.d.ts.map +1 -0
  32. package/dist/api/project-aggregates.js +296 -0
  33. package/dist/api/project-aggregates.js.map +1 -0
  34. package/dist/api/recording.d.ts +4 -0
  35. package/dist/api/recording.d.ts.map +1 -1
  36. package/dist/api/recording.js +6 -1
  37. package/dist/api/recording.js.map +1 -1
  38. package/dist/api/routes.d.ts +43 -17
  39. package/dist/api/routes.d.ts.map +1 -1
  40. package/dist/api/routes.js +245 -40
  41. package/dist/api/routes.js.map +1 -1
  42. package/dist/api/run-files.d.ts.map +1 -1
  43. package/dist/api/run-files.js +11 -2
  44. package/dist/api/run-files.js.map +1 -1
  45. package/dist/api/server.d.ts.map +1 -1
  46. package/dist/api/server.js +3 -0
  47. package/dist/api/server.js.map +1 -1
  48. package/dist/api/stall-watchdog.d.ts.map +1 -1
  49. package/dist/api/stall-watchdog.js +6 -1
  50. package/dist/api/stall-watchdog.js.map +1 -1
  51. package/dist/api/steering-landing.d.ts +1 -1
  52. package/dist/api/steering-landing.d.ts.map +1 -1
  53. package/dist/api/steering-landing.js +65 -29
  54. package/dist/api/steering-landing.js.map +1 -1
  55. package/dist/cli/index.d.ts.map +1 -1
  56. package/dist/cli/index.js +22 -1
  57. package/dist/cli/index.js.map +1 -1
  58. package/dist/core/adapter.d.ts +3 -0
  59. package/dist/core/adapter.d.ts.map +1 -1
  60. package/dist/core/adapter.js +23 -2
  61. package/dist/core/adapter.js.map +1 -1
  62. package/dist/core/assurance.d.ts +60 -0
  63. package/dist/core/assurance.d.ts.map +1 -0
  64. package/dist/core/assurance.js +88 -0
  65. package/dist/core/assurance.js.map +1 -0
  66. package/dist/core/deliver-exclusions.d.ts +9 -1
  67. package/dist/core/deliver-exclusions.d.ts.map +1 -1
  68. package/dist/core/deliver-exclusions.js +37 -2
  69. package/dist/core/deliver-exclusions.js.map +1 -1
  70. package/dist/core/deliver.d.ts +25 -1
  71. package/dist/core/deliver.d.ts.map +1 -1
  72. package/dist/core/deliver.js +77 -9
  73. package/dist/core/deliver.js.map +1 -1
  74. package/dist/core/deliverable-floor.d.ts.map +1 -1
  75. package/dist/core/deliverable-floor.js +6 -1
  76. package/dist/core/deliverable-floor.js.map +1 -1
  77. package/dist/core/inventory-claim.d.ts +34 -0
  78. package/dist/core/inventory-claim.d.ts.map +1 -0
  79. package/dist/core/inventory-claim.js +138 -0
  80. package/dist/core/inventory-claim.js.map +1 -0
  81. package/dist/core/types.d.ts +7 -0
  82. package/dist/core/types.d.ts.map +1 -1
  83. package/dist/core/types.js.map +1 -1
  84. package/dist/editors/registry.d.ts +3 -3
  85. package/dist/interactive/bridge-pool.d.ts +8 -2
  86. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  87. package/dist/interactive/bridge-pool.js +7 -1
  88. package/dist/interactive/bridge-pool.js.map +1 -1
  89. package/dist/interactive/docs-index.d.ts +8 -0
  90. package/dist/interactive/docs-index.d.ts.map +1 -1
  91. package/dist/interactive/docs-index.js +5 -2
  92. package/dist/interactive/docs-index.js.map +1 -1
  93. package/dist/qe/acceptance.d.ts.map +1 -1
  94. package/dist/qe/acceptance.js +49 -1
  95. package/dist/qe/acceptance.js.map +1 -1
  96. package/dist/standing-orders/history.d.ts +8 -0
  97. package/dist/standing-orders/history.d.ts.map +1 -1
  98. package/dist/standing-orders/history.js +8 -1
  99. package/dist/standing-orders/history.js.map +1 -1
  100. package/dist/studio/assets/index--O1AdvNx.css +32 -0
  101. package/dist/studio/assets/index-B1MYiNq-.js +567 -0
  102. package/dist/studio/editors/wicked-page/editor.json +1 -1
  103. package/dist/studio/editors/wicked-page/index.html +1 -1
  104. package/dist/studio/index.html +2 -2
  105. package/dist/studio/testid-inventory.json +764 -79
  106. package/endpoint-manifest.json +63 -2
  107. package/package.json +8 -8
  108. package/skills/wicked-crew/SKILL.md +2 -1
  109. package/dist/studio/assets/index-DofYURLG.js +0 -564
  110. package/dist/studio/assets/index-fSSul5Hu.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,14 @@ 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';
59
64
  import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
60
65
  import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
61
66
  import { registerInteractiveDocChecks } from '../interactive/doc-checks-routes.js';
@@ -399,6 +404,11 @@ export const LaunchSchema = z.object({
399
404
  * the issue. They are answered as `linkedIssues[].excluded`. Send only when
400
405
  * `GET /health.capabilities.linkedIssuesExclude === true`. */
401
406
  excludeLinkedIssues: ExcludeLinkedIssuesSchema,
407
+ /** wicked-core#850 (api-types 0.102.0) — the EXPLICIT reduced-assurance opt-in: the creator's seat
408
+ * may evaluate and a judge-less gate may pass, disclosed on every receipt. `false` is an explicit
409
+ * "full assurance", the same as omitting it: the launcher never chooses for the caller, and a
410
+ * full-assurance launch on a one-CLI roster is answered with `assuranceNotice` (`core/assurance.ts`). */
411
+ reducedAssurance: z.boolean().optional(),
402
412
  }).strict().refine((b) => b.plan === undefined || b.workflow === undefined, {
403
413
  message: 'plan and workflow are mutually exclusive — a launch carries a plan or names a preset, not both',
404
414
  path: ['plan'],
@@ -428,7 +438,10 @@ export const LaunchSchema = z.object({
428
438
  * evaluator's discarded, pinned edit as the creator's amendment. The ENGINE refuses each at any
429
439
  * other gate (a 409 naming the gate it answers). */
430
440
  export const ESCALATION_ACTIONS = ['extend', 'targeted', 'accept_partial', 'accept_suggestion'];
431
- const isEscalationAction = (a) => a !== undefined && ESCALATION_ACTIONS.includes(a);
441
+ /** (core#820) A consent gate's answer token: `consent:<choice id>` approves that row of the dry-run
442
+ * plan (the engine refuses an id its plan does not offer); `reject` stays the decline. */
443
+ export const CONSENT_CHOICE_ACTION = /^consent:[A-Za-z0-9_-]{1,32}$/u; // core `parse_plan` caps a choice id at 32
444
+ const isEscalationAction = (a) => a !== undefined && (ESCALATION_ACTIONS.includes(a) || CONSENT_CHOICE_ACTION.test(a));
432
445
  /** `POST /runs/:id/gate` (api-types 0.38.0 `GateDecision`; DES-L1 PR-2). Additive arms: `action`
433
446
  * names the arm (`approve` | `request_changes` | `reject`; absent = today's two-arm mapping of
434
447
  * `approve`), `amendScope` says where an approve's `amend` lands (`cursor` = the gated unit, today's
@@ -445,7 +458,12 @@ const isEscalationAction = (a) => a !== undefined && ESCALATION_ACTIONS.includes
445
458
  export const GateSchema = z.object({
446
459
  approve: z.boolean(),
447
460
  amend: z.string().optional(),
448
- action: z.enum(['approve', 'request_changes', 'reject', 'edit_plan', 'amend_intent', ...ESCALATION_ACTIONS]).optional(),
461
+ action: z
462
+ .union([
463
+ z.enum(['approve', 'request_changes', 'reject', 'edit_plan', 'amend_intent', ...ESCALATION_ACTIONS]),
464
+ z.string().regex(CONSENT_CHOICE_ACTION, 'a consent choice is `consent:<choice id>`'),
465
+ ])
466
+ .optional(),
449
467
  amendScope: z.enum(['cursor', 'creator']).optional(),
450
468
  /** DES-TEAMING-002 T3 — approve a `plan_approval` gate WITH AN EDIT (`GateDecision.plan`). */
451
469
  plan: PlanSchema.optional(),
@@ -453,11 +471,11 @@ export const GateSchema = z.object({
453
471
  * open gate is a 409 `gate_changed`: the decision was made on a gate that is no longer open. */
454
472
  ord: z.number().int().nonnegative().optional(),
455
473
  }).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(
474
+ || (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
475
  // (wicked-core#555) An intent amendment IS its text: an empty one amends nothing, and the arm's
458
476
  // scope is every unit at or after the cursor — which is what makes it reach the evaluator — so
459
477
  // `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'] });
478
+ (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
479
  /** `PUT /runs/:id/guidance` (DES-UX-002 §7.2, CREW-UX-7) — the durable pre-gate note body.
462
480
  * The empty string is a legal body: it CLEARS the note. The byte cap is checked in the route
463
481
  * (not zod's char-counting `max`) so the 400 names the actual limit. */
@@ -932,6 +950,10 @@ runtime = {}) {
932
950
  view.session.launch_actor = launchActor;
933
951
  const state = resolveDelivery(view, conflictStrand);
934
952
  view.session.delivery = state.delivery;
953
+ // wicked-core#850 EX-04: a post-hoc delivery reads unverified, with its trees. ABSENT otherwise.
954
+ const deliveryAssurance = deliveryIndex.assuranceFor(view.session.id);
955
+ if (deliveryAssurance !== undefined)
956
+ view.session.delivery_assurance = deliveryAssurance;
935
957
  // crew#762: was delivery asked for? The launch's decision (`deliver: 'pr'`), a post-hoc attempt
936
958
  // or a delivery on record says yes; a launch that said `deliver: 'none'` with neither says no.
937
959
  // ABSENT when the daemon holds no launch record and no attempt (never fabricated).
@@ -1038,6 +1060,10 @@ runtime = {}) {
1038
1060
  askPath: typeof adapter.engineCapabilities === 'function' ? adapter.engineCapabilities().askPath : false,
1039
1061
  // WT-W1: repo-bound launches mint an evidence root only on an addon that takes the field.
1040
1062
  walkthroughRoots: typeof adapter.evidenceRootsSupported === 'function' ? adapter.evidenceRootsSupported() : false,
1063
+ // wicked-core#850: `LaunchRunBody.reducedAssurance` is accepted (the installed addon carries the
1064
+ // assurance contract); a one-CLI full-assurance launch answers `assuranceNotice`. A route set
1065
+ // that cannot probe the addon reports no capability — never an invented one.
1066
+ reducedAssurance: typeof adapter.engineCapabilities === 'function' && addonSupportsReducedAssurance(),
1041
1067
  };
1042
1068
  // wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
1043
1069
  // daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
@@ -1719,8 +1745,18 @@ runtime = {}) {
1719
1745
  // F-E2E-030: only the explicit `'auto'` opts out of the engine's deliver gate. `'human'` and
1720
1746
  // an omitted field both leave `autoDeliver` off the input — the gate is the engine's default,
1721
1747
  // so the wire never has to say "gate me" to be gated.
1722
- if (b.deliverGate === 'auto')
1748
+ if (b.deliverGate === 'auto') {
1749
+ // wicked-core#850 EX-03: a workflow that requires QE acceptance delivers only through the deliver
1750
+ // gate, where crew checks the verdict. Unattended delivery would push before anyone could.
1751
+ const required = b.workflow !== undefined ? adapter.getWorkflow(b.workflow)?.required_instruments : undefined;
1752
+ if (required?.includes(QE_ACCEPTANCE) === true) {
1753
+ return reply.code(400).send({
1754
+ error: `workflow ${b.workflow} requires QE acceptance (required_instruments: qe_acceptance), so it delivers only ` +
1755
+ 'through the deliver gate, where the verdict is checked — deliverGate: "auto" cannot apply to it',
1756
+ });
1757
+ }
1723
1758
  input.autoDeliver = true;
1759
+ }
1724
1760
  if (b.repoRef !== undefined)
1725
1761
  input.repoRef = b.repoRef;
1726
1762
  if (b.workflow !== undefined)
@@ -1786,6 +1822,10 @@ runtime = {}) {
1786
1822
  }
1787
1823
  if (deliver === 'pr')
1788
1824
  input.deliver = 'pr';
1825
+ // wicked-core#850 EX-01/EX-02: forwarded only when the caller sent it — crew never waives
1826
+ // assurance on anyone's behalf (a one-CLI launch that did not is TOLD so in its answer, below).
1827
+ if (b.reducedAssurance === true)
1828
+ input.reducedAssurance = true;
1789
1829
  // DES-L9 / crew#550 — REVISION: `revisesPr` names an OPEN same-repository pull request whose
1790
1830
  // head branch becomes the run's base (`baseRef`, crew-internal → the engine's
1791
1831
  // `LaunchSpec.base_ref`) and the push target of the composed deliver phase — the PR gains
@@ -1907,6 +1947,9 @@ runtime = {}) {
1907
1947
  // trail says what the run will actually do, and whether the daemon decided it.
1908
1948
  deliver,
1909
1949
  ...(deliverDefaulted ? { deliverDefaulted: true } : {}),
1950
+ // wicked-core#850: the caller's explicit assurance choice (the engine persists the contract
1951
+ // on the session; this records WHO waived it).
1952
+ ...(b.reducedAssurance !== undefined ? { reducedAssurance: b.reducedAssurance } : {}),
1910
1953
  // CREW-UX-3: the trail is the durable record of lineage — the retry index (and a
1911
1954
  // restarted daemon's hydrate) reads it back from exactly this entry.
1912
1955
  ...(b.retryOf !== undefined ? { retryOf: b.retryOf } : {}),
@@ -1963,7 +2006,11 @@ runtime = {}) {
1963
2006
  // was built from, the launch answer says so (a recorded proof can disclose which garden ran).
1964
2007
  const skillsAhead = runtime.skills?.installedAheadFinding({ fresh: true }) ?? null; // fresh: an upgrade just before Send counts (codex r1)
1965
2008
  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 });
2009
+ // wicked-core#850: a full-assurance run on a one-CLI roster will wait at a gate for a seat that
2010
+ // can evaluate or judge it; the answer names the explicit opt-in to relaunch with.
2011
+ const notice = launchAssuranceNotice({ runId, reducedAssurance: b.reducedAssurance, engineClisJson: engineRosterJson(input.clisJson) });
2012
+ const assuranceNotice = notice === null ? {} : { assuranceNotice: notice };
2013
+ return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}), ...freeText, ...skillsWarning, ...assuranceNotice });
1967
2014
  }
1968
2015
  catch (err) {
1969
2016
  const msg = message(err);
@@ -2137,6 +2184,61 @@ runtime = {}) {
2137
2184
  await settleDeliveries([run]);
2138
2185
  return { run: decorateRun(run) };
2139
2186
  });
2187
+ // ── Inventory claims (crew#721) — whether each enumerating step's list is complete ──
2188
+ // A step that lists things a later step acts on ends its reply with a `wicked-inventory` block
2189
+ // (wicked-garden core/refs/inventory-report.md). This reads them off every unit's captured
2190
+ // output, so a reviewer sees "partial: 9 of 10, issue #936 unread" without opening transcripts
2191
+ // (crew#648). `complete` is true only when at least one unit claimed an inventory, every claim is
2192
+ // `full` and every unit's output was readable; `readable: false` when the adapter keeps none.
2193
+ app.get(`${V}/runs/:id/inventory`, {
2194
+ config: {
2195
+ manifest: {
2196
+ responseType: '{ runId: string; readable: boolean; complete: boolean; units: { ord: number; unitId: string; claims: InventoryClaim[] }[]; unreadUnits: string[] }',
2197
+ statusCodes: [200, 404],
2198
+ },
2199
+ },
2200
+ }, async (req, reply) => {
2201
+ const { id } = req.params;
2202
+ const views = await adapter.sessionsDetail();
2203
+ const run = views.find((v) => v.session.id === id);
2204
+ if (!run)
2205
+ return reply.code(404).send({ error: 'Run not found' });
2206
+ if (typeof adapter.workOutput !== 'function') {
2207
+ return { runId: id, readable: false, complete: false, units: [], unreadUnits: [] };
2208
+ }
2209
+ const units = [];
2210
+ // A unit whose output could not be READ is not a unit that claimed nothing: it may hold the
2211
+ // partial claim, so it is listed and the run is never `complete` while any remain (codex r1).
2212
+ const unreadUnits = [];
2213
+ for (const u of [...run.units].sort((a, b) => a.ord - b.ord)) {
2214
+ let text;
2215
+ try {
2216
+ text = await adapter.workOutput(coreUnitId(id, u));
2217
+ }
2218
+ catch {
2219
+ unreadUnits.push(u.id);
2220
+ continue;
2221
+ }
2222
+ if (text === null) {
2223
+ // No output from a unit that has not run is expected; from one that finished, the
2224
+ // transcript is missing and may have held the claim (codex r3).
2225
+ if (u.status === 'done' || u.status === 'rejected')
2226
+ unreadUnits.push(u.id);
2227
+ continue;
2228
+ }
2229
+ const claims = parseInventoryClaims(text);
2230
+ if (claims.length > 0)
2231
+ units.push({ ord: u.ord, unitId: u.id, claims });
2232
+ }
2233
+ const all = units.flatMap((u) => u.claims);
2234
+ return {
2235
+ runId: id,
2236
+ readable: true,
2237
+ complete: inventoryComplete(all) && unreadUnits.length === 0,
2238
+ units,
2239
+ unreadUnits,
2240
+ };
2241
+ });
2140
2242
  // ── Deliver text (crew#524 / F-3R2-014) — the PR title + body a run's delivery carries ──
2141
2243
  // `gh pr create --fill` gave wicked-studio#249 a mid-word title and an EMPTY body. The deliver
2142
2244
  // script now asks this route for the text composed from the PERSISTED RUN RECORD: the intent,
@@ -2217,8 +2319,10 @@ runtime = {}) {
2217
2319
  // re-run on a delivered run, so a double-click (or a retry after a slow response) cannot
2218
2320
  // double-open. The index only ever holds real runs, so this needs no store round-trip.
2219
2321
  const existing = deliveryIndex.urlFor(id);
2220
- if (existing !== undefined)
2221
- return { prUrl: existing };
2322
+ if (existing !== undefined) {
2323
+ const recorded = deliveryIndex.assuranceFor(id);
2324
+ return { prUrl: existing, ...(recorded !== undefined ? { assurance: recorded } : {}) };
2325
+ }
2222
2326
  // N1: a run whose branch is already on a non-GitHub origin has nothing left to lift, and no
2223
2327
  // pull request can be opened from here — say so instead of pushing the branch again.
2224
2328
  const pushedAlready = deliveryIndex.pushedFor(id);
@@ -2252,6 +2356,12 @@ runtime = {}) {
2252
2356
  // back to `| null`.
2253
2357
  const repoRef = s.repo_ref;
2254
2358
  const initialWorkdir = s.workdir;
2359
+ // wicked-core#850 EX-03: a run whose contract requires QE acceptance is lifted only on a PASS
2360
+ // attributed to it — checked BEFORE anything is spawned, so a refusal pushes nothing.
2361
+ const qeAcceptance = await qeAcceptanceCheckFor(run);
2362
+ if (qeAcceptance !== null && !qeAcceptance.satisfied) {
2363
+ return reply.code(409).send(qeAcceptanceRefusal(id, qeAcceptance, aggregateReceipt(run, null), 'post_hoc'));
2364
+ }
2255
2365
  // One delivery per run at a time: the script pushes and opens a PR, so two concurrent
2256
2366
  // spawns could race gh into two PRs — the exact double-open idempotency forbids. The guard
2257
2367
  // wraps the reprovision too, so a reaped run's `wicked/<id>` branch is only ever checked out
@@ -2375,12 +2485,16 @@ runtime = {}) {
2375
2485
  const record = deliveryRecordFrom(result.output);
2376
2486
  const url = record !== null && 'url' in record ? record.url : null;
2377
2487
  const pushedOnly = record !== null && 'pushed' in record ? record.pushed : null;
2488
+ // wicked-core#850 EX-04: no engine lift re-verified what this pushed — the run record says so,
2489
+ // with the trees the script named (the PR body carries the same label). Built before the
2490
+ // outcome branches, so a push-only delivery records it too (codex r1).
2491
+ const assurance = postHocDeliveryAssurance(run, unverifiedTreesFrom(result.output), qeAcceptance);
2378
2492
  if (pushedOnly !== null) {
2379
2493
  // N1: the script pushed the branch and gh could resolve no GitHub repository for the
2380
2494
  // origin. That IS a delivery — record it, so the run reads `delivery: 'pushed'` — but there
2381
2495
  // 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);
2496
+ audit.record('run.delivered', actorOf(req), { runId: id, detail: { pushed: pushedOnly, via: 'post-hoc', assurance } });
2497
+ deliveryIndex.setPushed(id, pushedOnly, assurance);
2384
2498
  return reply.code(409).send({ error: pushedOnlyRefusal(id, pushedOnly) });
2385
2499
  }
2386
2500
  if (url === null) {
@@ -2392,9 +2506,9 @@ runtime = {}) {
2392
2506
  }
2393
2507
  // The durable record first, then the read-side index — the same write order as the
2394
2508
  // 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 };
2509
+ audit.record('run.delivered', actorOf(req), { runId: id, detail: { url, via: 'post-hoc', assurance } });
2510
+ deliveryIndex.set(id, url, assurance);
2511
+ return { prUrl: url, assurance };
2398
2512
  });
2399
2513
  // Durable pre-gate guidance (DES-UX-002 §7.2 — spec'd there as CREW-UX-4, implemented as
2400
2514
  // CREW-UX-7 because crew#308 already spent that id; see guidance-index.ts). Upserts the ONE
@@ -2517,6 +2631,7 @@ runtime = {}) {
2517
2631
  scratchBase: chatScopes.base,
2518
2632
  log: (m) => req.log.warn(m),
2519
2633
  ...(runtime.entityCount !== undefined ? { entityCount: runtime.entityCount } : {}),
2634
+ ...(runtime.freshenCheckout !== undefined ? { freshen: runtime.freshenCheckout } : {}),
2520
2635
  });
2521
2636
  if (!resolution.ok) {
2522
2637
  chatScopes.release(chatId, token);
@@ -3056,6 +3171,8 @@ runtime = {}) {
3056
3171
  adapter,
3057
3172
  settings: projectSettings,
3058
3173
  docRuns,
3174
+ // crew#896: the index carries the grounding the per-project list carries.
3175
+ ...(runtime.docGrounding !== undefined ? { groundingStore: runtime.docGrounding } : {}),
3059
3176
  includeRetired: flag === '1' || flag === 'true',
3060
3177
  });
3061
3178
  });
@@ -3262,12 +3379,22 @@ runtime = {}) {
3262
3379
  const run = views.find((v) => v.session.id === id);
3263
3380
  if (!run)
3264
3381
  return reply.code(404).send({ error: 'Run not found' });
3382
+ return acceptanceViewOf(run, { ...(qeRunId !== undefined && qeRunId !== '' ? { qeRunId } : {}) });
3383
+ });
3384
+ /**
3385
+ * One run's acceptance answer — the body `GET /runs/:id/acceptance` serves, and what a delivery
3386
+ * reads (wicked-core#850 EX-03). `forceDeclared`: the run's assurance CONTRACT requires QE
3387
+ * acceptance, so a verdict is owed even when no phase raises `verified_evidence` — missing denies.
3388
+ */
3389
+ async function acceptanceViewOf(run, opts) {
3390
+ const id = run.session.id;
3265
3391
  // What the run must prove, from what it CONTAINS (seam X2): a plan or preset run's steps whose
3266
3392
  // catalog entry re-verifies evidence, a registered workflow's def, nothing for free text — and
3267
3393
  // an unknown run (or one the daemon cannot read) is declared and denied with the reason.
3268
3394
  const verifiedCatalog = typeof adapter.verifiedEvidenceCatalog === 'function' ? await adapter.verifiedEvidenceCatalog() : null;
3269
3395
  const knownCatalog = typeof adapter.catalogIds === 'function' ? await adapter.catalogIds() : null;
3270
- const requirement = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog, knownCatalog);
3396
+ const read = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog, knownCatalog);
3397
+ const requirement = opts.forceDeclared === true ? { ...read, declared: true } : read;
3271
3398
  // WT-W2: every walkthrough step the requirement names is resolved from its proof root and seal.
3272
3399
  const walkthroughs = await walkthroughAcceptance(adapter, run, requirement.phases);
3273
3400
  let repo = null;
@@ -3279,7 +3406,7 @@ runtime = {}) {
3279
3406
  runId: id,
3280
3407
  repo,
3281
3408
  requirement,
3282
- ...(qeRunId !== undefined && qeRunId !== '' ? { qeRunId } : {}),
3409
+ ...(opts.qeRunId !== undefined ? { qeRunId: opts.qeRunId } : {}),
3283
3410
  // AW-14 (arch-R13a + R16): the conformance section reads the same wires the standalone
3284
3411
  // `/governance/claims` and `/runs/:id/events` routes serve, but run-scoped and resolved
3285
3412
  // deny-dominates BESIDE the QE gate — so a wiki-rule violation and an unenforced governed
@@ -3308,7 +3435,19 @@ runtime = {}) {
3308
3435
  })(),
3309
3436
  },
3310
3437
  });
3311
- });
3438
+ }
3439
+ /** EX-03: the run's QE acceptance check when its contract requires `qe_acceptance`; `null` when it
3440
+ * does not. A read that fails is a refusal naming why, never a pass. */
3441
+ async function qeAcceptanceCheckFor(run) {
3442
+ if (!requiresQeAcceptance(run))
3443
+ return null;
3444
+ try {
3445
+ return qeAcceptanceFromView(await acceptanceViewOf(run, { forceDeclared: true }));
3446
+ }
3447
+ catch (err) {
3448
+ return unreadableQeAcceptance(err);
3449
+ }
3450
+ }
3312
3451
  /**
3313
3452
  * The run's OPEN gate, resolved the way `GET /runs/:id/gate` serves it: the cache, then the
3314
3453
  * engine's durable `interaction_requests` row (adopted into the cache), then the event-log replay.
@@ -3343,6 +3482,35 @@ runtime = {}) {
3343
3482
  return 'no-log';
3344
3483
  return gateCache.rebuild(id, events) ?? null;
3345
3484
  }
3485
+ /** The unit an open gate holds: the named `ord`, else the open gate's, else the cursor's. */
3486
+ async function gatedUnitOf(run, ord) {
3487
+ let gateOrd = ord;
3488
+ if (gateOrd === undefined) {
3489
+ // A route set whose adapter cannot read the gate (a partial stub without `runEvents`) falls
3490
+ // back to the cursor unit, exactly as a log that records no open gate does.
3491
+ const open = await resolveOpenGate(run.session.id).catch(() => null);
3492
+ gateOrd = open !== null && open !== 'no-log' ? open.ord : undefined;
3493
+ }
3494
+ const units = run.units ?? [];
3495
+ return gateOrd !== undefined
3496
+ ? units.find((u) => u.ord === gateOrd)
3497
+ : [...units].sort((a, b) => a.ord - b.ord)[run.session.unit_ix];
3498
+ }
3499
+ /**
3500
+ * The checks an APPROVE that runs the deliver unit must pass — the ONE place for both gate paths
3501
+ * (`POST /runs/:id/gate`, standing orders, and a gated `POST /runs/:id/resume`): the delivery
3502
+ * freeze (idea 15), and wicked-core#850 EX-03 — a run whose contract requires QE acceptance
3503
+ * delivers only on an attributed PASS. A refusal leaves the gate open.
3504
+ */
3505
+ async function deliverApprovalRefusal(run, deliverUnit) {
3506
+ if (deliveryFreeze.state().frozen)
3507
+ return { refusal: { code: 409, body: frozenRefusal(deliveryFreeze.state()) } };
3508
+ const qeAcceptance = await qeAcceptanceCheckFor(run);
3509
+ if (qeAcceptance !== null && !qeAcceptance.satisfied) {
3510
+ return { refusal: { code: 409, body: qeAcceptanceRefusal(run.session.id, qeAcceptance, receiptOf(deliverUnit), 'gate') } };
3511
+ }
3512
+ return { qeAcceptance };
3513
+ }
3346
3514
  /**
3347
3515
  * THE gate decision path. `POST /runs/:id/gate` answers with it, and so does a standing order
3348
3516
  * (behaviour 10) — the same awaiting check, the same ord check and 409 `gate_changed` /
@@ -3383,22 +3551,17 @@ runtime = {}) {
3383
3551
  } };
3384
3552
  }
3385
3553
  }
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
- }
3554
+ // The gated unit — the named gate, else the open one, else the unit the run's cursor points at —
3555
+ // and its receipt as it stands when the person decides (wicked-core#850: every gate answer
3556
+ // carries it). An APPROVE that would run the deliver unit passes the delivery checks first.
3557
+ const gated = await gatedUnitOf(run, parsed.data.ord);
3558
+ const assurance = receiptOf(gated);
3559
+ let deliveryCheck;
3560
+ if (parsed.data.approve && gated !== undefined && isDeliverUnit(gated)) {
3561
+ const refused = await deliverApprovalRefusal(run, gated);
3562
+ if ('refusal' in refused)
3563
+ return refused.refusal;
3564
+ deliveryCheck = { qeAcceptance: refused.qeAcceptance };
3402
3565
  }
3403
3566
  // The steering-author landing (crew#388): decided — and the gate prompt captured — BEFORE
3404
3567
  // the confirm, because a terminal-phase approve prunes the gate cache and moves the run out
@@ -3452,7 +3615,15 @@ runtime = {}) {
3452
3615
  const landing = steeringPropose
3453
3616
  ? await landSteeringProposal({ adapter, audit, actor }, run, gatePrompt)
3454
3617
  : undefined;
3455
- return { code: 200, body: { status, ...(landing !== undefined ? { landing } : {}) } };
3618
+ return {
3619
+ code: 200,
3620
+ body: {
3621
+ status,
3622
+ ...(landing !== undefined ? { landing } : {}),
3623
+ assurance,
3624
+ ...(deliveryCheck !== undefined ? { delivery: deliveryCheck } : {}),
3625
+ },
3626
+ };
3456
3627
  }
3457
3628
  catch (err) {
3458
3629
  if (err instanceof PlanLaunchUnsupportedError) {
@@ -3466,7 +3637,7 @@ runtime = {}) {
3466
3637
  config: {
3467
3638
  manifest: {
3468
3639
  requestType: 'GateDecision',
3469
- responseType: '{ status: SessionStatus; landing?: SteeringLandingResult }',
3640
+ responseType: 'GateDecisionResult',
3470
3641
  // 409 twice over: a run not awaiting a human gate, and an engine refusal at confirm.
3471
3642
  // 501: an edited plan on an addon without the plan approval gate (DES-TEAMING-002 T3).
3472
3643
  statusCodes: [200, 400, 404, 409, 501],
@@ -3536,6 +3707,16 @@ runtime = {}) {
3536
3707
  // write too (crew#388). Prompt captured pre-confirm; see the gate route.
3537
3708
  const steeringPropose = gated && isSteeringAuthorRun(run);
3538
3709
  const gatePrompt = steeringPropose ? gateCache.get(id)?.prompt : undefined;
3710
+ // A gated resume IS an approve, so it passes the same delivery checks (freeze, EX-03) — no side
3711
+ // door around the deliver gate — and answers with the gated unit's receipt.
3712
+ const gatedUnit = gated ? await gatedUnitOf(run, undefined) : undefined;
3713
+ let deliveryCheck;
3714
+ if (gatedUnit !== undefined && isDeliverUnit(gatedUnit)) {
3715
+ const refused = await deliverApprovalRefusal(run, gatedUnit);
3716
+ if ('refusal' in refused)
3717
+ return reply.code(refused.refusal.code).send(refused.refusal.body);
3718
+ deliveryCheck = { qeAcceptance: refused.qeAcceptance };
3719
+ }
3539
3720
  const status = gated ? await adapter.confirmGate(id, true) : await adapter.resumeRun(id);
3540
3721
  // A resume of a gated run IS a gate approval — audit it as one, so the
3541
3722
  // "who approved" trail has no side door (task #88).
@@ -3546,7 +3727,12 @@ runtime = {}) {
3546
3727
  const landing = steeringPropose
3547
3728
  ? await landSteeringProposal({ adapter, audit, actor: actorOf(req) }, run, gatePrompt)
3548
3729
  : undefined;
3549
- return reply.send({ status, ...(landing !== undefined ? { landing } : {}) });
3730
+ return reply.send({
3731
+ status,
3732
+ ...(landing !== undefined ? { landing } : {}),
3733
+ ...(gated ? { assurance: receiptOf(gatedUnit) } : {}),
3734
+ ...(deliveryCheck !== undefined ? { delivery: deliveryCheck } : {}),
3735
+ });
3550
3736
  }
3551
3737
  catch (err) {
3552
3738
  return reply.code(409).send({ error: message(err) });
@@ -3640,6 +3826,16 @@ runtime = {}) {
3640
3826
  });
3641
3827
  }
3642
3828
  }
3829
+ // A gated reassign APPROVES the gate first, so it passes the same delivery checks (freeze,
3830
+ // wicked-core#850 EX-03) when the gate holds the deliver unit (codex r1 on the EX-03 PR).
3831
+ if (gated) {
3832
+ const gatedUnit = await gatedUnitOf(run, undefined);
3833
+ if (gatedUnit !== undefined && isDeliverUnit(gatedUnit)) {
3834
+ const refused = await deliverApprovalRefusal(run, gatedUnit);
3835
+ if ('refusal' in refused)
3836
+ return reply.code(refused.refusal.code).send(refused.refusal.body);
3837
+ }
3838
+ }
3643
3839
  const cursor = resolveCursorUnit(run);
3644
3840
  const ord = cursor?.ord ?? run.session.unit_ix;
3645
3841
  try {
@@ -3674,9 +3870,14 @@ runtime = {}) {
3674
3870
  // read it. A restart is routine: deploy, crash, laptop sleep.
3675
3871
  app.get(`${V}/runs/:id/gate`, async (req, reply) => {
3676
3872
  const { id } = req.params;
3873
+ // wicked-core#850: the gate answer carries the gated unit's receipt — what assured the work it asks about.
3874
+ const receiptAt = async (ord) => {
3875
+ const run = (await adapter.sessionsDetail()).find((v) => v.session.id === id);
3876
+ return receiptOf(run?.units?.find((u) => u.ord === ord));
3877
+ };
3677
3878
  const cached = gateCache.get(id);
3678
3879
  if (cached)
3679
- return { runId: id, ...cached };
3880
+ return { runId: id, ...cached, assurance: await receiptAt(cached.ord) };
3680
3881
  // A miss is not evidence of anything on its own, so ask the run what it is doing before paying
3681
3882
  // to replay it. Only `awaiting_human` can have an open gate, and that answer is definitive:
3682
3883
  // every other status is a 404 that needs no log at all — which is also the overwhelmingly
@@ -3712,7 +3913,7 @@ runtime = {}) {
3712
3913
  // genuinely lost), as does a gate whose `awaitingHuman` predates the log's retention.
3713
3914
  return reply.code(404).send({ error: 'No open gate for this run' });
3714
3915
  }
3715
- return { runId: id, ...open };
3916
+ return { runId: id, ...open, assurance: receiptOf(run.units?.find((u) => u.ord === open.ord)) };
3716
3917
  });
3717
3918
  // ── Elicitation (DES-002) ────────────────────────────────────────────────────
3718
3919
  //
@@ -5513,6 +5714,10 @@ runtime = {}) {
5513
5714
  });
5514
5715
  // ── Projects (DES-PROJECT-001) — the 9-route experience-plane surface ────────
5515
5716
  registerProjectRoutes(app, adapter, { ...projects, settings: projectSettings }, security);
5717
+ // crew#371: a project's requirements / domain / coverage, folded over its repos' own reads.
5718
+ registerProjectAggregateRoutes(app, adapter);
5719
+ // crew#372: draft a product plan from selected requirements, as a governed run via POST /runs.
5720
+ registerProductRoutes(app, adapter);
5516
5721
  // ── Capture (Studio OS behaviour 8) — notes and photos → a project-filed run whose output is
5517
5722
  // proposals in the queue above; the launch is POST /runs itself (api/capture.ts).
5518
5723
  registerCaptureRoutes(app);