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.
- package/README.md +11 -8
- package/dist/api/auth.d.ts +6 -5
- package/dist/api/auth.d.ts.map +1 -1
- package/dist/api/auth.js +7 -5
- package/dist/api/auth.js.map +1 -1
- package/dist/api/capture.d.ts +4 -0
- package/dist/api/capture.d.ts.map +1 -1
- package/dist/api/capture.js +6 -1
- package/dist/api/capture.js.map +1 -1
- package/dist/api/chat-freshness.d.ts +37 -0
- package/dist/api/chat-freshness.d.ts.map +1 -0
- package/dist/api/chat-freshness.js +130 -0
- package/dist/api/chat-freshness.js.map +1 -0
- package/dist/api/chat-scope.d.ts +15 -0
- package/dist/api/chat-scope.d.ts.map +1 -1
- package/dist/api/chat-scope.js +55 -1
- package/dist/api/chat-scope.js.map +1 -1
- package/dist/api/delivery-assurance.d.ts +48 -0
- package/dist/api/delivery-assurance.d.ts.map +1 -0
- package/dist/api/delivery-assurance.js +118 -0
- package/dist/api/delivery-assurance.js.map +1 -0
- package/dist/api/delivery-index.d.ts +11 -5
- package/dist/api/delivery-index.d.ts.map +1 -1
- package/dist/api/delivery-index.js +38 -7
- package/dist/api/delivery-index.js.map +1 -1
- package/dist/api/product.d.ts +67 -0
- package/dist/api/product.d.ts.map +1 -0
- package/dist/api/product.js +169 -0
- package/dist/api/product.js.map +1 -0
- package/dist/api/project-aggregates.d.ts +61 -0
- package/dist/api/project-aggregates.d.ts.map +1 -0
- package/dist/api/project-aggregates.js +296 -0
- package/dist/api/project-aggregates.js.map +1 -0
- package/dist/api/recording.d.ts +4 -0
- package/dist/api/recording.d.ts.map +1 -1
- package/dist/api/recording.js +6 -1
- package/dist/api/recording.js.map +1 -1
- package/dist/api/routes.d.ts +43 -17
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +245 -40
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-files.d.ts.map +1 -1
- package/dist/api/run-files.js +11 -2
- package/dist/api/run-files.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +3 -0
- package/dist/api/server.js.map +1 -1
- package/dist/api/stall-watchdog.d.ts.map +1 -1
- package/dist/api/stall-watchdog.js +6 -1
- package/dist/api/stall-watchdog.js.map +1 -1
- package/dist/api/steering-landing.d.ts +1 -1
- package/dist/api/steering-landing.d.ts.map +1 -1
- package/dist/api/steering-landing.js +65 -29
- package/dist/api/steering-landing.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +22 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +3 -0
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +23 -2
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/assurance.d.ts +60 -0
- package/dist/core/assurance.d.ts.map +1 -0
- package/dist/core/assurance.js +88 -0
- package/dist/core/assurance.js.map +1 -0
- package/dist/core/deliver-exclusions.d.ts +9 -1
- package/dist/core/deliver-exclusions.d.ts.map +1 -1
- package/dist/core/deliver-exclusions.js +37 -2
- package/dist/core/deliver-exclusions.js.map +1 -1
- package/dist/core/deliver.d.ts +25 -1
- package/dist/core/deliver.d.ts.map +1 -1
- package/dist/core/deliver.js +77 -9
- package/dist/core/deliver.js.map +1 -1
- package/dist/core/deliverable-floor.d.ts.map +1 -1
- package/dist/core/deliverable-floor.js +6 -1
- package/dist/core/deliverable-floor.js.map +1 -1
- package/dist/core/inventory-claim.d.ts +34 -0
- package/dist/core/inventory-claim.d.ts.map +1 -0
- package/dist/core/inventory-claim.js +138 -0
- package/dist/core/inventory-claim.js.map +1 -0
- package/dist/core/types.d.ts +7 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/editors/registry.d.ts +3 -3
- package/dist/interactive/bridge-pool.d.ts +8 -2
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +7 -1
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/docs-index.d.ts +8 -0
- package/dist/interactive/docs-index.d.ts.map +1 -1
- package/dist/interactive/docs-index.js +5 -2
- package/dist/interactive/docs-index.js.map +1 -1
- package/dist/qe/acceptance.d.ts.map +1 -1
- package/dist/qe/acceptance.js +49 -1
- package/dist/qe/acceptance.js.map +1 -1
- package/dist/standing-orders/history.d.ts +8 -0
- package/dist/standing-orders/history.d.ts.map +1 -1
- package/dist/standing-orders/history.js +8 -1
- package/dist/standing-orders/history.js.map +1 -1
- package/dist/studio/assets/index--O1AdvNx.css +32 -0
- package/dist/studio/assets/index-B1MYiNq-.js +567 -0
- package/dist/studio/editors/wicked-page/editor.json +1 -1
- package/dist/studio/editors/wicked-page/index.html +1 -1
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +764 -79
- package/endpoint-manifest.json +63 -2
- package/package.json +8 -8
- package/skills/wicked-crew/SKILL.md +2 -1
- package/dist/studio/assets/index-DofYURLG.js +0 -564
- package/dist/studio/assets/index-fSSul5Hu.css +0 -32
package/dist/api/routes.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
3387
|
-
//
|
|
3388
|
-
//
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
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 {
|
|
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: '
|
|
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({
|
|
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);
|