wicked-crew 0.7.40 → 0.7.45
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/dist/api/audit.d.ts +3 -0
- package/dist/api/audit.d.ts.map +1 -1
- package/dist/api/audit.js +2 -0
- package/dist/api/audit.js.map +1 -1
- package/dist/api/capture.d.ts +149 -0
- package/dist/api/capture.d.ts.map +1 -0
- package/dist/api/capture.js +330 -0
- package/dist/api/capture.js.map +1 -0
- package/dist/api/chat-citations.d.ts +174 -0
- package/dist/api/chat-citations.d.ts.map +1 -0
- package/dist/api/chat-citations.js +528 -0
- package/dist/api/chat-citations.js.map +1 -0
- package/dist/api/chat-transcripts.d.ts +20 -1
- package/dist/api/chat-transcripts.d.ts.map +1 -1
- package/dist/api/chat-transcripts.js +29 -0
- package/dist/api/chat-transcripts.js.map +1 -1
- package/dist/api/delivery-cache.d.ts +2 -2
- package/dist/api/delivery-freeze.d.ts +66 -0
- package/dist/api/delivery-freeze.d.ts.map +1 -0
- package/dist/api/delivery-freeze.js +104 -0
- package/dist/api/delivery-freeze.js.map +1 -0
- package/dist/api/delivery-index.d.ts +53 -9
- package/dist/api/delivery-index.d.ts.map +1 -1
- package/dist/api/delivery-index.js +115 -22
- package/dist/api/delivery-index.js.map +1 -1
- package/dist/api/demo.d.ts +90 -0
- package/dist/api/demo.d.ts.map +1 -0
- package/dist/api/demo.js +492 -0
- package/dist/api/demo.js.map +1 -0
- package/dist/api/diagnostics.d.ts +64 -1
- package/dist/api/diagnostics.d.ts.map +1 -1
- package/dist/api/diagnostics.js +129 -1
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/api/eval-sample.d.ts +6 -6
- package/dist/api/governance-steering.d.ts +2 -2
- package/dist/api/mcp.d.ts +282 -0
- package/dist/api/mcp.d.ts.map +1 -0
- package/dist/api/mcp.js +521 -0
- package/dist/api/mcp.js.map +1 -0
- package/dist/api/plan-schema.d.ts +56 -0
- package/dist/api/plan-schema.d.ts.map +1 -0
- package/dist/api/plan-schema.js +26 -0
- package/dist/api/plan-schema.js.map +1 -0
- package/dist/api/roster-standing.d.ts +15 -2
- package/dist/api/roster-standing.d.ts.map +1 -1
- package/dist/api/roster-standing.js +55 -3
- package/dist/api/roster-standing.js.map +1 -1
- package/dist/api/routes.d.ts +630 -51
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +738 -127
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-files.d.ts.map +1 -1
- package/dist/api/run-files.js +31 -2
- package/dist/api/run-files.js.map +1 -1
- package/dist/api/run-liveness.d.ts +41 -0
- package/dist/api/run-liveness.d.ts.map +1 -0
- package/dist/api/run-liveness.js +166 -0
- package/dist/api/run-liveness.js.map +1 -0
- package/dist/api/seat-health.d.ts.map +1 -1
- package/dist/api/seat-health.js +20 -4
- package/dist/api/seat-health.js.map +1 -1
- package/dist/api/seat-probe.d.ts +141 -0
- package/dist/api/seat-probe.d.ts.map +1 -0
- package/dist/api/seat-probe.js +257 -0
- package/dist/api/seat-probe.js.map +1 -0
- package/dist/api/seat-record.d.ts +40 -0
- package/dist/api/seat-record.d.ts.map +1 -0
- package/dist/api/seat-record.js +218 -0
- package/dist/api/seat-record.js.map +1 -0
- package/dist/api/seat-signin.d.ts.map +1 -1
- package/dist/api/seat-signin.js +3 -1
- package/dist/api/seat-signin.js.map +1 -1
- package/dist/api/seat-standing.d.ts +24 -4
- package/dist/api/seat-standing.d.ts.map +1 -1
- package/dist/api/seat-standing.js +50 -3
- package/dist/api/seat-standing.js.map +1 -1
- package/dist/api/server.d.ts +34 -48
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +283 -107
- package/dist/api/server.js.map +1 -1
- package/dist/api/skills.d.ts +3 -1
- package/dist/api/skills.d.ts.map +1 -1
- package/dist/api/skills.js +7 -2
- package/dist/api/skills.js.map +1 -1
- package/dist/api/stall-watchdog.d.ts +34 -3
- package/dist/api/stall-watchdog.d.ts.map +1 -1
- package/dist/api/stall-watchdog.js +107 -8
- package/dist/api/stall-watchdog.js.map +1 -1
- package/dist/api/steering-landing.d.ts +6 -6
- package/dist/api/steering-landing.d.ts.map +1 -1
- package/dist/api/steering-landing.js +7 -9
- package/dist/api/steering-landing.js.map +1 -1
- package/dist/api/testing.d.ts +6 -4
- package/dist/api/testing.d.ts.map +1 -1
- package/dist/api/testing.js +82 -47
- package/dist/api/testing.js.map +1 -1
- package/dist/campaign/boot-resume.d.ts +34 -0
- package/dist/campaign/boot-resume.d.ts.map +1 -0
- package/dist/campaign/boot-resume.js +45 -0
- package/dist/campaign/boot-resume.js.map +1 -0
- package/dist/campaigns/rollup.d.ts +4 -1
- package/dist/campaigns/rollup.d.ts.map +1 -1
- package/dist/campaigns/rollup.js +6 -1
- package/dist/campaigns/rollup.js.map +1 -1
- package/dist/campaigns/routes.d.ts +2 -2
- package/dist/cli/bearer.d.ts +27 -1
- package/dist/cli/bearer.d.ts.map +1 -1
- package/dist/cli/bearer.js +62 -2
- package/dist/cli/bearer.js.map +1 -1
- package/dist/cli/governance.d.ts +11 -0
- package/dist/cli/governance.d.ts.map +1 -1
- package/dist/cli/governance.js +8 -2
- package/dist/cli/governance.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +140 -75
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.d.ts +11 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +40 -9
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/service.d.ts +78 -0
- package/dist/cli/service.d.ts.map +1 -0
- package/dist/cli/service.js +334 -0
- package/dist/cli/service.js.map +1 -0
- package/dist/core/adapter.d.ts +238 -12
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +619 -157
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-reaper.d.ts +4 -1
- package/dist/core/bridge-reaper.d.ts.map +1 -1
- package/dist/core/bridge-reaper.js +15 -1
- package/dist/core/bridge-reaper.js.map +1 -1
- package/dist/core/bus-notice.d.ts +27 -0
- package/dist/core/bus-notice.d.ts.map +1 -0
- package/dist/core/bus-notice.js +24 -0
- package/dist/core/bus-notice.js.map +1 -0
- package/dist/core/bus.d.ts +99 -0
- package/dist/core/bus.d.ts.map +1 -0
- package/dist/core/bus.js +176 -0
- package/dist/core/bus.js.map +1 -0
- package/dist/core/deliver-exclusions.d.ts +71 -0
- package/dist/core/deliver-exclusions.d.ts.map +1 -0
- package/dist/core/deliver-exclusions.js +110 -0
- package/dist/core/deliver-exclusions.js.map +1 -0
- package/dist/core/deliver-text.d.ts +61 -14
- package/dist/core/deliver-text.d.ts.map +1 -1
- package/dist/core/deliver-text.js +178 -30
- package/dist/core/deliver-text.js.map +1 -1
- package/dist/core/deliver-triage.d.ts +11 -2
- package/dist/core/deliver-triage.d.ts.map +1 -1
- package/dist/core/deliver-triage.js +19 -1
- package/dist/core/deliver-triage.js.map +1 -1
- package/dist/core/deliver.d.ts +161 -3
- package/dist/core/deliver.d.ts.map +1 -1
- package/dist/core/deliver.js +463 -25
- package/dist/core/deliver.js.map +1 -1
- package/dist/core/deliverable-floor.d.ts.map +1 -1
- package/dist/core/deliverable-floor.js +5 -7
- package/dist/core/deliverable-floor.js.map +1 -1
- package/dist/core/engine-bus.d.ts +40 -0
- package/dist/core/engine-bus.d.ts.map +1 -0
- package/dist/core/engine-bus.js +27 -0
- package/dist/core/engine-bus.js.map +1 -0
- package/dist/core/estate-mcp-client.d.ts +87 -5
- package/dist/core/estate-mcp-client.d.ts.map +1 -1
- package/dist/core/estate-mcp-client.js +90 -11
- package/dist/core/estate-mcp-client.js.map +1 -1
- package/dist/core/linked-issues.d.ts +46 -0
- package/dist/core/linked-issues.d.ts.map +1 -0
- package/dist/core/linked-issues.js +123 -0
- package/dist/core/linked-issues.js.map +1 -0
- package/dist/core/run-identity.d.ts +100 -0
- package/dist/core/run-identity.d.ts.map +1 -0
- package/dist/core/run-identity.js +178 -0
- package/dist/core/run-identity.js.map +1 -0
- package/dist/core/shutdown.d.ts +16 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +39 -0
- package/dist/core/shutdown.js.map +1 -0
- package/dist/core/types.d.ts +15 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +4 -0
- package/dist/core/types.js.map +1 -1
- package/dist/interactive/bridge-pool.d.ts +67 -3
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +129 -13
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/bridge-root.d.ts +9 -0
- package/dist/interactive/bridge-root.d.ts.map +1 -1
- package/dist/interactive/bridge-root.js +11 -4
- package/dist/interactive/bridge-root.js.map +1 -1
- package/dist/interactive/chat-events.d.ts +14 -9
- package/dist/interactive/chat-events.d.ts.map +1 -1
- package/dist/interactive/chat-events.js +55 -57
- package/dist/interactive/chat-events.js.map +1 -1
- package/dist/interactive/council-outcome.d.ts +1 -0
- package/dist/interactive/council-outcome.d.ts.map +1 -1
- package/dist/interactive/council-outcome.js +33 -0
- package/dist/interactive/council-outcome.js.map +1 -1
- package/dist/interactive/draft-events.d.ts +36 -17
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +121 -92
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/draft-skill.d.ts +59 -0
- package/dist/interactive/draft-skill.d.ts.map +1 -1
- package/dist/interactive/draft-skill.js +233 -0
- package/dist/interactive/draft-skill.js.map +1 -1
- package/dist/interactive/edit-events.d.ts +1 -14
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +37 -70
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/interactive/proxy-routes.d.ts +2 -2
- package/dist/interactive/proxy-routes.d.ts.map +1 -1
- package/dist/interactive/proxy-routes.js +4 -13
- package/dist/interactive/proxy-routes.js.map +1 -1
- package/dist/interactive/ws-relay.d.ts +9 -10
- package/dist/interactive/ws-relay.d.ts.map +1 -1
- package/dist/interactive/ws-relay.js +23 -50
- package/dist/interactive/ws-relay.js.map +1 -1
- package/dist/mcp/broker.d.ts +88 -0
- package/dist/mcp/broker.d.ts.map +1 -0
- package/dist/mcp/broker.js +307 -0
- package/dist/mcp/broker.js.map +1 -0
- package/dist/mcp/call-records.d.ts +61 -0
- package/dist/mcp/call-records.d.ts.map +1 -0
- package/dist/mcp/call-records.js +135 -0
- package/dist/mcp/call-records.js.map +1 -0
- package/dist/mcp/classify.d.ts +45 -0
- package/dist/mcp/classify.d.ts.map +1 -0
- package/dist/mcp/classify.js +83 -0
- package/dist/mcp/classify.js.map +1 -0
- package/dist/mcp/discovery.d.ts +24 -0
- package/dist/mcp/discovery.d.ts.map +1 -0
- package/dist/mcp/discovery.js +78 -0
- package/dist/mcp/discovery.js.map +1 -0
- package/dist/mcp/invoke.d.ts +36 -0
- package/dist/mcp/invoke.d.ts.map +1 -0
- package/dist/mcp/invoke.js +80 -0
- package/dist/mcp/invoke.js.map +1 -0
- package/dist/mcp/policies.d.ts +110 -0
- package/dist/mcp/policies.d.ts.map +1 -0
- package/dist/mcp/policies.js +400 -0
- package/dist/mcp/policies.js.map +1 -0
- package/dist/mcp/probe.d.ts +66 -0
- package/dist/mcp/probe.d.ts.map +1 -0
- package/dist/mcp/probe.js +116 -0
- package/dist/mcp/probe.js.map +1 -0
- package/dist/mcp/redact.d.ts +34 -0
- package/dist/mcp/redact.d.ts.map +1 -0
- package/dist/mcp/redact.js +84 -0
- package/dist/mcp/redact.js.map +1 -0
- package/dist/mcp/registry-store.d.ts +69 -0
- package/dist/mcp/registry-store.d.ts.map +1 -0
- package/dist/mcp/registry-store.js +82 -0
- package/dist/mcp/registry-store.js.map +1 -0
- package/dist/mcp/registry.d.ts +147 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/registry.js +509 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/rest.d.ts +111 -0
- package/dist/mcp/rest.d.ts.map +1 -0
- package/dist/mcp/rest.js +606 -0
- package/dist/mcp/rest.js.map +1 -0
- package/dist/mcp/secrets.d.ts +66 -0
- package/dist/mcp/secrets.d.ts.map +1 -0
- package/dist/mcp/secrets.js +180 -0
- package/dist/mcp/secrets.js.map +1 -0
- package/dist/mcp/tool-list.d.ts +25 -0
- package/dist/mcp/tool-list.d.ts.map +1 -0
- package/dist/mcp/tool-list.js +62 -0
- package/dist/mcp/tool-list.js.map +1 -0
- package/dist/mcp/upstream-error.d.ts +12 -0
- package/dist/mcp/upstream-error.d.ts.map +1 -0
- package/dist/mcp/upstream-error.js +14 -0
- package/dist/mcp/upstream-error.js.map +1 -0
- package/dist/mcp/usage.d.ts +31 -0
- package/dist/mcp/usage.d.ts.map +1 -0
- package/dist/mcp/usage.js +159 -0
- package/dist/mcp/usage.js.map +1 -0
- package/dist/presets/routes.d.ts +53 -0
- package/dist/presets/routes.d.ts.map +1 -0
- package/dist/presets/routes.js +115 -0
- package/dist/presets/routes.js.map +1 -0
- package/dist/projects/activity.d.ts +6 -18
- package/dist/projects/activity.d.ts.map +1 -1
- package/dist/projects/activity.js +17 -60
- package/dist/projects/activity.js.map +1 -1
- package/dist/projects/events.d.ts +6 -5
- package/dist/projects/events.d.ts.map +1 -1
- package/dist/projects/events.js +33 -58
- package/dist/projects/events.js.map +1 -1
- package/dist/projects/routes.d.ts.map +1 -1
- package/dist/projects/routes.js +19 -0
- package/dist/projects/routes.js.map +1 -1
- package/dist/projects/state-home-preflight.d.ts.map +1 -1
- package/dist/projects/state-home-preflight.js +7 -0
- package/dist/projects/state-home-preflight.js.map +1 -1
- package/dist/projects/state-home-registry.d.ts.map +1 -1
- package/dist/projects/state-home-registry.js +9 -1
- package/dist/projects/state-home-registry.js.map +1 -1
- package/dist/qe/acceptance.d.ts +28 -28
- package/dist/qe/acceptance.d.ts.map +1 -1
- package/dist/qe/acceptance.js +60 -61
- package/dist/qe/acceptance.js.map +1 -1
- package/dist/qe/test-sets.d.ts +3 -5
- package/dist/qe/test-sets.d.ts.map +1 -1
- package/dist/qe/test-sets.js +5 -7
- package/dist/qe/test-sets.js.map +1 -1
- package/dist/skills/installed.d.ts +51 -0
- package/dist/skills/installed.d.ts.map +1 -0
- package/dist/skills/installed.js +46 -0
- package/dist/skills/installed.js.map +1 -0
- package/dist/standing-orders/evaluator.d.ts +113 -0
- package/dist/standing-orders/evaluator.d.ts.map +1 -0
- package/dist/standing-orders/evaluator.js +225 -0
- package/dist/standing-orders/evaluator.js.map +1 -0
- package/dist/standing-orders/history.d.ts +58 -0
- package/dist/standing-orders/history.d.ts.map +1 -0
- package/dist/standing-orders/history.js +117 -0
- package/dist/standing-orders/history.js.map +1 -0
- package/dist/standing-orders/parse.d.ts +19 -0
- package/dist/standing-orders/parse.d.ts.map +1 -0
- package/dist/standing-orders/parse.js +75 -0
- package/dist/standing-orders/parse.js.map +1 -0
- package/dist/standing-orders/routes.d.ts +180 -0
- package/dist/standing-orders/routes.d.ts.map +1 -0
- package/dist/standing-orders/routes.js +95 -0
- package/dist/standing-orders/routes.js.map +1 -0
- package/dist/standing-orders/store.d.ts +154 -0
- package/dist/standing-orders/store.d.ts.map +1 -0
- package/dist/standing-orders/store.js +189 -0
- package/dist/standing-orders/store.js.map +1 -0
- package/dist/studio/assets/index-CvV5qITf.css +32 -0
- package/dist/studio/assets/index-jCOy6Ns7.js +568 -0
- package/dist/studio/index.html +3 -3
- package/dist/studio/testid-inventory.json +3274 -894
- package/dist/team/key.d.ts +5 -0
- package/dist/team/key.d.ts.map +1 -0
- package/dist/team/key.js +21 -0
- package/dist/team/key.js.map +1 -0
- package/dist/team/routes.d.ts +196 -0
- package/dist/team/routes.d.ts.map +1 -0
- package/dist/team/routes.js +274 -0
- package/dist/team/routes.js.map +1 -0
- package/dist/team/ws-relay.d.ts +39 -0
- package/dist/team/ws-relay.d.ts.map +1 -0
- package/dist/team/ws-relay.js +63 -0
- package/dist/team/ws-relay.js.map +1 -0
- package/endpoint-manifest.json +502 -6
- package/package.json +6 -6
- package/dist/interactive/demo-events.d.ts +0 -367
- package/dist/interactive/demo-events.d.ts.map +0 -1
- package/dist/interactive/demo-events.js +0 -1293
- package/dist/interactive/demo-events.js.map +0 -1
- package/dist/qe/gate-events.d.ts +0 -113
- package/dist/qe/gate-events.d.ts.map +0 -1
- package/dist/qe/gate-events.js +0 -172
- package/dist/qe/gate-events.js.map +0 -1
- package/dist/studio/assets/index-Bg7Z3BR-.css +0 -32
- package/dist/studio/assets/index-BvOFMjf9.js +0 -554
package/dist/api/routes.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { busSeamsOffWarning, busUnavailableWarning } from '../core/bus-notice.js';
|
|
2
|
+
import { PlanSchema, toLaunchPlan } from './plan-schema.js';
|
|
1
3
|
import { z } from 'zod';
|
|
2
4
|
import { listRequirements, getRequirement, patchRequirement } from './requirements.js';
|
|
3
5
|
import { randomUUID } from 'node:crypto';
|
|
@@ -5,12 +7,12 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
5
7
|
import { promises as fsp } from 'node:fs';
|
|
6
8
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
7
9
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
|
|
10
|
+
import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, GovernanceReplayUnsupportedError, PlanLaunchUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
|
|
9
11
|
import { codeGraphDb, codeGraphErrorStatus, requirementsGraph } from '../core/repoPaths.js';
|
|
10
12
|
import { resolveCursorUnit } from '../core/cursor.js';
|
|
11
13
|
import { detectRefusal } from './gate-cache.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
+
import { acceptanceRequirementOf, buildAcceptanceView } from '../qe/acceptance.js';
|
|
15
|
+
import { runIdentityOf, wireIdentity } from '../core/run-identity.js';
|
|
14
16
|
import { buildEvidenceBundle, coreUnitId, evidenceFilename } from './evidence.js';
|
|
15
17
|
import { outputUnavailableReason, resolveUnit, unitKeysFor } from './unit-output.js';
|
|
16
18
|
import { execCapped, ExecOutputTooLarge } from '../core/exec.js';
|
|
@@ -28,12 +30,18 @@ import { listInteractiveDocs } from '../interactive/docs-index.js';
|
|
|
28
30
|
import { TestSetIndex } from '../qe/test-sets.js';
|
|
29
31
|
import { estateExe, parseEstateTotals, resolveProjectGraphBinding } from '../projects/graph.js';
|
|
30
32
|
import { registerProjectRoutes } from '../projects/routes.js';
|
|
33
|
+
import { registerPresetRoutes } from '../presets/routes.js';
|
|
34
|
+
import { registerTeamRoutes } from '../team/routes.js';
|
|
31
35
|
import { registerCampaignRoutes } from '../campaigns/routes.js';
|
|
32
36
|
import { registerGovernanceWikiRoutes } from './governance-wiki.js';
|
|
33
37
|
import { DEFAULT_STEERING_TYPE, STEERING_TYPE_VALUES, STEERING_TYPES, registerGovernanceSteeringRoutes, } from './governance-steering.js';
|
|
34
38
|
import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js';
|
|
39
|
+
import { ApproveEditSchema, approveEdited, captureLaunchRoots, editInFlight, FileProposalSchema, registerCaptureRoutes } from './capture.js';
|
|
35
40
|
import { registerTestingRoutes } from './testing.js';
|
|
41
|
+
import { DEMO_PRESET, demoLaunchRoots, registerDemoRoutes } from './demo.js';
|
|
36
42
|
import { registerSkillsRoutes } from './skills.js';
|
|
43
|
+
import { registerMcpRoutes } from './mcp.js';
|
|
44
|
+
import { McpPolicies } from '../mcp/policies.js';
|
|
37
45
|
import { disabledSkillsHealth } from '../skills/runtime.js';
|
|
38
46
|
import { phaseSkillFindings, withPhaseSkillGaps } from '../skills/phase-skill-gaps.js';
|
|
39
47
|
import { STATE_HOME_BLOCKER_CODE, STATE_HOME_REMEDY, isEngineStateHomeRefusal, stateHomeBlockerBody, } from '../projects/state-home-preflight.js';
|
|
@@ -41,22 +49,27 @@ import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../s
|
|
|
41
49
|
import { noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
|
|
42
50
|
import { ProjectSettingsStore } from '../projects/settings.js';
|
|
43
51
|
import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
|
|
44
|
-
import { composeDeliverText, factsFromRun, framedDeliverText, runUrlFor } from '../core/deliver-text.js';
|
|
45
|
-
import { resolvePullRequest as resolvePullRequestViaGh } from '../core/deliver.js';
|
|
52
|
+
import { composeDeliverText, configuredPublicOrigin, extractFollowUps, factsFromRun, FOLLOW_UPS_MAX, framedDeliverText, runUrlFor, } from '../core/deliver-text.js';
|
|
53
|
+
import { deliverRepoFor, deliverTargetView, isGitHubLogin, readDeliverOriginUrl, resolvePullRequest as resolvePullRequestViaGh, } from '../core/deliver.js';
|
|
46
54
|
import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
|
|
47
55
|
import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
|
|
48
56
|
import { registerInteractiveDocList } from '../interactive/doc-list-routes.js';
|
|
49
57
|
import { MembershipIndex } from '../projects/membership-index.js';
|
|
50
58
|
import { MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
|
|
59
|
+
import { resolveLinkedIssues } from '../core/linked-issues.js';
|
|
51
60
|
import { AuditLog } from './audit.js';
|
|
52
61
|
import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
|
|
53
62
|
import { GovernanceDiagnostics } from './governance-health.js';
|
|
54
63
|
import { legacyHomeOutboxPath } from '../core/governance-store.js';
|
|
64
|
+
import { UsageError, replayOutboxInto } from '../cli/governance.js';
|
|
65
|
+
import { foldDeadletters } from './governance-health.js';
|
|
55
66
|
import { RetryIndex } from './retry-index.js';
|
|
67
|
+
import { SEAT_RECORD_DEFAULT_DAYS, SEAT_RECORD_MAX_DAYS, seatRecord } from './seat-record.js';
|
|
68
|
+
import { DeliveryFreeze, frozenRefusal, isDeliverUnit, registerDeliveryFreezeRoutes } from './delivery-freeze.js';
|
|
56
69
|
import { GroupIndex } from './group-index.js';
|
|
57
70
|
import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
|
|
58
71
|
import { GuidanceIndex } from './guidance-index.js';
|
|
59
|
-
import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded,
|
|
72
|
+
import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, deliveryRecordFrom, pushedState, canDeliverResolver, } from './delivery-index.js';
|
|
60
73
|
import { DeliveryDerivationCache } from './delivery-cache.js';
|
|
61
74
|
import { gitReprovisionWorktree, runDeliverScript, } from './post-hoc-deliver.js';
|
|
62
75
|
import { LOCAL_ACTOR } from './auth.js';
|
|
@@ -65,6 +78,10 @@ import { LOCAL_ACTOR } from './auth.js';
|
|
|
65
78
|
export { API_PREFIX } from './api-prefix.js';
|
|
66
79
|
import { API_PREFIX } from './api-prefix.js';
|
|
67
80
|
const V = API_PREFIX;
|
|
81
|
+
/** How long `GET /health` waits on the engine's actor before answering `ping: "busy"` (crew#471). */
|
|
82
|
+
export const HEALTH_PING_WAIT_MS = 1_000;
|
|
83
|
+
/** `HealthResponse.ping` while the engine's actor is occupied: the daemon serves, the engine is busy. */
|
|
84
|
+
export const ENGINE_BUSY_PING = 'busy';
|
|
68
85
|
// Daemon version reported by /health — read from package.json so it never drifts
|
|
69
86
|
// from the shipped version across releases. Resolves the package root from the
|
|
70
87
|
// compiled module location (dist/api/routes.js → ../../package.json) and works the
|
|
@@ -306,6 +323,12 @@ const RegisterRepoSchema = z
|
|
|
306
323
|
// defaults to ~/.wicked/repos/<name>.
|
|
307
324
|
rootPath: z.string().optional(),
|
|
308
325
|
gitUrl: z.string().optional(),
|
|
326
|
+
/** crew#496: the project the register form had selected — the onboarding run is filed there
|
|
327
|
+
* (membership `crew.run`), not left unfiled while the repo joins the project. */
|
|
328
|
+
projectId: z.string().min(1).max(128).optional(),
|
|
329
|
+
/** crew#496 / crew#632: the launch surface and caller, recorded on the run's launch entry. */
|
|
330
|
+
channel: z.enum(['studio', 'cli', 'api']).optional(),
|
|
331
|
+
actor: z.string().min(1).max(256).optional(),
|
|
309
332
|
})
|
|
310
333
|
.strict()
|
|
311
334
|
.refine((d) => {
|
|
@@ -315,6 +338,14 @@ const RegisterRepoSchema = z
|
|
|
315
338
|
// or rootPath alone (register existing local repo) — all valid.
|
|
316
339
|
return hasRemote || hasLocal;
|
|
317
340
|
}, { message: 'Provide gitUrl (remote clone) or rootPath (local registration), or both.' });
|
|
341
|
+
/** `POST /repos/:id/onboard`'s optional body (crew#496): where to file the run, and who asked. */
|
|
342
|
+
const OnboardBodySchema = z
|
|
343
|
+
.object({
|
|
344
|
+
projectId: z.string().min(1).max(128).optional(),
|
|
345
|
+
channel: z.enum(['studio', 'cli', 'api']).optional(),
|
|
346
|
+
actor: z.string().min(1).max(256).optional(),
|
|
347
|
+
})
|
|
348
|
+
.strict();
|
|
318
349
|
/**
|
|
319
350
|
* The launch body. Every field but `problem` is optional, which is what made stripping dangerous:
|
|
320
351
|
* omitting one is a legitimate request that gets an engine default, so a MISSPELLED one was
|
|
@@ -331,6 +362,7 @@ const RegisterRepoSchema = z
|
|
|
331
362
|
// The request-body schemas below are exported so `tests/wire-contract.test.ts` can prove, at
|
|
332
363
|
// compile time, that every body the published contract (`wicked-crew-api-types`) lets a client
|
|
333
364
|
// send is a body these schemas accept — the request-direction half of the drift guard (task #84).
|
|
365
|
+
export { PlanSchema };
|
|
334
366
|
export const LaunchSchema = z.object({
|
|
335
367
|
problem: z.string().min(1),
|
|
336
368
|
sessionId: z.string().min(1).optional(),
|
|
@@ -339,6 +371,10 @@ export const LaunchSchema = z.object({
|
|
|
339
371
|
humanConfirm: z.string().min(1).optional(),
|
|
340
372
|
repoRef: z.string().min(1).optional(),
|
|
341
373
|
workflow: z.string().min(1).optional(),
|
|
374
|
+
/** DES-TEAMING-002 T3 — a user-composed plan (`LaunchRunBody.plan`), forwarded to the engine,
|
|
375
|
+
* which publishes `plan.proposed{by:"human"}` and holds it at a `plan_approval` gate when the
|
|
376
|
+
* approval matrix says so. Mutually exclusive with `workflow` (refine below). */
|
|
377
|
+
plan: PlanSchema.optional(),
|
|
342
378
|
/** DES-PROJECT-001 §2.2 — file the run into a project; membership attaches atomically with
|
|
343
379
|
* the launch record. Unknown/archived ⇒ the launch fails (never a silent unfiled run). */
|
|
344
380
|
projectId: z.string().min(1).optional(),
|
|
@@ -389,7 +425,10 @@ export const LaunchSchema = z.object({
|
|
|
389
425
|
* launch (e.g. a CI job name or a studio tab id). Persisted on the `run.launched` audit entry
|
|
390
426
|
* (`detail.actor`) and served on the run DTO; absent = caller omitted it. */
|
|
391
427
|
actor: z.string().min(1).max(256).optional(),
|
|
392
|
-
}).strict().refine((b) => b.
|
|
428
|
+
}).strict().refine((b) => b.plan === undefined || b.workflow === undefined, {
|
|
429
|
+
message: 'plan and workflow are mutually exclusive — a launch carries a plan or names a preset, not both',
|
|
430
|
+
path: ['plan'],
|
|
431
|
+
}).refine((b) => b.plan !== undefined || b.deliver !== 'pr' || b.workflow !== undefined, {
|
|
393
432
|
message: 'deliver: "pr" requires a workflow — a free-text run has no def to append the deliver phase to',
|
|
394
433
|
path: ['deliver'],
|
|
395
434
|
}).refine((b) => b.deliverGate === undefined || b.deliver !== 'none', {
|
|
@@ -408,19 +447,43 @@ export const LaunchSchema = z.object({
|
|
|
408
447
|
message: 'revisesPr needs deliver: "pr" — a launch that declines delivery has no phase to push the revision with',
|
|
409
448
|
path: ['revisesPr'],
|
|
410
449
|
});
|
|
450
|
+
/** The escalation arms (wicked-core#469 / #467): approve-shaped, no `amend` / `amendScope`.
|
|
451
|
+
* `extend` | `targeted` | `accept_partial` answer the gate of a repo-checks floor that did not
|
|
452
|
+
* finish (`repo_checks_timeout`) — the engine re-runs the floor on the tree as it stands with 2×
|
|
453
|
+
* bounds, the targeted test set, or the unfinished checks waived; `accept_suggestion` adopts the
|
|
454
|
+
* evaluator's discarded, pinned edit as the creator's amendment. The ENGINE refuses each at any
|
|
455
|
+
* other gate (a 409 naming the gate it answers). */
|
|
456
|
+
export const ESCALATION_ACTIONS = ['extend', 'targeted', 'accept_partial', 'accept_suggestion'];
|
|
457
|
+
const isEscalationAction = (a) => a !== undefined && ESCALATION_ACTIONS.includes(a);
|
|
411
458
|
/** `POST /runs/:id/gate` (api-types 0.38.0 `GateDecision`; DES-L1 PR-2). Additive arms: `action`
|
|
412
459
|
* names the arm (`approve` | `request_changes` | `reject`; absent = today's two-arm mapping of
|
|
413
460
|
* `approve`), `amendScope` says where an approve's `amend` lands (`cursor` = the gated unit, today's
|
|
414
461
|
* behaviour | `creator` = the first creator phase at/after the cursor — an intake steer reaches the
|
|
415
462
|
* phase that implements). `request_changes` sends a NOT-PASS review back to the creator with the
|
|
416
463
|
* findings in context (`amend` = the operator's note). A disagreement between `action` and
|
|
417
|
-
* `approve` is a 400 that names both.
|
|
464
|
+
* `approve` is a 400 that names both. The escalation arms ({@link ESCALATION_ACTIONS}) require
|
|
465
|
+
* `approve: true` and take no `amend` / `amendScope` / `plan`.
|
|
466
|
+
*
|
|
467
|
+
* `amend_intent` (wicked-core#555) approves the gate AND AMENDS THE RUN'S INTENT: `amend` is
|
|
468
|
+
* what the acceptance list now says (required, non-empty), `approve: true`, and no `amendScope`
|
|
469
|
+
* — an intent amendment reaches every unit at or after the cursor, which is what makes it reach
|
|
470
|
+
* the LATER EVALUATOR. It is the only arm that can descope a run mid-flight. */
|
|
418
471
|
export const GateSchema = z.object({
|
|
419
472
|
approve: z.boolean(),
|
|
420
473
|
amend: z.string().optional(),
|
|
421
|
-
action: z.enum(['approve', 'request_changes', 'reject']).optional(),
|
|
474
|
+
action: z.enum(['approve', 'request_changes', 'reject', 'edit_plan', 'amend_intent', ...ESCALATION_ACTIONS]).optional(),
|
|
422
475
|
amendScope: z.enum(['cursor', 'creator']).optional(),
|
|
423
|
-
|
|
476
|
+
/** DES-TEAMING-002 T3 — approve a `plan_approval` gate WITH AN EDIT (`GateDecision.plan`). */
|
|
477
|
+
plan: PlanSchema.optional(),
|
|
478
|
+
/** api-types 0.44.0 — the gate this decision answers (its unit `ord`). A mismatch with the run's
|
|
479
|
+
* open gate is a 409 `gate_changed`: the decision was made on a gate that is no longer open. */
|
|
480
|
+
ord: z.number().int().nonnegative().optional(),
|
|
481
|
+
}).strict().refine((b) => b.action === undefined
|
|
482
|
+
|| (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(
|
|
483
|
+
// (wicked-core#555) An intent amendment IS its text: an empty one amends nothing, and the arm's
|
|
484
|
+
// scope is every unit at or after the cursor — which is what makes it reach the evaluator — so
|
|
485
|
+
// `amendScope` cannot narrow it. Both are 400s here rather than a 409 from the engine.
|
|
486
|
+
(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'] });
|
|
424
487
|
/** `PUT /runs/:id/guidance` (DES-UX-002 §7.2, CREW-UX-7) — the durable pre-gate note body.
|
|
425
488
|
* The empty string is a legal body: it CLEARS the note. The byte cap is checked in the route
|
|
426
489
|
* (not zod's char-counting `max`) so the 400 names the actual limit. */
|
|
@@ -464,6 +527,14 @@ export const OpenPathSchema = z.object({
|
|
|
464
527
|
export const RetireMemorySchema = z.object({
|
|
465
528
|
scope_prefix: z.string(),
|
|
466
529
|
}).strict();
|
|
530
|
+
/** `POST /memory/retire-item` body (studio#206): ONE memory, by its `memory_id` from `GET /memory`.
|
|
531
|
+
* `.strict()`: a stray `scope_prefix` is a 400, never read as a wider erase than one row. */
|
|
532
|
+
export const RetireMemoryItemSchema = z.object({
|
|
533
|
+
memory_id: z.string().min(1).max(512),
|
|
534
|
+
}).strict();
|
|
535
|
+
/** The error an estate from before wicked-estate#188 answers to `memory.erase {id}`: it knows only the
|
|
536
|
+
* subtree erase, so it refuses the call (deleting nothing) for want of a `scope_prefix`. */
|
|
537
|
+
const ESTATE_NO_ERASE_BY_ID = /scope_prefix \(non-empty\) required/i;
|
|
467
538
|
/**
|
|
468
539
|
* A SKIN-OWNED settings key (crew#323): one lowercase segment under the `studio.` namespace,
|
|
469
540
|
* e.g. `studio.appearance`, `studio.notifications`. The daemon never interprets these values —
|
|
@@ -596,11 +667,13 @@ at) {
|
|
|
596
667
|
'The single-seat root cause is tracked as wicked-core#563; this chat makes it visible.',
|
|
597
668
|
};
|
|
598
669
|
}
|
|
670
|
+
/** N1: why a post-hoc deliver answers 409 for a branch already on a non-GitHub origin. */
|
|
671
|
+
function pushedOnlyRefusal(runId, pushed) {
|
|
672
|
+
return (`run ${runId} is delivered as a pushed branch: ${pushed.branch} is on ${pushed.remote}, and no ` +
|
|
673
|
+
'pull request can be opened there because that origin is not a GitHub host gh can resolve — ' +
|
|
674
|
+
'open the pull/merge request on your forge; merge stays human');
|
|
675
|
+
}
|
|
599
676
|
export function registerRoutes(app, adapter, gateCache, elicitationCache,
|
|
600
|
-
// Defaulted so a caller that never arms the bus seam (tests drive this
|
|
601
|
-
// function directly) gets the same behavior as an unarmed daemon: an empty
|
|
602
|
-
// cache, `busEvent: null`, and the lazy ledger read doing all the work.
|
|
603
|
-
qeGateEvents = new QeGateCache(),
|
|
604
677
|
// Defaulted for the same reason: tests that drive this function directly get
|
|
605
678
|
// the projects surface with no bus (events skipped) and a fresh index.
|
|
606
679
|
projects = { bus: null, index: new MembershipIndex(), log: () => undefined },
|
|
@@ -614,6 +687,8 @@ security = { audit: AuditLog.noop(), authMode: 'off' },
|
|
|
614
687
|
runtime = {}) {
|
|
615
688
|
const { audit } = security;
|
|
616
689
|
const seatHealth = runtime.seatHealth ?? new SeatHealthTracker();
|
|
690
|
+
const deliveryFreeze = runtime.deliveryFreeze ?? new DeliveryFreeze();
|
|
691
|
+
registerDeliveryFreezeRoutes(app, { freeze: deliveryFreeze, audit });
|
|
617
692
|
const openWithOs = runtime.openWithOs ?? openWithSystemDefault;
|
|
618
693
|
const signedIn = runtime.signedIn ?? signedInHeuristic;
|
|
619
694
|
const retryIndex = runtime.retryIndex ?? new RetryIndex();
|
|
@@ -651,7 +726,7 @@ runtime = {}) {
|
|
|
651
726
|
new DeliveryDerivationCache({
|
|
652
727
|
listViews: () => adapter.sessionsDetail(),
|
|
653
728
|
probes: vacuityProbes,
|
|
654
|
-
isDelivered: (runId) => deliveryIndex.
|
|
729
|
+
isDelivered: (runId) => deliveryIndex.isDelivered(runId),
|
|
655
730
|
canDeliver,
|
|
656
731
|
});
|
|
657
732
|
const deliverExec = runtime.deliverExec ?? runDeliverScript;
|
|
@@ -670,6 +745,32 @@ runtime = {}) {
|
|
|
670
745
|
const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
|
|
671
746
|
/** DES-L9: PR number → head branch, through `gh pr view` unless the runtime injected an answerer. */
|
|
672
747
|
const resolvePullRequest = runtime.resolvePullRequest ?? resolvePullRequestViaGh;
|
|
748
|
+
/** crew#627: the intent's linked issues, read under the daemon's identity at launch. */
|
|
749
|
+
const readLinkedIssues = runtime.resolveLinkedIssues ?? ((p, root, ref) => resolveLinkedIssues(p, root, ref));
|
|
750
|
+
/** crew#550 P-7: the follow-ups the run's evaluator units flagged, read from their captured output.
|
|
751
|
+
* A unit whose output cannot be read contributes nothing (the section then says what WAS read);
|
|
752
|
+
* an adapter without transcripts (a directly-driven route set) reads as "not read" — absent. */
|
|
753
|
+
const evaluatorFollowUps = async (view) => {
|
|
754
|
+
if (typeof adapter.workOutput !== 'function')
|
|
755
|
+
return undefined;
|
|
756
|
+
const evaluators = view.units.filter((u) => u.role === 'evaluator').sort((a, b) => a.ord - b.ord);
|
|
757
|
+
const out = [];
|
|
758
|
+
for (const u of evaluators) {
|
|
759
|
+
let text = null;
|
|
760
|
+
try {
|
|
761
|
+
text = await adapter.workOutput(coreUnitId(view.session.id, u));
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
text = null;
|
|
765
|
+
}
|
|
766
|
+
if (text === null)
|
|
767
|
+
continue;
|
|
768
|
+
for (const x of extractFollowUps(text))
|
|
769
|
+
if (!out.includes(x))
|
|
770
|
+
out.push(x);
|
|
771
|
+
}
|
|
772
|
+
return out.slice(0, FOLLOW_UPS_MAX);
|
|
773
|
+
};
|
|
673
774
|
// The run-DTO joins (DES-UX-001 §8.2/§8.3, DES-UX-002 §7.2): `project_id` from the membership
|
|
674
775
|
// record — `null` = genuinely unfiled, so the field is ALWAYS present on served runs —
|
|
675
776
|
// `retry_of` from the lineage index and `guidance` from the guidance index, each set only
|
|
@@ -717,12 +818,19 @@ runtime = {}) {
|
|
|
717
818
|
const url = deliveryIndex.urlFor(view.session.id);
|
|
718
819
|
if (url !== undefined)
|
|
719
820
|
return { delivery: 'delivered', deliverUrl: url };
|
|
821
|
+
// N1: a branch pushed to a non-GitHub origin IS the delivery — never 'stranded'.
|
|
822
|
+
const pushed = deliveryIndex.pushedFor(view.session.id);
|
|
823
|
+
if (pushed !== undefined)
|
|
824
|
+
return pushedState(pushed);
|
|
720
825
|
if (conflictStrand)
|
|
721
826
|
return { delivery: 'stranded' };
|
|
722
827
|
return deliveryCache.read(view);
|
|
723
828
|
};
|
|
724
829
|
const decorateRun = (view) => {
|
|
725
830
|
const conflictStrand = normalizeStranded(view);
|
|
831
|
+
// Seam X2: what the run IS — the adapter attaches it from the engine's record; a view from a
|
|
832
|
+
// directly-driven adapter gets it resolved from its record here, so it is always served.
|
|
833
|
+
view.session.run_identity = wireIdentity(runIdentityOf(view));
|
|
726
834
|
view.session.project_id = projects.index.projectOf(view.session.id) ?? null;
|
|
727
835
|
// Wave 6 (F-4R2-006): the interactive document this run answered, from the seams' handoff
|
|
728
836
|
// ledgers — `null` = genuinely not a document run, so the field is ALWAYS present on served runs.
|
|
@@ -769,6 +877,10 @@ runtime = {}) {
|
|
|
769
877
|
view.session.delivery = state.delivery;
|
|
770
878
|
if (state.deliverUrl !== undefined)
|
|
771
879
|
view.session.deliverUrl = state.deliverUrl;
|
|
880
|
+
if (state.deliverBranch !== undefined)
|
|
881
|
+
view.session.deliverBranch = state.deliverBranch;
|
|
882
|
+
if (state.deliverRemote !== undefined)
|
|
883
|
+
view.session.deliverRemote = state.deliverRemote;
|
|
772
884
|
// crew#641/#642 (item 5): chat promotion provenance — ABSENT when not a chat-promoted run.
|
|
773
885
|
const chatSeatCount = runTimingIndex.chatSeatCountFor(view.session.id);
|
|
774
886
|
if (chatSeatCount !== undefined)
|
|
@@ -787,13 +899,31 @@ runtime = {}) {
|
|
|
787
899
|
// still gets the ONE actor shape via this accessor.
|
|
788
900
|
const actorOf = (req) => req.actor ?? LOCAL_ACTOR;
|
|
789
901
|
// Liveness — also proves the actor + event pump are up.
|
|
902
|
+
//
|
|
903
|
+
// crew#471: the engine's ping is a round trip through its single-writer actor, and a launch that
|
|
904
|
+
// plans many units holds that actor for minutes. Awaiting it made `/health` go dark exactly when
|
|
905
|
+
// an operator needed to know the daemon was up, and each waiting probe pinned a libuv worker
|
|
906
|
+
// thread the rest of the read path needs. So at most ONE ping is in flight (later probes join it)
|
|
907
|
+
// and `/health` waits for it at most HEALTH_PING_WAIT_MS: past that it answers `ping: "busy"` —
|
|
908
|
+
// the daemon serves, the engine is occupied.
|
|
909
|
+
let pingInFlight = null;
|
|
910
|
+
const enginePing = () => {
|
|
911
|
+
pingInFlight ??= adapter.ping().finally(() => {
|
|
912
|
+
pingInFlight = null;
|
|
913
|
+
});
|
|
914
|
+
let timer;
|
|
915
|
+
const busy = new Promise((resolve) => {
|
|
916
|
+
timer = setTimeout(() => resolve(ENGINE_BUSY_PING), HEALTH_PING_WAIT_MS);
|
|
917
|
+
});
|
|
918
|
+
return Promise.race([pingInFlight, busy]).finally(() => clearTimeout(timer));
|
|
919
|
+
};
|
|
790
920
|
// `config.manifest` on the routes below (TH-11): the declaration channel the endpoint manifest
|
|
791
921
|
// reads — type names bind to `wicked-crew-api-types` exports where one exists, structural
|
|
792
922
|
// spellings where the contract has no name, statusCodes list every code the route answers on
|
|
793
923
|
// purpose. Declared on the highest-traffic run-lifecycle routes first; the manifest records
|
|
794
924
|
// null / [] for the rest ("where declared", never invented). See src/api/endpoint-manifest.ts.
|
|
795
925
|
app.get(`${V}/health`, { config: { manifest: { statusCodes: [200] } } }, async () => {
|
|
796
|
-
const ping = await
|
|
926
|
+
const ping = await enginePing();
|
|
797
927
|
// F-E2E-030: what this deployment can keep. A composer reads `capabilities.deliverGate`
|
|
798
928
|
// before it promises "pauses at the deliver gate"; a stub-driven route set without the
|
|
799
929
|
// probe honestly reports no gate.
|
|
@@ -821,6 +951,12 @@ runtime = {}) {
|
|
|
821
951
|
...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
|
|
822
952
|
...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
|
|
823
953
|
...phaseSkillFindings(runtime.phaseSkills?.gaps() ?? []),
|
|
954
|
+
// DES-TEAMING-002 T0: the daemon's bus did not open at boot, so the engine was handed none
|
|
955
|
+
// and runs un-teamed. A daemon-level notice (status stays ok: it serves); the per-run
|
|
956
|
+
// `transport:"none"` record lands with P1.
|
|
957
|
+
...(adapter.busUnavailable != null ? [busUnavailableWarning(adapter.busUnavailable)] : []),
|
|
958
|
+
// wicked-core#631: the engine has the bus but not the calls crew reaches it through.
|
|
959
|
+
...(adapter.busSeamsOff != null ? [busSeamsOffWarning(adapter.busSeamsOff)] : []),
|
|
824
960
|
];
|
|
825
961
|
return {
|
|
826
962
|
status: 'ok',
|
|
@@ -894,8 +1030,10 @@ runtime = {}) {
|
|
|
894
1030
|
authMode: security.authMode,
|
|
895
1031
|
}));
|
|
896
1032
|
// The actor audit trail (task #88): who launched/steered/governed what.
|
|
897
|
-
// Read-only (observer trust); newest first; `?runId=` / `?action=` / `?limit=`.
|
|
898
|
-
|
|
1033
|
+
// Read-only (observer trust); newest first; `?runId=` / `?action=` / `?limit=` / `?since=`.
|
|
1034
|
+
// `since` (unix millis, inclusive) is the lower time bound a skin's "while you were away"
|
|
1035
|
+
// handover reads — the trail since the operator's last visit (studio wave 2b).
|
|
1036
|
+
app.get(`${V}/audit`, { config: { manifest: { responseType: 'AuditPage', statusCodes: [200, 400, 500] } } }, async (req, reply) => {
|
|
899
1037
|
const q = req.query;
|
|
900
1038
|
const first = (v) => (Array.isArray(v) ? v[0] : v)?.trim() || undefined;
|
|
901
1039
|
const runId = first(q.runId);
|
|
@@ -907,11 +1045,18 @@ runtime = {}) {
|
|
|
907
1045
|
if (limitRaw !== undefined && (!Number.isFinite(limit) || limit < 1 || !Number.isInteger(limit))) {
|
|
908
1046
|
return reply.code(400).send({ error: '`limit` must be a positive integer' });
|
|
909
1047
|
}
|
|
1048
|
+
// Same strict parse as `limit`, but zero is a valid clock (the epoch).
|
|
1049
|
+
const sinceRaw = first(q.since);
|
|
1050
|
+
const since = sinceRaw !== undefined ? Number(sinceRaw) : undefined;
|
|
1051
|
+
if (sinceRaw !== undefined && (!Number.isInteger(since) || since < 0)) {
|
|
1052
|
+
return reply.code(400).send({ error: '`since` must be a non-negative integer (unix millis)' });
|
|
1053
|
+
}
|
|
910
1054
|
try {
|
|
911
1055
|
const entries = await audit.read({
|
|
912
1056
|
...(runId !== undefined ? { runId } : {}),
|
|
913
1057
|
...(action !== undefined ? { action } : {}),
|
|
914
1058
|
...(limit !== undefined ? { limit } : {}),
|
|
1059
|
+
...(since !== undefined ? { since } : {}),
|
|
915
1060
|
});
|
|
916
1061
|
return { entries };
|
|
917
1062
|
}
|
|
@@ -936,6 +1081,33 @@ runtime = {}) {
|
|
|
936
1081
|
// disagree about a seat.
|
|
937
1082
|
const rosterWithStanding = runtime.rosterWithStanding ?? rosterWithStandingFactory({ seatHealth, signedIn });
|
|
938
1083
|
app.get(`${V}/roster`, async () => ({ roster: rosterWithStanding() }));
|
|
1084
|
+
// Each seat's week (studio's weekly 1:1 per agent; api-types 0.52.0): units, first pass, rework,
|
|
1085
|
+
// stalls, bench and cost, folded from the runs' durable event logs by `seatRecord` — nothing new
|
|
1086
|
+
// is stored. The daemon's remembered stall frames join the engine's, as on `/runs/:id/events`.
|
|
1087
|
+
app.get(`${V}/roster/record`, { config: { manifest: { responseType: 'SeatRecordResponse', statusCodes: [200, 400, 503] } } }, async (req, reply) => {
|
|
1088
|
+
const raw = req.query.days;
|
|
1089
|
+
let days = SEAT_RECORD_DEFAULT_DAYS;
|
|
1090
|
+
if (raw !== undefined) {
|
|
1091
|
+
const one = Array.isArray(raw) ? undefined : raw;
|
|
1092
|
+
const n = one !== undefined && /^[1-9][0-9]?$/.test(one) ? Number(one) : NaN;
|
|
1093
|
+
if (!(n >= 1 && n <= SEAT_RECORD_MAX_DAYS)) {
|
|
1094
|
+
return reply.code(400).send({ error: `Invalid ?days — expected one whole number from 1 to ${SEAT_RECORD_MAX_DAYS}, got ${JSON.stringify(raw)}` });
|
|
1095
|
+
}
|
|
1096
|
+
days = n;
|
|
1097
|
+
}
|
|
1098
|
+
const views = (await adapter.sessionsDetail()).map(decorateRun);
|
|
1099
|
+
const out = await seatRecord(views, async (id) => {
|
|
1100
|
+
const engine = await adapter.runEvents(id);
|
|
1101
|
+
if (engine === null)
|
|
1102
|
+
return null;
|
|
1103
|
+
const daemon = runtime.stallFrames?.(id) ?? [];
|
|
1104
|
+
return daemon.length === 0 ? engine : [...engine, ...daemon];
|
|
1105
|
+
}, { days, now: Date.now() });
|
|
1106
|
+
if (out === null) {
|
|
1107
|
+
return reply.code(503).send({ error: 'Seat records are unavailable: this wicked-core build has no event-log read binding' });
|
|
1108
|
+
}
|
|
1109
|
+
return out;
|
|
1110
|
+
});
|
|
939
1111
|
// Open a file/folder with the OS default application (crew#273) — the studio Files tab's
|
|
940
1112
|
// click-to-open. The open MUST happen daemon-side (the SPA cannot spawn a process), which is
|
|
941
1113
|
// why the path is validated first: absolute, and inside one of the caller-visible roots — the
|
|
@@ -1172,33 +1344,61 @@ runtime = {}) {
|
|
|
1172
1344
|
});
|
|
1173
1345
|
// Registered repos → target-repo picker.
|
|
1174
1346
|
app.get(`${V}/repos`, async () => ({ repos: await adapter.listRepos() }));
|
|
1347
|
+
/** Filing for an onboarding run: what `POST /runs` does for a launch, for the runs `POST /repos`
|
|
1348
|
+
* and `POST /repos/:id/onboard` start (crew#496). The launch entry (`run.launched`, which dates
|
|
1349
|
+
* the run and names who started it) is always written; with a `projectId` the run joins that
|
|
1350
|
+
* project. The run is LIVE by then, so a filing failure is named in the answer, never a 4xx. */
|
|
1351
|
+
const recordOnboardingLaunch = async (req, runId, repoId, opts) => {
|
|
1352
|
+
recordRunLaunched(audit, runTimingIndex, actorOf(req), runId, {
|
|
1353
|
+
workflow: 'onboarding',
|
|
1354
|
+
repoRef: repoId,
|
|
1355
|
+
...(opts.projectId !== undefined ? { projectId: opts.projectId } : {}),
|
|
1356
|
+
...(opts.channel !== undefined ? { channel: opts.channel } : {}),
|
|
1357
|
+
...(opts.actor !== undefined ? { actor: opts.actor } : {}),
|
|
1358
|
+
});
|
|
1359
|
+
if (opts.projectId === undefined)
|
|
1360
|
+
return undefined;
|
|
1361
|
+
try {
|
|
1362
|
+
const { member } = await adapter.projectMemberAttach(opts.projectId, 'crew.run', runId, { onboarding: true }, actorOf(req).id);
|
|
1363
|
+
projects.index.set(runId, opts.projectId);
|
|
1364
|
+
projects.bus?.emit(MEMBERSHIP_ATTACHED, { project_id: opts.projectId, member: { kind: 'crew.run', ref: runId }, actor: actorOf(req).id }, membershipAttachedKey(opts.projectId, 'crew.run', runId, member.attached_at));
|
|
1365
|
+
return undefined;
|
|
1366
|
+
}
|
|
1367
|
+
catch (err) {
|
|
1368
|
+
req.log.warn({ runId, projectId: opts.projectId }, `onboarding run filing failed: ${message(err)}`);
|
|
1369
|
+
return message(err);
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1175
1372
|
app.post(`${V}/repos`, async (req, reply) => {
|
|
1176
1373
|
const parsed = RegisterRepoSchema.safeParse(req.body);
|
|
1177
1374
|
if (!parsed.success) {
|
|
1178
1375
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
1179
1376
|
}
|
|
1180
|
-
const { name, rootPath, gitUrl } = parsed.data;
|
|
1377
|
+
const { name, rootPath, gitUrl, projectId, channel, actor } = parsed.data;
|
|
1378
|
+
let repo;
|
|
1379
|
+
let runId;
|
|
1181
1380
|
try {
|
|
1182
1381
|
if (gitUrl) {
|
|
1183
1382
|
// Remote: clone to rootPath (if provided) or default ~/.wicked/repos/<name>,
|
|
1184
1383
|
// register, and launch onboarding run.
|
|
1185
|
-
const
|
|
1384
|
+
const cloned = await adapter.cloneAndRegisterRepo(name, gitUrl, rootPath);
|
|
1385
|
+
runId = cloned.runId;
|
|
1186
1386
|
const repos = await adapter.listRepos();
|
|
1187
|
-
|
|
1387
|
+
repo = repos.find((r) => r.id === cloned.repoId);
|
|
1188
1388
|
if (!repo)
|
|
1189
1389
|
return reply.code(500).send({ error: 'Repo registered but could not be retrieved' });
|
|
1190
|
-
return reply.code(201).send({ repo, onboardRunId: runId });
|
|
1191
1390
|
}
|
|
1192
1391
|
else {
|
|
1193
1392
|
// Local: register then launch onboarding run.
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
return reply.code(201).send({ repo, onboardRunId: runId });
|
|
1393
|
+
repo = await adapter.registerRepo(name, rootPath);
|
|
1394
|
+
runId = await adapter.launchOnboardingRun(repo.id, name);
|
|
1197
1395
|
}
|
|
1198
1396
|
}
|
|
1199
1397
|
catch (err) {
|
|
1200
1398
|
return reply.code(400).send({ error: message(err) });
|
|
1201
1399
|
}
|
|
1400
|
+
const projectAttachError = await recordOnboardingLaunch(req, runId, repo.id, { projectId, channel, actor });
|
|
1401
|
+
return reply.code(201).send({ repo, onboardRunId: runId, ...(projectAttachError !== undefined ? { projectAttachError } : {}) });
|
|
1202
1402
|
});
|
|
1203
1403
|
// Return the onboarding run id for a repo (so the UI can navigate to it).
|
|
1204
1404
|
app.get(`${V}/repos/:id/onboard`, async (req, reply) => {
|
|
@@ -1213,13 +1413,18 @@ runtime = {}) {
|
|
|
1213
1413
|
const repo = repos.find((r) => r.id === id);
|
|
1214
1414
|
if (!repo)
|
|
1215
1415
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
1416
|
+
const body = OnboardBodySchema.safeParse(req.body ?? {});
|
|
1417
|
+
if (!body.success)
|
|
1418
|
+
return reply.code(400).send(invalidBody(body.error, 'Invalid request body'));
|
|
1419
|
+
let runId;
|
|
1216
1420
|
try {
|
|
1217
|
-
|
|
1218
|
-
return reply.code(201).send({ runId });
|
|
1421
|
+
runId = await adapter.launchOnboardingRun(repo.id, repo.name);
|
|
1219
1422
|
}
|
|
1220
1423
|
catch (err) {
|
|
1221
1424
|
return reply.code(400).send({ error: message(err) });
|
|
1222
1425
|
}
|
|
1426
|
+
const projectAttachError = await recordOnboardingLaunch(req, runId, repo.id, body.data);
|
|
1427
|
+
return reply.code(201).send({ runId, ...(projectAttachError !== undefined ? { projectAttachError } : {}) });
|
|
1223
1428
|
});
|
|
1224
1429
|
// Launch a run (replaces POST /sessions). `clisJson` defaults to the roster;
|
|
1225
1430
|
// `sessionId` is minted if the client omits it.
|
|
@@ -1227,7 +1432,7 @@ runtime = {}) {
|
|
|
1227
1432
|
config: {
|
|
1228
1433
|
manifest: {
|
|
1229
1434
|
requestType: 'LaunchRunBody',
|
|
1230
|
-
responseType: '
|
|
1435
|
+
responseType: 'LaunchRunResponse',
|
|
1231
1436
|
// 404/409: unknown project / unknown campaignId (wicked-studio#27) /
|
|
1232
1437
|
// archived-or-synthesized project + busy engine + the state-home blocker
|
|
1233
1438
|
// (`state_home_unregistered`) + a roster with no eligible seat (`no_eligible_seat`,
|
|
@@ -1244,6 +1449,13 @@ runtime = {}) {
|
|
|
1244
1449
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
1245
1450
|
}
|
|
1246
1451
|
const b = parsed.data;
|
|
1452
|
+
// The demo preset launches only from POST /projects/:id/demo (api/demo.ts), which mints the run's
|
|
1453
|
+
// demo root and brief first: launched here directly it would have neither.
|
|
1454
|
+
if (b.workflow === DEMO_PRESET && (b.sessionId === undefined || !demoLaunchRoots.has(b.sessionId))) {
|
|
1455
|
+
return reply.code(400).send({
|
|
1456
|
+
error: 'the demo preset launches from POST /projects/:id/demo, which makes its demo root and brief; POST /runs does not take it directly',
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1247
1459
|
// wicked-core#411 / crew#497: the state-home blocker, judged BEFORE anything is resolved or
|
|
1248
1460
|
// committed. While the handed skills snapshot derives a state home with an entry the worker
|
|
1249
1461
|
// Read fence cannot classify, the engine refuses this launch at intake (and an engine before
|
|
@@ -1256,6 +1468,10 @@ runtime = {}) {
|
|
|
1256
1468
|
return reply.code(409).send(stateHomeBlockerBody(stateHome));
|
|
1257
1469
|
}
|
|
1258
1470
|
}
|
|
1471
|
+
// crew#645: an expired login reads signed out BEFORE routing — wait (bounded) for every seat's
|
|
1472
|
+
// missing or stale login check, so the standing below is the seats' own answer.
|
|
1473
|
+
if (b.clisJson === undefined)
|
|
1474
|
+
await rosterWithStanding.ready?.();
|
|
1259
1475
|
const input = {
|
|
1260
1476
|
problem: b.problem,
|
|
1261
1477
|
sessionId: b.sessionId ?? randomUUID(),
|
|
@@ -1264,6 +1480,15 @@ runtime = {}) {
|
|
|
1264
1480
|
// (`core/engine-roster.ts`), so a signed-out seat is never convened, never a judge.
|
|
1265
1481
|
clisJson: b.clisJson ?? JSON.stringify(rosterWithStanding()),
|
|
1266
1482
|
};
|
|
1483
|
+
// A capture launch (api/capture.ts) reads its materials from a per-run inbox outside every
|
|
1484
|
+
// sandbox: the capture route registered it under the run id it minted.
|
|
1485
|
+
const captureRoot = captureLaunchRoots.get(input.sessionId);
|
|
1486
|
+
if (captureRoot !== undefined)
|
|
1487
|
+
input.extraWriteRoots = [captureRoot];
|
|
1488
|
+
// A demo launch (api/demo.ts) writes every file into the demo root the demo route minted for it.
|
|
1489
|
+
const demoRoot = demoLaunchRoots.get(input.sessionId);
|
|
1490
|
+
if (demoRoot !== undefined)
|
|
1491
|
+
input.extraWriteRoots = [demoRoot];
|
|
1267
1492
|
if (b.entityMode !== undefined)
|
|
1268
1493
|
input.entityMode = b.entityMode;
|
|
1269
1494
|
if (b.humanConfirm !== undefined)
|
|
@@ -1277,6 +1502,10 @@ runtime = {}) {
|
|
|
1277
1502
|
input.repoRef = b.repoRef;
|
|
1278
1503
|
if (b.workflow !== undefined)
|
|
1279
1504
|
input.workflow = b.workflow;
|
|
1505
|
+
// DES-TEAMING-002 T3: the plan is the ENGINE's to propose, score, floor and gate — crew
|
|
1506
|
+
// forwards the command and publishes nothing (§4.0).
|
|
1507
|
+
if (b.plan !== undefined)
|
|
1508
|
+
input.plan = toLaunchPlan(b.plan);
|
|
1280
1509
|
if (b.projectId !== undefined) {
|
|
1281
1510
|
input.projectId = b.projectId;
|
|
1282
1511
|
// A project is a CONTEXT (crew#326): a run filed into one should see the project's whole
|
|
@@ -1395,6 +1624,26 @@ runtime = {}) {
|
|
|
1395
1624
|
throw err;
|
|
1396
1625
|
}
|
|
1397
1626
|
}
|
|
1627
|
+
// crew#627: a workflow run's intent may name its work only by issue ("fix #541"), and the
|
|
1628
|
+
// worker cannot read GitHub. The daemon reads each linked issue now and appends its text to the
|
|
1629
|
+
// problem, inside a marked block the deliver text takes back out. Workflow launches only: the
|
|
1630
|
+
// free-text planner turns sentences into units, so issue text there would become work units.
|
|
1631
|
+
let linkedIssues;
|
|
1632
|
+
if (b.workflow !== undefined) {
|
|
1633
|
+
// Best-effort by contract: reading issues never fails the launch it decorates. A resolver
|
|
1634
|
+
// fault (the repo list unreadable, say) launches the run on its intent alone and is logged.
|
|
1635
|
+
try {
|
|
1636
|
+
const root = b.repoRef !== undefined ? await repoRootOf(b.repoRef) : undefined;
|
|
1637
|
+
const linked = await readLinkedIssues(b.problem, root, b.repoRef);
|
|
1638
|
+
if (linked.block !== null) {
|
|
1639
|
+
input.problem = `${b.problem.trimEnd()}\n\n${linked.block}`;
|
|
1640
|
+
linkedIssues = linked.issues;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
catch (err) {
|
|
1644
|
+
req.log.warn(`linked-issue resolution failed; launching on the intent alone: ${message(err)}`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1398
1647
|
try {
|
|
1399
1648
|
const runId = await adapter.launchRun(input);
|
|
1400
1649
|
// crew#641/#642 (item 5): when a run is promoted from a chat, record how many seats
|
|
@@ -1452,6 +1701,8 @@ runtime = {}) {
|
|
|
1452
1701
|
// crew#632: launch surface and caller identity — human-readable provenance on the audit trail.
|
|
1453
1702
|
...(b.channel !== undefined ? { channel: b.channel } : {}),
|
|
1454
1703
|
...(b.actor !== undefined ? { actor: b.actor } : {}),
|
|
1704
|
+
// crew#627: which linked issues the daemon read into the problem, and which it could not.
|
|
1705
|
+
...(linkedIssues !== undefined ? { linkedIssues } : {}),
|
|
1455
1706
|
});
|
|
1456
1707
|
// Stamp the index live so the field is served without waiting for a daemon restart.
|
|
1457
1708
|
if (chatSeatCount !== undefined && chatGrounded !== undefined) {
|
|
@@ -1479,10 +1730,15 @@ runtime = {}) {
|
|
|
1479
1730
|
// is always reproducible even if the chat is idle-reclaimed before the run finishes.
|
|
1480
1731
|
if (b.chatId !== undefined)
|
|
1481
1732
|
runtime.linkChatRun?.(b.chatId, runId);
|
|
1482
|
-
return reply.code(201).send({ runId });
|
|
1733
|
+
return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}) });
|
|
1483
1734
|
}
|
|
1484
1735
|
catch (err) {
|
|
1485
1736
|
const msg = message(err);
|
|
1737
|
+
// DES-TEAMING-002 T3: a plan on an engine without the plan approval gate is an "upgrade the
|
|
1738
|
+
// engine" 501 — never a free-text run launched in its place.
|
|
1739
|
+
if (err instanceof PlanLaunchUnsupportedError) {
|
|
1740
|
+
return reply.code(501).send({ error: msg });
|
|
1741
|
+
}
|
|
1486
1742
|
// The engine's own intake refusal (wicked-core#411 `StateHomeConfigError`) is a configuration
|
|
1487
1743
|
// error, not a malformed request: the same typed 409 the pre-check above answers, over a fresh
|
|
1488
1744
|
// survey — the pre-check saw a clean state home (or an addon that could not classify), the
|
|
@@ -1645,8 +1901,6 @@ runtime = {}) {
|
|
|
1645
1901
|
return reply.code(404).send({ error: 'Run not found' });
|
|
1646
1902
|
return { run: decorateRun(run) };
|
|
1647
1903
|
});
|
|
1648
|
-
/** The full workflow registry, or `[]` on a directly-driven route set whose fake adapter has none. */
|
|
1649
|
-
const listWorkflowsSafe = () => typeof adapter.listWorkflows === 'function' ? adapter.listWorkflows() : [];
|
|
1650
1904
|
// ── Deliver text (crew#524 / F-3R2-014) — the PR title + body a run's delivery carries ──
|
|
1651
1905
|
// `gh pr create --fill` gave wicked-studio#249 a mid-word title and an EMPTY body. The deliver
|
|
1652
1906
|
// script now asks this route for the text composed from the PERSISTED RUN RECORD: the intent,
|
|
@@ -1661,12 +1915,12 @@ runtime = {}) {
|
|
|
1661
1915
|
const run = views.find((v) => v.session.id === id);
|
|
1662
1916
|
if (!run)
|
|
1663
1917
|
return reply.code(404).send({ error: 'Run not found' });
|
|
1664
|
-
const
|
|
1665
|
-
// The
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
workflowId: def?.id ?? null,
|
|
1918
|
+
const followUps = await evaluatorFollowUps(run);
|
|
1919
|
+
// The run's preset or workflow NAME, as the engine recorded it (seam X2).
|
|
1920
|
+
const text = composeDeliverText(factsFromRun(decorateRun(run), runUrlFor(configuredPublicOrigin(), id), {
|
|
1921
|
+
workflowId: runIdentityOf(run).name,
|
|
1669
1922
|
revisesPr: retryIndex.revisesPrFor(id) ?? null,
|
|
1923
|
+
...(followUps !== undefined ? { followUps } : {}),
|
|
1670
1924
|
}));
|
|
1671
1925
|
return reply.type('text/plain; charset=utf-8').send(framedDeliverText(text));
|
|
1672
1926
|
});
|
|
@@ -1727,10 +1981,19 @@ runtime = {}) {
|
|
|
1727
1981
|
const existing = deliveryIndex.urlFor(id);
|
|
1728
1982
|
if (existing !== undefined)
|
|
1729
1983
|
return { prUrl: existing };
|
|
1984
|
+
// N1: a run whose branch is already on a non-GitHub origin has nothing left to lift, and no
|
|
1985
|
+
// pull request can be opened from here — say so instead of pushing the branch again.
|
|
1986
|
+
const pushedAlready = deliveryIndex.pushedFor(id);
|
|
1987
|
+
if (pushedAlready !== undefined) {
|
|
1988
|
+
return reply.code(409).send({ error: pushedOnlyRefusal(id, pushedAlready) });
|
|
1989
|
+
}
|
|
1730
1990
|
const views = await adapter.sessionsDetail();
|
|
1731
1991
|
const run = views.find((v) => v.session.id === id);
|
|
1732
1992
|
if (!run)
|
|
1733
1993
|
return reply.code(404).send({ error: 'Run not found' });
|
|
1994
|
+
// Freeze deliveries (idea 15): nothing pushes while the switch is on.
|
|
1995
|
+
if (deliveryFreeze.state().frozen)
|
|
1996
|
+
return reply.code(409).send(frozenRefusal(deliveryFreeze.state()));
|
|
1734
1997
|
// crew#418/#432: a run stranded by a recoverable deliver lift failure reads `failed` from the
|
|
1735
1998
|
// engine but IS liftable post-hoc — normalize its status to `completed` so this route
|
|
1736
1999
|
// accepts it, exactly as the run wire and the resume route see it.
|
|
@@ -1794,7 +2057,7 @@ runtime = {}) {
|
|
|
1794
2057
|
// crew#524: the post-hoc lift composes its PR text from the run record it already
|
|
1795
2058
|
// holds (the fallback), and names this daemon so the script can re-ask at delivery.
|
|
1796
2059
|
const origin = boundOrigin(app.server.address());
|
|
1797
|
-
const
|
|
2060
|
+
const workflowName = runIdentityOf(run).name;
|
|
1798
2061
|
const revising = retryIndex.revisesPrFor(id);
|
|
1799
2062
|
let revisesPr = null;
|
|
1800
2063
|
if (revising !== undefined) {
|
|
@@ -1808,14 +2071,20 @@ runtime = {}) {
|
|
|
1808
2071
|
}
|
|
1809
2072
|
revisesPr = { number: revising.number, headRef: again.pr.headRef, url: again.pr.url };
|
|
1810
2073
|
}
|
|
2074
|
+
const followUps = await evaluatorFollowUps(run);
|
|
1811
2075
|
result = await deliverExec(workdir, s.problem ?? undefined, {
|
|
1812
2076
|
runId: id,
|
|
1813
2077
|
apiOrigin: origin,
|
|
1814
|
-
facts: factsFromRun(run, runUrlFor(
|
|
1815
|
-
workflowId:
|
|
2078
|
+
facts: factsFromRun(run, runUrlFor(configuredPublicOrigin(), id), {
|
|
2079
|
+
workflowId: workflowName,
|
|
1816
2080
|
revisesPr: revisesPr === null ? null : { number: revisesPr.number, url: revisesPr.url },
|
|
2081
|
+
...(followUps !== undefined ? { followUps } : {}),
|
|
1817
2082
|
}),
|
|
1818
2083
|
revisesPr,
|
|
2084
|
+
// R1 (Copilot on crew#736): the post-hoc delivery binds `gh pr create --repo` to the
|
|
2085
|
+
// origin's GitHub repository too, read now from the worktree (it shares its repo's
|
|
2086
|
+
// remotes), so the PR opens where origin points rather than where gh's default does.
|
|
2087
|
+
originUrl: await readDeliverOriginUrl(workdir),
|
|
1819
2088
|
});
|
|
1820
2089
|
}
|
|
1821
2090
|
finally {
|
|
@@ -1857,7 +2126,19 @@ runtime = {}) {
|
|
|
1857
2126
|
error: `deliver failed (exit ${result.status}): ${deliverErrorTail(result.output)}`,
|
|
1858
2127
|
});
|
|
1859
2128
|
}
|
|
1860
|
-
|
|
2129
|
+
// The SAME parser the in-run record uses: the script's push-only verdict outranks a URL a
|
|
2130
|
+
// remote hook echoed earlier in the transcript (Copilot on crew#734).
|
|
2131
|
+
const record = deliveryRecordFrom(result.output);
|
|
2132
|
+
const url = record !== null && 'url' in record ? record.url : null;
|
|
2133
|
+
const pushedOnly = record !== null && 'pushed' in record ? record.pushed : null;
|
|
2134
|
+
if (pushedOnly !== null) {
|
|
2135
|
+
// N1: the script pushed the branch and gh could resolve no GitHub repository for the
|
|
2136
|
+
// origin. That IS a delivery — record it, so the run reads `delivery: 'pushed'` — but there
|
|
2137
|
+
// is no PR URL to answer with, so the reply says what happened instead of a 200 `prUrl`.
|
|
2138
|
+
audit.record('run.delivered', actorOf(req), { runId: id, detail: { pushed: pushedOnly, via: 'post-hoc' } });
|
|
2139
|
+
deliveryIndex.setPushed(id, pushedOnly);
|
|
2140
|
+
return reply.code(409).send({ error: pushedOnlyRefusal(id, pushedOnly) });
|
|
2141
|
+
}
|
|
1861
2142
|
if (url === null) {
|
|
1862
2143
|
// Exit 0 with no URL should be unreachable (the script re-derives its own success),
|
|
1863
2144
|
// but a delivery nothing can be pointed at is never recorded (crew#317).
|
|
@@ -2497,11 +2778,11 @@ runtime = {}) {
|
|
|
2497
2778
|
const run = views.find((v) => v.session.id === id);
|
|
2498
2779
|
if (!run)
|
|
2499
2780
|
return reply.code(404).send({ error: 'Run not found' });
|
|
2500
|
-
//
|
|
2501
|
-
//
|
|
2502
|
-
//
|
|
2503
|
-
|
|
2504
|
-
const
|
|
2781
|
+
// What the run must prove, from what it CONTAINS (seam X2): a plan or preset run's steps whose
|
|
2782
|
+
// catalog entry re-verifies evidence, a registered workflow's def, nothing for free text — and
|
|
2783
|
+
// an unknown run (or one the daemon cannot read) is declared and denied with the reason.
|
|
2784
|
+
const verifiedCatalog = typeof adapter.verifiedEvidenceCatalog === 'function' ? await adapter.verifiedEvidenceCatalog() : null;
|
|
2785
|
+
const requirement = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog);
|
|
2505
2786
|
let repo = null;
|
|
2506
2787
|
if (run.session.repo_ref !== null) {
|
|
2507
2788
|
const repos = await adapter.listRepos();
|
|
@@ -2510,8 +2791,7 @@ runtime = {}) {
|
|
|
2510
2791
|
return buildAcceptanceView({
|
|
2511
2792
|
runId: id,
|
|
2512
2793
|
repo,
|
|
2513
|
-
|
|
2514
|
-
gateEvents: qeGateEvents,
|
|
2794
|
+
requirement,
|
|
2515
2795
|
...(qeRunId !== undefined && qeRunId !== '' ? { qeRunId } : {}),
|
|
2516
2796
|
// AW-14 (arch-R13a + R16): the conformance section reads the same wires the standalone
|
|
2517
2797
|
// `/governance/claims` and `/runs/:id/events` routes serve, but run-scoped and resolved
|
|
@@ -2521,55 +2801,122 @@ runtime = {}) {
|
|
|
2521
2801
|
events: (rid) => adapter.runEvents(rid),
|
|
2522
2802
|
});
|
|
2523
2803
|
});
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2804
|
+
/**
|
|
2805
|
+
* The run's OPEN gate, resolved the way `GET /runs/:id/gate` serves it: the cache, then the
|
|
2806
|
+
* engine's durable `interaction_requests` row (adopted into the cache), then the event-log replay.
|
|
2807
|
+
* The caller has already checked `awaiting_human`. `null` = the log records no open gate;
|
|
2808
|
+
* `'no-log'` = this build cannot read the log, so nothing can be said.
|
|
2809
|
+
*/
|
|
2810
|
+
async function resolveOpenGate(id) {
|
|
2811
|
+
const cached = gateCache.get(id);
|
|
2812
|
+
if (cached)
|
|
2813
|
+
return cached;
|
|
2814
|
+
const durableRows = typeof adapter.interactionRequests === 'function'
|
|
2815
|
+
? await adapter.interactionRequests(id, 'open')
|
|
2816
|
+
: null;
|
|
2817
|
+
const durableGate = durableRows?.find((r) => r.kind === 'gate');
|
|
2818
|
+
if (durableGate !== undefined) {
|
|
2819
|
+
// This path builds the entry inline (not through `fold`), so it must run the SAME refusal
|
|
2820
|
+
// detection (issue #419) — otherwise a gate served from the durable row after a restart would
|
|
2821
|
+
// silently drop the warning that the live/replay paths carry.
|
|
2822
|
+
const refusal = detectRefusal(durableGate.prompt);
|
|
2823
|
+
const entry = {
|
|
2824
|
+
ord: durableGate.ord ?? 0,
|
|
2825
|
+
prompt: durableGate.prompt,
|
|
2826
|
+
lifecycle: 'open',
|
|
2827
|
+
receivedAt: new Date(durableGate.created_at).toISOString(),
|
|
2828
|
+
...(refusal !== undefined ? { refusal } : {}),
|
|
2829
|
+
};
|
|
2830
|
+
gateCache.adopt(id, entry);
|
|
2831
|
+
return entry;
|
|
2832
|
+
}
|
|
2833
|
+
const events = await adapter.runEvents(id);
|
|
2834
|
+
if (events === null)
|
|
2835
|
+
return 'no-log';
|
|
2836
|
+
return gateCache.rebuild(id, events) ?? null;
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* THE gate decision path. `POST /runs/:id/gate` answers with it, and so does a standing order
|
|
2840
|
+
* (behaviour 10) — the same awaiting check, the same ord check and 409 `gate_changed` /
|
|
2841
|
+
* `gate_unknown`, the same `confirmGate`, the same `gate.decided` audit. There is no side path:
|
|
2842
|
+
* an order is just another actor, named on the audit line (`extraDetail.standingOrder`).
|
|
2843
|
+
*/
|
|
2844
|
+
async function decideGate(id, decision, actor, extraDetail = {}) {
|
|
2845
|
+
const parsed = GateSchema.safeParse(decision);
|
|
2537
2846
|
if (!parsed.success) {
|
|
2538
|
-
return
|
|
2847
|
+
return { code: 400, body: invalidBody(parsed.error, 'Invalid request body') };
|
|
2539
2848
|
}
|
|
2540
2849
|
const views = await adapter.sessionsDetail();
|
|
2541
2850
|
const run = views.find((v) => v.session.id === id);
|
|
2542
2851
|
if (!run)
|
|
2543
|
-
return
|
|
2852
|
+
return { code: 404, body: { error: 'Run not found' } };
|
|
2544
2853
|
if (run.session.status !== 'awaiting_human') {
|
|
2545
|
-
return
|
|
2546
|
-
|
|
2547
|
-
|
|
2854
|
+
return { code: 409, body: { error: `Run is not awaiting a human gate (status: ${run.session.status})` } };
|
|
2855
|
+
}
|
|
2856
|
+
// api-types 0.44.0: a decision that names its gate answers THAT gate or nothing. When the
|
|
2857
|
+
// daemon cannot tell which gate is open (no cache, no durable row, no log binding, or a log
|
|
2858
|
+
// recording no open gate) it cannot confirm the named one either, so it refuses — the skin
|
|
2859
|
+
// re-reads the gate and the person decides again. A request without `ord` is unchanged.
|
|
2860
|
+
if (parsed.data.ord !== undefined) {
|
|
2861
|
+
const open = await resolveOpenGate(id);
|
|
2862
|
+
if (open === null || open === 'no-log') {
|
|
2863
|
+
return { code: 409, body: {
|
|
2864
|
+
error: `Gate unknown: this decision names the gate before unit ${parsed.data.ord}, but the daemon cannot `
|
|
2865
|
+
+ 'tell which gate is open on this run — refresh and decide again.',
|
|
2866
|
+
code: 'gate_unknown',
|
|
2867
|
+
} };
|
|
2868
|
+
}
|
|
2869
|
+
if (open.ord !== parsed.data.ord) {
|
|
2870
|
+
return { code: 409, body: {
|
|
2871
|
+
error: `Gate changed: this decision was made on the gate before unit ${parsed.data.ord}, but the open gate `
|
|
2872
|
+
+ `is before unit ${open.ord} — it was answered or replaced. Read the open gate before deciding.`,
|
|
2873
|
+
code: 'gate_changed',
|
|
2874
|
+
openOrd: open.ord,
|
|
2875
|
+
} };
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
// Freeze deliveries (idea 15): an APPROVE that would run the run's deliver unit is held while
|
|
2879
|
+
// the switch is on — the gate stays open, so unfreezing lets the same approve through. The gate
|
|
2880
|
+
// is the named one, else the open one, else the unit the run's cursor points at.
|
|
2881
|
+
if (parsed.data.approve && deliveryFreeze.state().frozen) {
|
|
2882
|
+
let gateOrd = parsed.data.ord;
|
|
2883
|
+
if (gateOrd === undefined) {
|
|
2884
|
+
const open = await resolveOpenGate(id);
|
|
2885
|
+
gateOrd = open !== null && open !== 'no-log' ? open.ord : undefined;
|
|
2886
|
+
}
|
|
2887
|
+
const units = run.units ?? [];
|
|
2888
|
+
const gated = gateOrd !== undefined
|
|
2889
|
+
? units.find((u) => u.ord === gateOrd)
|
|
2890
|
+
: [...units].sort((a, b) => a.ord - b.ord)[run.session.unit_ix];
|
|
2891
|
+
if (gated !== undefined && isDeliverUnit(gated)) {
|
|
2892
|
+
return { code: 409, body: frozenRefusal(deliveryFreeze.state()) };
|
|
2893
|
+
}
|
|
2548
2894
|
}
|
|
2549
2895
|
// The steering-author landing (crew#388): decided — and the gate prompt captured — BEFORE
|
|
2550
2896
|
// the confirm, because a terminal-phase approve prunes the gate cache and moves the run out
|
|
2551
|
-
// of `awaiting_human`. The landing itself runs AFTER a successful approve only.
|
|
2552
|
-
// (
|
|
2553
|
-
//
|
|
2554
|
-
const steeringPropose = parsed.data.approve &&
|
|
2555
|
-
typeof adapter.listWorkflows === 'function' &&
|
|
2556
|
-
isSteeringAuthorRun(run, adapter.listWorkflows());
|
|
2897
|
+
// of `awaiting_human`. The landing itself runs AFTER a successful approve only. The run is
|
|
2898
|
+
// classified by its recorded name (seam X2), so a partial-stub adapter with no registry cannot
|
|
2899
|
+
// 500 the legacy gate path here.
|
|
2900
|
+
const steeringPropose = parsed.data.approve && isSteeringAuthorRun(run);
|
|
2557
2901
|
const gatePrompt = steeringPropose ? gateCache.get(id)?.prompt : undefined;
|
|
2558
2902
|
try {
|
|
2559
2903
|
// All five positionals, explicit `undefined` for the absent ones (DES-L1 PR-2; the
|
|
2560
2904
|
// gate-arms tripwire pins the arity). An engine refusal (no creator to send back to,
|
|
2561
2905
|
// an engine older than the arm) surfaces as the existing 409 below.
|
|
2562
|
-
const status = await adapter.confirmGate(id, parsed.data.approve, parsed.data.amend, parsed.data.action, parsed.data.amendScope);
|
|
2906
|
+
const status = await adapter.confirmGate(id, parsed.data.approve, parsed.data.amend, parsed.data.action, parsed.data.amendScope, parsed.data.plan !== undefined ? toLaunchPlan(parsed.data.plan) : undefined);
|
|
2563
2907
|
// WHO approved/rejected — the gate-decision audit (task #88). The engine
|
|
2564
2908
|
// records THAT the gate resolved (interaction_requests / gateDecided);
|
|
2565
2909
|
// only this HTTP layer knows the authenticated principal behind it.
|
|
2566
|
-
audit.record('gate.decided',
|
|
2910
|
+
audit.record('gate.decided', actor, {
|
|
2567
2911
|
runId: id,
|
|
2568
2912
|
detail: {
|
|
2569
2913
|
approve: parsed.data.approve,
|
|
2570
2914
|
...(parsed.data.amend !== undefined ? { amend: parsed.data.amend } : {}),
|
|
2571
2915
|
...(parsed.data.action !== undefined ? { action: parsed.data.action } : {}),
|
|
2572
2916
|
...(parsed.data.amendScope !== undefined ? { amendScope: parsed.data.amendScope } : {}),
|
|
2917
|
+
...(parsed.data.plan !== undefined ? { planSteps: parsed.data.plan.steps.length } : {}),
|
|
2918
|
+
...(parsed.data.ord !== undefined ? { ord: parsed.data.ord } : {}),
|
|
2919
|
+
...extraDetail,
|
|
2573
2920
|
status,
|
|
2574
2921
|
},
|
|
2575
2922
|
});
|
|
@@ -2582,13 +2929,32 @@ runtime = {}) {
|
|
|
2582
2929
|
// text (an operator who wants different rules rejects and re-authors). A reject lands
|
|
2583
2930
|
// nothing — the run cancels and the proposal stays an artifact.
|
|
2584
2931
|
const landing = steeringPropose
|
|
2585
|
-
? await landSteeringProposal({ adapter, audit, actor
|
|
2932
|
+
? await landSteeringProposal({ adapter, audit, actor }, run, gatePrompt)
|
|
2586
2933
|
: undefined;
|
|
2587
|
-
return
|
|
2934
|
+
return { code: 200, body: { status, ...(landing !== undefined ? { landing } : {}) } };
|
|
2588
2935
|
}
|
|
2589
2936
|
catch (err) {
|
|
2590
|
-
|
|
2937
|
+
if (err instanceof PlanLaunchUnsupportedError) {
|
|
2938
|
+
return { code: 501, body: { error: message(err) } };
|
|
2939
|
+
}
|
|
2940
|
+
return { code: 409, body: { error: message(err) } };
|
|
2591
2941
|
}
|
|
2942
|
+
}
|
|
2943
|
+
// The steering gate (§11.1). approve+amend = approve-with-steer; approve:false = reject (cancels).
|
|
2944
|
+
app.post(`${V}/runs/:id/gate`, {
|
|
2945
|
+
config: {
|
|
2946
|
+
manifest: {
|
|
2947
|
+
requestType: 'GateDecision',
|
|
2948
|
+
responseType: '{ status: SessionStatus; landing?: SteeringLandingResult }',
|
|
2949
|
+
// 409 twice over: a run not awaiting a human gate, and an engine refusal at confirm.
|
|
2950
|
+
// 501: an edited plan on an addon without the plan approval gate (DES-TEAMING-002 T3).
|
|
2951
|
+
statusCodes: [200, 400, 404, 409, 501],
|
|
2952
|
+
},
|
|
2953
|
+
},
|
|
2954
|
+
}, async (req, reply) => {
|
|
2955
|
+
const { id } = req.params;
|
|
2956
|
+
const out = await decideGate(id, req.body, actorOf(req));
|
|
2957
|
+
return reply.code(out.code).send(out.body);
|
|
2592
2958
|
});
|
|
2593
2959
|
// Cancel a running or paused run (distinct third action, §11.1).
|
|
2594
2960
|
app.post(`${V}/runs/:id/cancel`, async (req, reply) => {
|
|
@@ -2635,9 +3001,11 @@ runtime = {}) {
|
|
|
2635
3001
|
? `run ${id} is cancelled — a terminal run cannot be resumed; relaunch the work as a new run with POST /runs {"retryOf":"${id}"}`
|
|
2636
3002
|
: state.delivery === 'stranded'
|
|
2637
3003
|
? `run ${id} is already completed — nothing to resume; its unlifted work is on the wicked/${id} branch: deliver it with POST /runs/${id}/deliver`
|
|
2638
|
-
: state.delivery === '
|
|
2639
|
-
? `run ${id} is already completed
|
|
2640
|
-
:
|
|
3004
|
+
: state.delivery === 'pushed'
|
|
3005
|
+
? `run ${id} is already completed and delivered — its branch ${state.deliverBranch ?? `wicked/${id}`} is on ${state.deliverRemote ?? 'origin'} (no pull request: that origin is not a GitHub host gh can resolve); nothing to resume`
|
|
3006
|
+
: state.delivery === 'vacuous'
|
|
3007
|
+
? `run ${id} is already completed, but VACUOUSLY — its units produced no work to resume or deliver; relaunch with POST /runs {"retryOf":"${id}"}`
|
|
3008
|
+
: `run ${id} is already completed — nothing to resume; relaunch the work as a new run with POST /runs {"retryOf":"${id}"}`;
|
|
2641
3009
|
return reply.code(409).send({ error: why, recovery });
|
|
2642
3010
|
}
|
|
2643
3011
|
try {
|
|
@@ -2645,9 +3013,7 @@ runtime = {}) {
|
|
|
2645
3013
|
// A gated resume IS a gate approval, so it lands a steering-author proposal exactly like
|
|
2646
3014
|
// POST /runs/:id/gate would — the "no side door" rule (task #88) holds for the landing
|
|
2647
3015
|
// write too (crew#388). Prompt captured pre-confirm; see the gate route.
|
|
2648
|
-
const steeringPropose = gated &&
|
|
2649
|
-
typeof adapter.listWorkflows === 'function' &&
|
|
2650
|
-
isSteeringAuthorRun(run, adapter.listWorkflows());
|
|
3016
|
+
const steeringPropose = gated && isSteeringAuthorRun(run);
|
|
2651
3017
|
const gatePrompt = steeringPropose ? gateCache.get(id)?.prompt : undefined;
|
|
2652
3018
|
const status = gated ? await adapter.confirmGate(id, true) : await adapter.resumeRun(id);
|
|
2653
3019
|
// A resume of a gated run IS a gate approval — audit it as one, so the
|
|
@@ -2729,7 +3095,7 @@ runtime = {}) {
|
|
|
2729
3095
|
`this build cannot approve the gate on your behalf (no confirmGate); approve via POST /runs/${id}/gate, then reassign`,
|
|
2730
3096
|
});
|
|
2731
3097
|
}
|
|
2732
|
-
if (gated &&
|
|
3098
|
+
if (gated && isSteeringAuthorRun(run)) {
|
|
2733
3099
|
return reply.code(409).send({
|
|
2734
3100
|
error: `run ${id} is awaiting a steering-author propose gate — approve or reject it via POST /runs/${id}/gate (the approve lands the proposal); reassign is not a review of the rules`,
|
|
2735
3101
|
});
|
|
@@ -2797,34 +3163,12 @@ runtime = {}) {
|
|
|
2797
3163
|
return reply.code(404).send({ error: 'No open gate for this run' });
|
|
2798
3164
|
}
|
|
2799
3165
|
// DURABLE TRUTH FIRST (DES-PROJECT-001 §5.3): the engine persists the open prompt in
|
|
2800
|
-
// `interaction_requests`, written in the same transaction as the `awaiting_human` pause —
|
|
2801
|
-
//
|
|
2802
|
-
//
|
|
2803
|
-
//
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
// fall through and let the log speak.
|
|
2807
|
-
const durableRows = typeof adapter.interactionRequests === 'function'
|
|
2808
|
-
? await adapter.interactionRequests(id, 'open')
|
|
2809
|
-
: null;
|
|
2810
|
-
const durableGate = durableRows?.find((r) => r.kind === 'gate');
|
|
2811
|
-
if (durableGate !== undefined) {
|
|
2812
|
-
// This path builds the entry inline (not through `fold`), so it must run the SAME refusal
|
|
2813
|
-
// detection (issue #419) — otherwise a gate served from the durable row after a restart would
|
|
2814
|
-
// silently drop the warning that the live/replay paths carry.
|
|
2815
|
-
const refusal = detectRefusal(durableGate.prompt);
|
|
2816
|
-
const entry = {
|
|
2817
|
-
ord: durableGate.ord ?? 0,
|
|
2818
|
-
prompt: durableGate.prompt,
|
|
2819
|
-
lifecycle: 'open',
|
|
2820
|
-
receivedAt: new Date(durableGate.created_at).toISOString(),
|
|
2821
|
-
...(refusal !== undefined ? { refusal } : {}),
|
|
2822
|
-
};
|
|
2823
|
-
gateCache.adopt(id, entry);
|
|
2824
|
-
return { runId: id, ...entry };
|
|
2825
|
-
}
|
|
2826
|
-
const events = await adapter.runEvents(id);
|
|
2827
|
-
if (events === null) {
|
|
3166
|
+
// `interaction_requests`, written in the same transaction as the `awaiting_human` pause — so a
|
|
3167
|
+
// daemon restart reads it back instead of replaying the log; the replay stays the FALLBACK for
|
|
3168
|
+
// runs parked before the engine grew the table. `resolveOpenGate` is that one resolution, shared
|
|
3169
|
+
// with the POST route's `ord` check.
|
|
3170
|
+
const open = await resolveOpenGate(id);
|
|
3171
|
+
if (open === 'no-log') {
|
|
2828
3172
|
// Now — and only now — 503 is the honest answer: this run really is holding for a human, and
|
|
2829
3173
|
// this build cannot say what it is asking. Distinct cause, distinct message; answering "no
|
|
2830
3174
|
// gate" here would report a capability gap as a fact about the run (the FINDING-050 shape).
|
|
@@ -2832,13 +3176,12 @@ runtime = {}) {
|
|
|
2832
3176
|
error: 'Gate history is unavailable: this wicked-core build has no event-log read binding',
|
|
2833
3177
|
});
|
|
2834
3178
|
}
|
|
2835
|
-
|
|
2836
|
-
if (!replayed) {
|
|
3179
|
+
if (open === null) {
|
|
2837
3180
|
// Parked, with a history that records no open gate. Pre-log runs land here (their prompt is
|
|
2838
3181
|
// genuinely lost), as does a gate whose `awaitingHuman` predates the log's retention.
|
|
2839
3182
|
return reply.code(404).send({ error: 'No open gate for this run' });
|
|
2840
3183
|
}
|
|
2841
|
-
return { runId: id, ...
|
|
3184
|
+
return { runId: id, ...open };
|
|
2842
3185
|
});
|
|
2843
3186
|
// ── Elicitation (DES-002) ────────────────────────────────────────────────────
|
|
2844
3187
|
//
|
|
@@ -3128,6 +3471,74 @@ runtime = {}) {
|
|
|
3128
3471
|
const claims = await adapter.listConformanceClaims();
|
|
3129
3472
|
return { claims };
|
|
3130
3473
|
});
|
|
3474
|
+
// Replay THIS daemon's governance dead-letter outbox into its own store — the
|
|
3475
|
+
// `wicked-crew governance replay` CLI (crew#495) as a route, so a skin can offer the repair
|
|
3476
|
+
// beside the count it shows (`/diagnostics.governance.deadletters`). `dryRun: true` folds the
|
|
3477
|
+
// outbox and moves nothing, and says up front whether a real replay could run at all
|
|
3478
|
+
// (`blocker`); a real replay drains exactly as the CLI does (archive first, entries that fail
|
|
3479
|
+
// go back onto the live outbox and stay dead letters). One replay at a time per daemon.
|
|
3480
|
+
let deadletterReplayInFlight = false;
|
|
3481
|
+
app.post(`${V}/governance/deadletters/replay`, {
|
|
3482
|
+
config: {
|
|
3483
|
+
manifest: {
|
|
3484
|
+
requestType: 'GovernanceReplayBody',
|
|
3485
|
+
responseType: 'GovernanceReplayOutcome',
|
|
3486
|
+
statusCodes: [200, 400, 409, 500, 501],
|
|
3487
|
+
},
|
|
3488
|
+
},
|
|
3489
|
+
}, async (req, reply) => {
|
|
3490
|
+
const parsed = z.object({ dryRun: z.boolean().optional() }).strict().safeParse(req.body ?? {});
|
|
3491
|
+
if (!parsed.success)
|
|
3492
|
+
return reply.code(400).send({ error: parsed.error.message });
|
|
3493
|
+
const dryRun = parsed.data.dryRun === true;
|
|
3494
|
+
const store = adapter.governanceStore ?? null;
|
|
3495
|
+
if (store === null) {
|
|
3496
|
+
return reply.code(409).send({ error: 'this daemon resolved no governance store — there is nothing to replay into' });
|
|
3497
|
+
}
|
|
3498
|
+
const blocker = store.dbPath === ':memory:'
|
|
3499
|
+
? 'the governance store is in-memory — name a durable store before replaying'
|
|
3500
|
+
: !CoreAdapter.replayEmitOutboxSupported()
|
|
3501
|
+
? 'the installed engine cannot replay a dead-letter outbox — upgrade wicked-core-ts'
|
|
3502
|
+
: null;
|
|
3503
|
+
// Checked FIRST, for both modes: a running replay has renamed the live outbox to its archive,
|
|
3504
|
+
// so any read now would report a false zero while those dead letters are still in flight.
|
|
3505
|
+
if (deadletterReplayInFlight)
|
|
3506
|
+
return reply.code(409).send({ error: 'a dead-letter replay is already running' });
|
|
3507
|
+
const outbox = store.outboxPath;
|
|
3508
|
+
if (!existsSync(outbox)) {
|
|
3509
|
+
// No outbox = no dead letters: a zero outcome, never an error.
|
|
3510
|
+
const fold = await foldDeadletters(outbox);
|
|
3511
|
+
return {
|
|
3512
|
+
outbox, store: { path: store.displayPath, source: store.source }, archive: null,
|
|
3513
|
+
read: 0, replayed: 0, alreadyPresent: null, failed: 0, dryRun, note: null,
|
|
3514
|
+
...(dryRun ? { fold } : {}), blocker,
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
if (!dryRun && blocker !== null)
|
|
3518
|
+
return reply.code(blocker.startsWith('the installed engine') ? 501 : 409).send({ error: blocker });
|
|
3519
|
+
if (!dryRun)
|
|
3520
|
+
deadletterReplayInFlight = true;
|
|
3521
|
+
try {
|
|
3522
|
+
const { outcome } = await replayOutboxInto(outbox, store, { dryRun });
|
|
3523
|
+
if (!dryRun) {
|
|
3524
|
+
audit.record('governance.deadletters.replayed', actorOf(req), {
|
|
3525
|
+
detail: { read: outcome.read, replayed: outcome.replayed, failed: outcome.failed },
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
return { ...outcome, blocker: dryRun ? blocker : null };
|
|
3529
|
+
}
|
|
3530
|
+
catch (err) {
|
|
3531
|
+
if (err instanceof GovernanceReplayUnsupportedError)
|
|
3532
|
+
return reply.code(501).send({ error: message(err) });
|
|
3533
|
+
if (err instanceof UsageError)
|
|
3534
|
+
return reply.code(409).send({ error: message(err) });
|
|
3535
|
+
return reply.code(500).send({ error: message(err) });
|
|
3536
|
+
}
|
|
3537
|
+
finally {
|
|
3538
|
+
if (!dryRun)
|
|
3539
|
+
deadletterReplayInFlight = false;
|
|
3540
|
+
}
|
|
3541
|
+
});
|
|
3131
3542
|
// Normalize a `?repo=` query: Fastify parses a repeated `?repo=a&repo=b` into a `string[]` (take
|
|
3132
3543
|
// the first), and the value is TRIMMED so it's both validated and PASSED trimmed — a `?repo=%20x%20`
|
|
3133
3544
|
// must not pass the guard then miss the registry lookup on whitespace (Copilot #227).
|
|
@@ -3297,15 +3708,41 @@ runtime = {}) {
|
|
|
3297
3708
|
}
|
|
3298
3709
|
});
|
|
3299
3710
|
// ── Workflow viewer + builder (crew#44) ───────────────────────────────────
|
|
3711
|
+
// (crew#718) Start the engine's verdict pass at registration, so every SYNC reader of
|
|
3712
|
+
// `listWorkflows()` / `getWorkflow()` (the deliver resolver, the acceptance fold, the launch
|
|
3713
|
+
// lookup) sees the judged catalog from the first request rather than after the first catalog GET.
|
|
3714
|
+
// The pass is memoized: the handlers below await the same promise.
|
|
3715
|
+
// Guarded like `listWorkflows` at :1110 — the endpoint-manifest collector registers these routes
|
|
3716
|
+
// against a partial adapter that carries neither.
|
|
3717
|
+
if (typeof adapter.workflowCatalog === 'function') {
|
|
3718
|
+
void adapter.workflowCatalog().catch((err) => {
|
|
3719
|
+
app.log.warn(`wicked-crew: the workflow overlay could not be judged (${message(err)}); the catalog lists what parsed`);
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
// (crew#718) The catalog is what the ENGINE will honour, not what parses as `{id, phases[]}`.
|
|
3723
|
+
// A drop-in core refused at boot ("gate evaluates nothing: …") used to be listed here and
|
|
3724
|
+
// offered in studio's selector, and the launch then 400'd `unknown workflow` — the operator
|
|
3725
|
+
// learnt about it only from the failed launch. `workflowCatalog()` asks core's own validator:
|
|
3726
|
+
// the refused def leaves `workflows` (so it cannot be launched) and appears in `unavailable`
|
|
3727
|
+
// with core's verbatim reason (so its author is told why, and what to fix).
|
|
3300
3728
|
app.get(`${V}/workflows`, async () => {
|
|
3301
|
-
const workflows = adapter.
|
|
3302
|
-
return { workflows };
|
|
3729
|
+
const { workflows, unavailable } = await adapter.workflowCatalog();
|
|
3730
|
+
return { workflows, unavailable };
|
|
3303
3731
|
});
|
|
3304
3732
|
app.get(`${V}/workflows/:id`, async (req, reply) => {
|
|
3305
3733
|
const { id } = req.params;
|
|
3734
|
+
// (crew#718) JUDGE FIRST (review of PR #724, MEDIUM). `getWorkflow` only hydrates the overlay
|
|
3735
|
+
// dir; `workflowRefusal` is what runs the engine's verdict pass and drops the defs it refused.
|
|
3736
|
+
// Read the other way round, the FIRST request for a refused drop-in answered 200 with the def
|
|
3737
|
+
// the engine had thrown away — and this route is where the launch looks it up.
|
|
3738
|
+
const refusal = await adapter.workflowRefusal(id);
|
|
3739
|
+
if (refusal !== null) {
|
|
3740
|
+
return reply.code(404).send({ error: `workflow '${id}' was refused by the engine: ${refusal}`, reason: refusal });
|
|
3741
|
+
}
|
|
3306
3742
|
const workflow = adapter.getWorkflow(id);
|
|
3307
|
-
if (!workflow)
|
|
3743
|
+
if (!workflow) {
|
|
3308
3744
|
return reply.code(404).send({ error: `workflow '${id}' not found` });
|
|
3745
|
+
}
|
|
3309
3746
|
// humanGates: the phases that will PAUSE for a person even under humanConfirm:none (core#208).
|
|
3310
3747
|
// Surfaced so an operator can see a workflow's gates BEFORE launching it (FINDING-023).
|
|
3311
3748
|
return { workflow, humanGates: humanGatePhaseIds(workflow) };
|
|
@@ -3627,6 +4064,18 @@ runtime = {}) {
|
|
|
3627
4064
|
return reply.code(500).send({ error: message(err) });
|
|
3628
4065
|
}
|
|
3629
4066
|
});
|
|
4067
|
+
// R3 (ship-prove-3): what a delivering launch on this repo would push and where — read by the
|
|
4068
|
+
// SAME origin preflight the deliver gate card uses (crew#730), so the launch composer cannot
|
|
4069
|
+
// promise "opens a PR" on an origin that can never carry one. Read-only: one
|
|
4070
|
+
// `git remote get-url --push origin` in the registered checkout; nothing is pushed or probed.
|
|
4071
|
+
app.get(`${V}/repos/:id/deliver-target`, { config: { manifest: { responseType: 'DeliverTargetResponse', statusCodes: [200, 404] } } }, async (req, reply) => {
|
|
4072
|
+
const { id } = req.params;
|
|
4073
|
+
const repo = deliverRepoFor(await adapter.listRepos(), id);
|
|
4074
|
+
if (repo === undefined)
|
|
4075
|
+
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
4076
|
+
const originUrl = repo.root_path === '' ? null : await readDeliverOriginUrl(repo.root_path);
|
|
4077
|
+
return reply.send({ repo: repo.id, ...deliverTargetView(originUrl) });
|
|
4078
|
+
});
|
|
3630
4079
|
app.get(`${V}/repos/:id/git-history`, async (req, reply) => {
|
|
3631
4080
|
const { id } = req.params;
|
|
3632
4081
|
const repos = await adapter.listRepos();
|
|
@@ -3809,6 +4258,22 @@ runtime = {}) {
|
|
|
3809
4258
|
.send({ error: 'workerStallMaxEscalations must be an integer between 1 and 10' });
|
|
3810
4259
|
}
|
|
3811
4260
|
}
|
|
4261
|
+
// deliverIdentityLogin (crew#549): the login the deliver phase must push as. A typo here
|
|
4262
|
+
// would be an identity the script can never match — every delivery would refuse — so an
|
|
4263
|
+
// invalid value is a 400, never a silently-dropped key. `''` clears it (back to GH_ACCOUNT).
|
|
4264
|
+
// A LOGIN, never a token: the same validator the deliver script's splice uses.
|
|
4265
|
+
if (Object.hasOwn(patch, 'deliverIdentityLogin')) {
|
|
4266
|
+
const login = patch.deliverIdentityLogin;
|
|
4267
|
+
if (typeof login !== 'string' || (login !== '' && !isGitHubLogin(login.trim()))) {
|
|
4268
|
+
return reply.code(400).send({
|
|
4269
|
+
error: 'deliverIdentityLogin must be a GitHub login (letters, digits and single hyphens, '
|
|
4270
|
+
+ 'up to 39 characters), or "" to fall back to the GH_ACCOUNT environment variable. '
|
|
4271
|
+
+ 'It is a login, never a token — the credential stays in gh\'s keyring or GH_TOKEN.',
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
if (typeof login === 'string')
|
|
4275
|
+
patch.deliverIdentityLogin = login.trim();
|
|
4276
|
+
}
|
|
3812
4277
|
// deliverDefault (crew#393): the repo-scoped launch delivery default. Two values only —
|
|
3813
4278
|
// this knob decides whether completed code runs open PRs, so a typo must be a 400, never
|
|
3814
4279
|
// a silently-dropped key that leaves the operator believing they flipped it.
|
|
@@ -3880,6 +4345,7 @@ runtime = {}) {
|
|
|
3880
4345
|
'workerStallEscalateAction',
|
|
3881
4346
|
'workerStallMaxEscalations',
|
|
3882
4347
|
'deliverDefault',
|
|
4348
|
+
'deliverIdentityLogin',
|
|
3883
4349
|
'baseSkillRef',
|
|
3884
4350
|
'baseSkillPolicy',
|
|
3885
4351
|
];
|
|
@@ -3964,6 +4430,41 @@ runtime = {}) {
|
|
|
3964
4430
|
return estateUpstreamError(reply, err);
|
|
3965
4431
|
}
|
|
3966
4432
|
});
|
|
4433
|
+
// POST /proposals {content, project?, source?} → proposal.submit → 201 { id } (api-types 0.54.0).
|
|
4434
|
+
// A skin files ONE preference into the same review queue — studio's "takes": the person picks one
|
|
4435
|
+
// of two candidate outputs, and the pick becomes "prefers takes like X", learned only once they
|
|
4436
|
+
// accept it here. The kind (`memory`), tier and `capture: "preference"` marker are the daemon's; a
|
|
4437
|
+
// caller cannot file a policy through this door (the schema is strict). `project` becomes the
|
|
4438
|
+
// `project` facet, so the preference is recalled on that project only.
|
|
4439
|
+
app.post(`${V}/proposals`, {
|
|
4440
|
+
config: {
|
|
4441
|
+
manifest: { requestType: 'FileProposalBody', responseType: 'FileProposalResponse', statusCodes: [201, 400, 502] },
|
|
4442
|
+
},
|
|
4443
|
+
}, async (req, reply) => {
|
|
4444
|
+
const parsed = FileProposalSchema.safeParse(req.body ?? {});
|
|
4445
|
+
if (!parsed.success) {
|
|
4446
|
+
return reply.code(400).send(invalidBody(parsed.error, 'Invalid proposal body'));
|
|
4447
|
+
}
|
|
4448
|
+
const { content, project, source } = parsed.data;
|
|
4449
|
+
const payload = { content, tier: 'semantic', capture: 'preference' };
|
|
4450
|
+
if (source !== undefined)
|
|
4451
|
+
payload.source = source;
|
|
4452
|
+
let submitted;
|
|
4453
|
+
try {
|
|
4454
|
+
submitted = (await estateTool('proposal.submit', {
|
|
4455
|
+
kind_type: 'memory',
|
|
4456
|
+
payload,
|
|
4457
|
+
facets: project !== undefined ? { project } : {},
|
|
4458
|
+
}));
|
|
4459
|
+
}
|
|
4460
|
+
catch (err) {
|
|
4461
|
+
return estateUpstreamError(reply, err);
|
|
4462
|
+
}
|
|
4463
|
+
if (typeof submitted?.id !== 'string' || submitted.id === '') {
|
|
4464
|
+
return reply.code(502).send({ error: 'proposal.submit returned no id' });
|
|
4465
|
+
}
|
|
4466
|
+
return reply.code(201).send({ id: submitted.id });
|
|
4467
|
+
});
|
|
3967
4468
|
// POST /proposals/:id/approve → proposal.approve →
|
|
3968
4469
|
// { outcome:'promoted', active_id } — a MEMORY proposal, now an active memory (complete);
|
|
3969
4470
|
// { outcome:'handed_off', payload, landing } — a POLICY proposal, LANDED here as a steering rule.
|
|
@@ -3978,7 +4479,16 @@ runtime = {}) {
|
|
|
3978
4479
|
// 500, never the silent no-op that crew#388 exists to end — and `payload` is still returned for
|
|
3979
4480
|
// backward compatibility. The MEMORY path is UNTOUCHED: a `promoted` outcome returns immediately
|
|
3980
4481
|
// with no extra estate call and no landing.
|
|
3981
|
-
app.post(`${V}/proposals/:id/approve`, {
|
|
4482
|
+
app.post(`${V}/proposals/:id/approve`, {
|
|
4483
|
+
config: {
|
|
4484
|
+
manifest: {
|
|
4485
|
+
requestType: 'ApproveProposalBody',
|
|
4486
|
+
responseType: 'ApproveProposalResponse',
|
|
4487
|
+
// 404: an accept-with-edit naming no pending proposal; 409: one already in flight for it.
|
|
4488
|
+
statusCodes: [200, 400, 404, 409, 502],
|
|
4489
|
+
},
|
|
4490
|
+
},
|
|
4491
|
+
}, async (req, reply) => {
|
|
3982
4492
|
// Normalize ONCE and use the trimmed value throughout — an id like `%20pol1%20`
|
|
3983
4493
|
// decodes to a padded, non-empty string that would otherwise ride upstream as-is
|
|
3984
4494
|
// and into `proposal:<id>` derivations.
|
|
@@ -3986,6 +4496,24 @@ runtime = {}) {
|
|
|
3986
4496
|
if (id === '') {
|
|
3987
4497
|
return reply.code(400).send({ error: '`id` is required' });
|
|
3988
4498
|
}
|
|
4499
|
+
// An accept WITH an edit (capture review, api-types 0.49.0): the edited copy replaces the
|
|
4500
|
+
// original in the same queue — see `approveEdited`. No body / `{}` is the plain approve below.
|
|
4501
|
+
const edit = ApproveEditSchema.safeParse(req.body ?? {});
|
|
4502
|
+
if (!edit.success) {
|
|
4503
|
+
return reply.code(400).send(invalidBody(edit.error, 'Invalid approve body'));
|
|
4504
|
+
}
|
|
4505
|
+
if (edit.data.content !== undefined || edit.data.reach !== undefined) {
|
|
4506
|
+
try {
|
|
4507
|
+
const done = await approveEdited(estateTool, id, edit.data, (runId) => projects.index.projectOf(runId));
|
|
4508
|
+
return reply.code(done.status).send(done.body);
|
|
4509
|
+
}
|
|
4510
|
+
catch (err) {
|
|
4511
|
+
return estateUpstreamError(reply, err);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
if (editInFlight(id)) {
|
|
4515
|
+
return reply.code(409).send({ error: `proposal ${id} is already being accepted` });
|
|
4516
|
+
}
|
|
3989
4517
|
let approved;
|
|
3990
4518
|
try {
|
|
3991
4519
|
approved = (await estateTool('proposal.approve', { id }));
|
|
@@ -4056,11 +4584,16 @@ runtime = {}) {
|
|
|
4056
4584
|
return { outcome: 'handed_off', payload: approved.payload, landing };
|
|
4057
4585
|
});
|
|
4058
4586
|
// POST /proposals/:id/reject → proposal.reject → { ok: true }.
|
|
4059
|
-
app.post(`${V}/proposals/:id/reject`,
|
|
4587
|
+
app.post(`${V}/proposals/:id/reject`,
|
|
4588
|
+
// 409: an accept-with-edit of this proposal is in flight (api/capture.ts).
|
|
4589
|
+
{ config: { manifest: { responseType: 'RejectProposalResponse', statusCodes: [200, 400, 409, 502] } } }, async (req, reply) => {
|
|
4060
4590
|
const { id } = req.params;
|
|
4061
4591
|
if (id.trim() === '') {
|
|
4062
4592
|
return reply.code(400).send({ error: '`id` is required' });
|
|
4063
4593
|
}
|
|
4594
|
+
if (editInFlight(id.trim())) {
|
|
4595
|
+
return reply.code(409).send({ error: `proposal ${id.trim()} is being accepted` });
|
|
4596
|
+
}
|
|
4064
4597
|
try {
|
|
4065
4598
|
return (await estateTool('proposal.reject', { id }));
|
|
4066
4599
|
}
|
|
@@ -4086,10 +4619,12 @@ runtime = {}) {
|
|
|
4086
4619
|
// applied HERE as post-filters over that complete set (see below) — filtering, not retrieval.
|
|
4087
4620
|
// • list returns { memory_id, scope, content, tier, facets, created_at }; facets ride through to
|
|
4088
4621
|
// MemoryItem.facets, powering the surface's facet filter.
|
|
4089
|
-
// • RETIRE
|
|
4090
|
-
// hard-deletes EVERY memory whose scope equals or descends from
|
|
4091
|
-
// empty prefix (a total-wipe guard)
|
|
4092
|
-
//
|
|
4622
|
+
// • RETIRE comes in two widths. `POST /memory/retire { scope_prefix }` is SUBTREE-scoped:
|
|
4623
|
+
// `memory.erase {scope_prefix}` hard-deletes EVERY memory whose scope equals or descends from
|
|
4624
|
+
// the prefix, and refuses an empty prefix (a total-wipe guard) — the UI must show the operator
|
|
4625
|
+
// the subtree. `POST /memory/retire-item { memory_id }` (studio#206) removes exactly ONE memory
|
|
4626
|
+
// through `memory.erase {id}` (wicked-estate#188); an older estate refuses that call without
|
|
4627
|
+
// deleting anything, and the route answers 501 naming the upgrade — never a subtree fallback.
|
|
4093
4628
|
// GET /memory?query=&scope_prefix=&facets=<json>&limit= → memory.list + post-filter → { memories }.
|
|
4094
4629
|
app.get(`${V}/memory`, { config: { manifest: { responseType: 'ListMemoriesResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
|
|
4095
4630
|
const q = req.query;
|
|
@@ -4192,9 +4727,9 @@ runtime = {}) {
|
|
|
4192
4727
|
return estateUpstreamError(reply, err);
|
|
4193
4728
|
}
|
|
4194
4729
|
});
|
|
4195
|
-
// POST /memory/retire { scope_prefix } → memory.erase → { erased }. SUBTREE-scoped (
|
|
4196
|
-
// delete
|
|
4197
|
-
// same total-wipe guard estate enforces with -32602.
|
|
4730
|
+
// POST /memory/retire { scope_prefix } → memory.erase → { erased }. SUBTREE-scoped (the per-item
|
|
4731
|
+
// delete is /memory/retire-item below); an empty scope_prefix is refused at the route (a 400
|
|
4732
|
+
// BEFORE any spawn) — the same total-wipe guard estate enforces with -32602.
|
|
4198
4733
|
app.post(`${V}/memory/retire`, {
|
|
4199
4734
|
config: {
|
|
4200
4735
|
manifest: {
|
|
@@ -4211,7 +4746,7 @@ runtime = {}) {
|
|
|
4211
4746
|
const scopePrefix = parsed.data.scope_prefix.trim();
|
|
4212
4747
|
if (scopePrefix === '') {
|
|
4213
4748
|
return reply.code(400).send({
|
|
4214
|
-
error: '`scope_prefix` must not be empty —
|
|
4749
|
+
error: '`scope_prefix` must not be empty — this retire is subtree-scoped and refuses a total wipe (one memory: POST /memory/retire-item)',
|
|
4215
4750
|
});
|
|
4216
4751
|
}
|
|
4217
4752
|
try {
|
|
@@ -4227,8 +4762,64 @@ runtime = {}) {
|
|
|
4227
4762
|
return estateUpstreamError(reply, err);
|
|
4228
4763
|
}
|
|
4229
4764
|
});
|
|
4765
|
+
// POST /memory/retire-item { memory_id } → memory.erase { id } → { erased: 1 } (studio#206). Removes
|
|
4766
|
+
// exactly ONE memory: the id is the only target sent, so no reading of it can widen to a subtree.
|
|
4767
|
+
// 404 when estate holds no memory with that id (it erased 0). 501 when the estate predates
|
|
4768
|
+
// wicked-estate#188: it refuses an id-only erase for want of `scope_prefix`, deleting nothing.
|
|
4769
|
+
app.post(`${V}/memory/retire-item`, {
|
|
4770
|
+
config: {
|
|
4771
|
+
manifest: {
|
|
4772
|
+
requestType: 'RetireMemoryItemBody',
|
|
4773
|
+
responseType: 'RetireMemoryResponse',
|
|
4774
|
+
statusCodes: [200, 400, 404, 501, 502],
|
|
4775
|
+
},
|
|
4776
|
+
},
|
|
4777
|
+
}, async (req, reply) => {
|
|
4778
|
+
const parsed = RetireMemoryItemSchema.safeParse(req.body);
|
|
4779
|
+
if (!parsed.success) {
|
|
4780
|
+
return reply.code(400).send(invalidBody(parsed.error, 'Invalid retire request'));
|
|
4781
|
+
}
|
|
4782
|
+
const memoryId = parsed.data.memory_id.trim();
|
|
4783
|
+
if (memoryId === '')
|
|
4784
|
+
return reply.code(400).send({ error: '`memory_id` must not be empty' });
|
|
4785
|
+
try {
|
|
4786
|
+
const raw = await estateTool('memory.erase', { id: memoryId });
|
|
4787
|
+
const deleted = typeof raw === 'object' && raw !== null ? raw['deleted_count'] : undefined;
|
|
4788
|
+
if (typeof deleted !== 'number') {
|
|
4789
|
+
throw new EstateMcpError('memory.erase returned an unexpected shape');
|
|
4790
|
+
}
|
|
4791
|
+
if (deleted === 0)
|
|
4792
|
+
return reply.code(404).send({ error: `no memory ${memoryId}` });
|
|
4793
|
+
if (deleted !== 1) {
|
|
4794
|
+
// An id names one memory; any other count is estate misbehaving, and it is reported as such.
|
|
4795
|
+
return reply.code(502).send({ error: `memory.erase {id} removed ${deleted} memories, expected 1` });
|
|
4796
|
+
}
|
|
4797
|
+
const body = { erased: 1 };
|
|
4798
|
+
return body;
|
|
4799
|
+
}
|
|
4800
|
+
catch (err) {
|
|
4801
|
+
if (err instanceof EstateMcpError && err.code === -32602 && ESTATE_NO_ERASE_BY_ID.test(err.message)) {
|
|
4802
|
+
return reply.code(501).send({
|
|
4803
|
+
error: 'this wicked-estate cannot erase one memory by id (it needs wicked-estate#188); upgrade wicked-estate-mcp — nothing was deleted',
|
|
4804
|
+
code: 'estate_upgrade_required',
|
|
4805
|
+
});
|
|
4806
|
+
}
|
|
4807
|
+
return estateUpstreamError(reply, err);
|
|
4808
|
+
}
|
|
4809
|
+
});
|
|
4230
4810
|
// ── Projects (DES-PROJECT-001) — the 9-route experience-plane surface ────────
|
|
4231
4811
|
registerProjectRoutes(app, adapter, { ...projects, settings: projectSettings }, security);
|
|
4812
|
+
// ── Capture (Studio OS behaviour 8) — notes and photos → a project-filed run whose output is
|
|
4813
|
+
// proposals in the queue above; the launch is POST /runs itself (api/capture.ts).
|
|
4814
|
+
registerCaptureRoutes(app);
|
|
4815
|
+
// The Demo experience (studio#373): the demo preset's launch, view, files and script edit.
|
|
4816
|
+
registerDemoRoutes(app, adapter);
|
|
4817
|
+
// ── Presets (DES-TEAMING-002 §8.4, seam C2) — saved phase selections in the engine's store;
|
|
4818
|
+
// a launch names one via `workflow`, and the ENGINE resolves it.
|
|
4819
|
+
registerPresetRoutes(app, adapter, security);
|
|
4820
|
+
// ── Team (DES-TEAMING-002 T8) — reads and commands over the engine; the live stream is the
|
|
4821
|
+
// teamEvent relay (team/ws-relay.ts). Crew publishes no team fact (§4.0).
|
|
4822
|
+
registerTeamRoutes(app, adapter, security);
|
|
4232
4823
|
// ── Campaigns (crew#342 + TH-9) — the engine's durable Run-DAG scheduler over REST ──────────
|
|
4233
4824
|
// Progress streams as campaign* CoreEvents on the existing allowlist-free /ws relay; these
|
|
4234
4825
|
// routes are launch/resume/cancel + the reads a scoreboard builds from. The GET routes join
|
|
@@ -4246,6 +4837,7 @@ runtime = {}) {
|
|
|
4246
4837
|
// Wave 6 (F-7R2-014): the registered test sets ride beside the campaigns + groups.
|
|
4247
4838
|
testSets,
|
|
4248
4839
|
deliveryUrlFor: (runId) => deliveryIndex.urlFor(runId),
|
|
4840
|
+
deliveryPushedFor: (runId) => deliveryIndex.pushedFor(runId),
|
|
4249
4841
|
vacuity: vacuityProbes,
|
|
4250
4842
|
// A non-probe derivation throw in the rollup is a defect — error level, loud in diagnostics.
|
|
4251
4843
|
logDefect: (m) => app.log.error(m),
|
|
@@ -4299,6 +4891,24 @@ runtime = {}) {
|
|
|
4299
4891
|
audit,
|
|
4300
4892
|
actorOf,
|
|
4301
4893
|
});
|
|
4894
|
+
// ── MCP tools registry (DES-MCP-TOOLS-001 S2) — upstream servers, their tools, their secrets ─
|
|
4895
|
+
// Preview-bound saves, the tool diff on test, and a secret route that never answers the value.
|
|
4896
|
+
// S6: the policy preview and the approvals, over the same registry and the engine's rule store.
|
|
4897
|
+
const mcpPolicies = runtime.mcp !== undefined
|
|
4898
|
+
? new McpPolicies(runtime.mcp, {
|
|
4899
|
+
listRules: () => adapter.listConformanceRules(),
|
|
4900
|
+
upsertRule: (rule) => adapter.upsertConformanceRule(rule),
|
|
4901
|
+
previewCalls: (json) => adapter.previewMcpCalls(json),
|
|
4902
|
+
})
|
|
4903
|
+
: undefined;
|
|
4904
|
+
registerMcpRoutes(app, {
|
|
4905
|
+
...(runtime.mcp !== undefined ? { registry: runtime.mcp, toolLister: () => CoreAdapter.mcpToolLister() } : {}),
|
|
4906
|
+
...(runtime.mcpBroker !== undefined ? { broker: runtime.mcpBroker } : {}),
|
|
4907
|
+
...(mcpPolicies !== undefined ? { policies: mcpPolicies } : {}),
|
|
4908
|
+
...(runtime.mcpCalls !== undefined ? { usage: runtime.mcpCalls } : {}),
|
|
4909
|
+
audit,
|
|
4910
|
+
actorOf,
|
|
4911
|
+
});
|
|
4302
4912
|
// ── The wicked-interactive bridge, reverse-proxied (DES-MERGE-001 §5.3/§7.2) ──
|
|
4303
4913
|
// Mounted BESIDE the routes above and under the same `${V}` prefix, so it inherits one
|
|
4304
4914
|
// origin, one auth hook, and one CORS posture — the whole point of slice 1.
|
|
@@ -4349,5 +4959,6 @@ runtime = {}) {
|
|
|
4349
4959
|
pool: interactiveBridges,
|
|
4350
4960
|
log: (m) => app.log.warn(m),
|
|
4351
4961
|
});
|
|
4962
|
+
return { decideGate };
|
|
4352
4963
|
}
|
|
4353
4964
|
//# sourceMappingURL=routes.js.map
|