wicked-crew 0.7.39 → 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-scope.d.ts +37 -1
- package/dist/api/chat-scope.d.ts.map +1 -1
- package/dist/api/chat-scope.js +131 -7
- package/dist/api/chat-scope.js.map +1 -1
- 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 +647 -59
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +906 -146
- 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/run-timing-index.d.ts +10 -0
- package/dist/api/run-timing-index.d.ts.map +1 -1
- package/dist/api/run-timing-index.js +24 -0
- package/dist/api/run-timing-index.js.map +1 -1
- 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 +316 -116
- 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 +247 -14
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +638 -161
- 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/graph.d.ts +11 -0
- package/dist/projects/graph.d.ts.map +1 -1
- package/dist/projects/graph.js +12 -0
- package/dist/projects/graph.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/skills/phase-skill-gaps.d.ts +108 -0
- package/dist/skills/phase-skill-gaps.d.ts.map +1 -0
- package/dist/skills/phase-skill-gaps.js +199 -0
- package/dist/skills/phase-skill-gaps.js.map +1 -0
- package/dist/skills/runtime.d.ts +1 -1
- package/dist/skills/runtime.d.ts.map +1 -1
- package/dist/skills/runtime.js +2 -1
- package/dist/skills/runtime.js.map +1 -1
- 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 +3307 -897
- 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-BRDAN35i.js +0 -548
- package/dist/studio/assets/index-Drfcsy8b.css +0 -32
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';
|
|
@@ -20,42 +22,54 @@ import { applyWorkerConfigRoot, signedInHeuristic } from './seat-signin.js';
|
|
|
20
22
|
import { chatSeatAdmission } from './seat-standing.js';
|
|
21
23
|
import { rosterWithStandingFactory } from './roster-standing.js';
|
|
22
24
|
import { ChatTurnIndex } from './chat-turns.js';
|
|
23
|
-
import { ChatScopeIndex, chatScopeDeps, prepareChatScratch, removeChatScratch, resolveChatScope, } from './chat-scope.js';
|
|
25
|
+
import { ChatScopeIndex, chatScopeDeps, prepareChatScratch, removeChatScratch, namedKindRefusal, resolveChatScope, } from './chat-scope.js';
|
|
24
26
|
import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-path.js';
|
|
25
27
|
import { InvalidDiffBaseError, MERGE_BASE_LITERAL, NotARegularFileError, UnresolvableDiffBaseError, branchDiff, isPlainRef, readFileCapped, worktreeDiff, } from './run-files.js';
|
|
26
28
|
import { DocRunIndex } from '../interactive/doc-run-index.js';
|
|
27
29
|
import { listInteractiveDocs } from '../interactive/docs-index.js';
|
|
28
30
|
import { TestSetIndex } from '../qe/test-sets.js';
|
|
29
|
-
import { estateExe, resolveProjectGraphBinding } from '../projects/graph.js';
|
|
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';
|
|
46
|
+
import { phaseSkillFindings, withPhaseSkillGaps } from '../skills/phase-skill-gaps.js';
|
|
38
47
|
import { STATE_HOME_BLOCKER_CODE, STATE_HOME_REMEDY, isEngineStateHomeRefusal, stateHomeBlockerBody, } from '../projects/state-home-preflight.js';
|
|
39
48
|
import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../skills/base-skill.js';
|
|
40
49
|
import { noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
|
|
41
50
|
import { ProjectSettingsStore } from '../projects/settings.js';
|
|
42
51
|
import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
|
|
43
|
-
import { composeDeliverText, factsFromRun, framedDeliverText, runUrlFor } from '../core/deliver-text.js';
|
|
44
|
-
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';
|
|
45
54
|
import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
|
|
46
55
|
import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
|
|
47
56
|
import { registerInteractiveDocList } from '../interactive/doc-list-routes.js';
|
|
48
57
|
import { MembershipIndex } from '../projects/membership-index.js';
|
|
49
58
|
import { MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
|
|
59
|
+
import { resolveLinkedIssues } from '../core/linked-issues.js';
|
|
50
60
|
import { AuditLog } from './audit.js';
|
|
51
61
|
import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
|
|
52
62
|
import { GovernanceDiagnostics } from './governance-health.js';
|
|
53
63
|
import { legacyHomeOutboxPath } from '../core/governance-store.js';
|
|
64
|
+
import { UsageError, replayOutboxInto } from '../cli/governance.js';
|
|
65
|
+
import { foldDeadletters } from './governance-health.js';
|
|
54
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';
|
|
55
69
|
import { GroupIndex } from './group-index.js';
|
|
56
70
|
import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
|
|
57
71
|
import { GuidanceIndex } from './guidance-index.js';
|
|
58
|
-
import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded,
|
|
72
|
+
import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, deliveryRecordFrom, pushedState, canDeliverResolver, } from './delivery-index.js';
|
|
59
73
|
import { DeliveryDerivationCache } from './delivery-cache.js';
|
|
60
74
|
import { gitReprovisionWorktree, runDeliverScript, } from './post-hoc-deliver.js';
|
|
61
75
|
import { LOCAL_ACTOR } from './auth.js';
|
|
@@ -64,6 +78,10 @@ import { LOCAL_ACTOR } from './auth.js';
|
|
|
64
78
|
export { API_PREFIX } from './api-prefix.js';
|
|
65
79
|
import { API_PREFIX } from './api-prefix.js';
|
|
66
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';
|
|
67
85
|
// Daemon version reported by /health — read from package.json so it never drifts
|
|
68
86
|
// from the shipped version across releases. Resolves the package root from the
|
|
69
87
|
// compiled module location (dist/api/routes.js → ../../package.json) and works the
|
|
@@ -83,17 +101,10 @@ const STATUS_ORDER = {
|
|
|
83
101
|
function sortActionableFirst(views) {
|
|
84
102
|
return [...views].sort((a, b) => (STATUS_ORDER[a.session.status] ?? 9) - (STATUS_ORDER[b.session.status] ?? 9));
|
|
85
103
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
*/
|
|
91
|
-
export function parseEstateTotals(stdout) {
|
|
92
|
-
const m = /^nodes=(\d+) edges=(\d+) files=(\d+)/m.exec(stdout);
|
|
93
|
-
if (m === null)
|
|
94
|
-
return undefined;
|
|
95
|
-
return { nodes: Number(m[1]), edges: Number(m[2]), files: Number(m[3]) };
|
|
96
|
-
}
|
|
104
|
+
// parseEstateTotals lives in projects/graph.ts — shared with chat-scope.ts (crew#642) to avoid
|
|
105
|
+
// a circular import (routes.ts → chat-scope.ts). Re-exported here for backward compat
|
|
106
|
+
// (tests import it from routes.js; see top-of-file import for local use).
|
|
107
|
+
export { parseEstateTotals };
|
|
97
108
|
function codeGraphDbOr503(repo, reply) {
|
|
98
109
|
try {
|
|
99
110
|
return codeGraphDb(repo);
|
|
@@ -312,6 +323,12 @@ const RegisterRepoSchema = z
|
|
|
312
323
|
// defaults to ~/.wicked/repos/<name>.
|
|
313
324
|
rootPath: z.string().optional(),
|
|
314
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(),
|
|
315
332
|
})
|
|
316
333
|
.strict()
|
|
317
334
|
.refine((d) => {
|
|
@@ -321,6 +338,14 @@ const RegisterRepoSchema = z
|
|
|
321
338
|
// or rootPath alone (register existing local repo) — all valid.
|
|
322
339
|
return hasRemote || hasLocal;
|
|
323
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();
|
|
324
349
|
/**
|
|
325
350
|
* The launch body. Every field but `problem` is optional, which is what made stripping dangerous:
|
|
326
351
|
* omitting one is a legitimate request that gets an engine default, so a MISSPELLED one was
|
|
@@ -337,6 +362,7 @@ const RegisterRepoSchema = z
|
|
|
337
362
|
// The request-body schemas below are exported so `tests/wire-contract.test.ts` can prove, at
|
|
338
363
|
// compile time, that every body the published contract (`wicked-crew-api-types`) lets a client
|
|
339
364
|
// send is a body these schemas accept — the request-direction half of the drift guard (task #84).
|
|
365
|
+
export { PlanSchema };
|
|
340
366
|
export const LaunchSchema = z.object({
|
|
341
367
|
problem: z.string().min(1),
|
|
342
368
|
sessionId: z.string().min(1).optional(),
|
|
@@ -345,6 +371,10 @@ export const LaunchSchema = z.object({
|
|
|
345
371
|
humanConfirm: z.string().min(1).optional(),
|
|
346
372
|
repoRef: z.string().min(1).optional(),
|
|
347
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(),
|
|
348
378
|
/** DES-PROJECT-001 §2.2 — file the run into a project; membership attaches atomically with
|
|
349
379
|
* the launch record. Unknown/archived ⇒ the launch fails (never a silent unfiled run). */
|
|
350
380
|
projectId: z.string().min(1).optional(),
|
|
@@ -395,7 +425,10 @@ export const LaunchSchema = z.object({
|
|
|
395
425
|
* launch (e.g. a CI job name or a studio tab id). Persisted on the `run.launched` audit entry
|
|
396
426
|
* (`detail.actor`) and served on the run DTO; absent = caller omitted it. */
|
|
397
427
|
actor: z.string().min(1).max(256).optional(),
|
|
398
|
-
}).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, {
|
|
399
432
|
message: 'deliver: "pr" requires a workflow — a free-text run has no def to append the deliver phase to',
|
|
400
433
|
path: ['deliver'],
|
|
401
434
|
}).refine((b) => b.deliverGate === undefined || b.deliver !== 'none', {
|
|
@@ -414,19 +447,43 @@ export const LaunchSchema = z.object({
|
|
|
414
447
|
message: 'revisesPr needs deliver: "pr" — a launch that declines delivery has no phase to push the revision with',
|
|
415
448
|
path: ['revisesPr'],
|
|
416
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);
|
|
417
458
|
/** `POST /runs/:id/gate` (api-types 0.38.0 `GateDecision`; DES-L1 PR-2). Additive arms: `action`
|
|
418
459
|
* names the arm (`approve` | `request_changes` | `reject`; absent = today's two-arm mapping of
|
|
419
460
|
* `approve`), `amendScope` says where an approve's `amend` lands (`cursor` = the gated unit, today's
|
|
420
461
|
* behaviour | `creator` = the first creator phase at/after the cursor — an intake steer reaches the
|
|
421
462
|
* phase that implements). `request_changes` sends a NOT-PASS review back to the creator with the
|
|
422
463
|
* findings in context (`amend` = the operator's note). A disagreement between `action` and
|
|
423
|
-
* `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. */
|
|
424
471
|
export const GateSchema = z.object({
|
|
425
472
|
approve: z.boolean(),
|
|
426
473
|
amend: z.string().optional(),
|
|
427
|
-
action: z.enum(['approve', 'request_changes', 'reject']).optional(),
|
|
474
|
+
action: z.enum(['approve', 'request_changes', 'reject', 'edit_plan', 'amend_intent', ...ESCALATION_ACTIONS]).optional(),
|
|
428
475
|
amendScope: z.enum(['cursor', 'creator']).optional(),
|
|
429
|
-
|
|
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'] });
|
|
430
487
|
/** `PUT /runs/:id/guidance` (DES-UX-002 §7.2, CREW-UX-7) — the durable pre-gate note body.
|
|
431
488
|
* The empty string is a legal body: it CLEARS the note. The byte cap is checked in the route
|
|
432
489
|
* (not zod's char-counting `max`) so the 400 names the actual limit. */
|
|
@@ -470,6 +527,14 @@ export const OpenPathSchema = z.object({
|
|
|
470
527
|
export const RetireMemorySchema = z.object({
|
|
471
528
|
scope_prefix: z.string(),
|
|
472
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;
|
|
473
538
|
/**
|
|
474
539
|
* A SKIN-OWNED settings key (crew#323): one lowercase segment under the `studio.` namespace,
|
|
475
540
|
* e.g. `studio.appearance`, `studio.notifications`. The daemon never interprets these values —
|
|
@@ -526,12 +591,89 @@ export const ChatOpenSchema = z.object({
|
|
|
526
591
|
/** File the chat into a project (`crew.chat` membership) — and, with no `repoRefs`, scope it to
|
|
527
592
|
* every registered `crew.repo` member of that project (crew#502). */
|
|
528
593
|
projectId: z.string().min(1).optional(),
|
|
594
|
+
/** The scope the caller NAMES (studio#323 R4) — omitted ⇒ the legacy inference. Shape rules
|
|
595
|
+
* (which kinds need / refuse `projectId` / `repoRefs`) are the resolver's (`chat-scope.ts`). */
|
|
596
|
+
scopeKind: z.enum(['system', 'everything', 'project', 'repo', 'repos', 'none']).optional(),
|
|
529
597
|
}).strict();
|
|
598
|
+
/**
|
|
599
|
+
* The `singleSeat` disclosure (crew#641, hole closed in crew#650) — built in ONE place so the 201
|
|
600
|
+
* and the 202 cannot drift.
|
|
601
|
+
*
|
|
602
|
+
* The property this field states is **"this chat has one seat; it cannot disagree with itself"**,
|
|
603
|
+
* and that is true of every one-warm-seat chat. The first cut also required `refused.length > 0`,
|
|
604
|
+
* so the case an operator is most likely to create — a chat deliberately opened with a single seat
|
|
605
|
+
* — returned `ok: true` with no signal at all, which is the exact failure mode #641 was filed
|
|
606
|
+
* about. It also made the 202 go quiet after a restart, when `refusedOf` no longer knows the
|
|
607
|
+
* refusals, even though the chat is just as single-seated as it was a minute earlier.
|
|
608
|
+
*
|
|
609
|
+
* `refused` therefore rides along as EVIDENCE when there is any — it never decides whether the
|
|
610
|
+
* caller is told.
|
|
611
|
+
*
|
|
612
|
+
* THREE states, not two (review of #658). The refusal list has a third possibility that `[]` cannot
|
|
613
|
+
* express: this daemon does not KNOW. `chatScopes.refusedOf` answers `undefined` for a chat this
|
|
614
|
+
* daemon did not open — the engine keeps the warm session across a restart, the in-memory index does
|
|
615
|
+
* not — and the first cut collapsed that to `[]` with `?? []`, which made the 202 tell an operator
|
|
616
|
+
* *"It was opened with a single seat — no other seat was refused"* about a chat that was opened with
|
|
617
|
+
* two seats, one of which was refused. Both halves false, stated confidently. It is the same
|
|
618
|
+
* distinction #651 got right for chat-promotion provenance (`.catch(() => null)`, never `[]`):
|
|
619
|
+
* unreadable and empty are different answers, and only one of them may be asserted.
|
|
620
|
+
*
|
|
621
|
+
* TWO MOMENTS, and only one route witnessed each (third review of #658). `refused` describes the
|
|
622
|
+
* daemon's record — what the OPEN (or the last re-seat) observed; `warmRoster` describes NOW. Any
|
|
623
|
+
* sentence built from both fabricates the instant they diverge, and they diverge routinely: a seat
|
|
624
|
+
* that blows its turn budget is RELEASED by the engine (`chat-turns.ts`), a re-seat clears an entry
|
|
625
|
+
* from the record, a restart drops the record entirely. A chat opened with `['claude','opencode']`
|
|
626
|
+
* — both warm, nothing refused, nothing unknown — whose `opencode` is later released would have been
|
|
627
|
+
* told *"It was opened with a single seat — no other seat was refused"*: false in both halves, with
|
|
628
|
+
* no `undefined` anywhere near it. So `at` names the moment the caller is asking about:
|
|
629
|
+
*
|
|
630
|
+
* • `'open'` — the 201. This route just performed the open, so it is the ONLY one with standing to
|
|
631
|
+
* describe it: refusals known and empty ⇒ it may say the chat was opened with a single seat.
|
|
632
|
+
* • `'turn'` — the 202. It knows the roster NOW and the refusal record; it knows nothing about how
|
|
633
|
+
* the chat came to look like this. Refusals known and empty ⇒ it says NOTHING about opening.
|
|
634
|
+
* It still names refusals that are on record, and still says so when the record is unavailable —
|
|
635
|
+
* both are statements about the record it holds, not about a moment it did not see.
|
|
636
|
+
*
|
|
637
|
+
* `refusalsKnown` carries the known/unknown distinction for a machine reader, because `refused: []`
|
|
638
|
+
* is exactly as ambiguous on the wire as the sentence was in prose.
|
|
639
|
+
*/
|
|
640
|
+
function singleSeatDisclosure(warmed,
|
|
641
|
+
/** `undefined` = this daemon has no refusal record for the chat (it did not open it). */
|
|
642
|
+
refused,
|
|
643
|
+
/** Which moment the caller is asking about — see the note above; only `'open'` may describe one. */
|
|
644
|
+
at) {
|
|
645
|
+
const known = refused !== undefined;
|
|
646
|
+
const list = refused ?? [];
|
|
647
|
+
const refusalClause = list.length > 0
|
|
648
|
+
? `Refused: ${list.map((r) => `${r.cliKey} (${r.reason})`).join('; ')}. `
|
|
649
|
+
: known
|
|
650
|
+
? at === 'open'
|
|
651
|
+
? 'It was opened with a single seat — no other seat was refused. '
|
|
652
|
+
: // A turn has no standing to say how the chat was opened: this seat may be the survivor of
|
|
653
|
+
// a two-seat chat whose other seat was released mid-session. Say nothing.
|
|
654
|
+
''
|
|
655
|
+
: 'This daemon holds no refusal record for this chat — it was opened before a restart — so ' +
|
|
656
|
+
'whether another seat was refused is UNKNOWN: the empty refused[] means the record is gone, ' +
|
|
657
|
+
'not that nothing was refused. ';
|
|
658
|
+
return {
|
|
659
|
+
degraded: true,
|
|
660
|
+
warmed,
|
|
661
|
+
refused: list,
|
|
662
|
+
refusalsKnown: known,
|
|
663
|
+
message:
|
|
664
|
+
// "warm" on a turn: the claim is about the roster as it stands, not about how it got there.
|
|
665
|
+
`This chat has one ${at === 'open' ? 'seat' : 'warm seat'} (${warmed}); it cannot disagree with itself. ` +
|
|
666
|
+
refusalClause +
|
|
667
|
+
'The single-seat root cause is tracked as wicked-core#563; this chat makes it visible.',
|
|
668
|
+
};
|
|
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
|
+
}
|
|
530
676
|
export function registerRoutes(app, adapter, gateCache, elicitationCache,
|
|
531
|
-
// Defaulted so a caller that never arms the bus seam (tests drive this
|
|
532
|
-
// function directly) gets the same behavior as an unarmed daemon: an empty
|
|
533
|
-
// cache, `busEvent: null`, and the lazy ledger read doing all the work.
|
|
534
|
-
qeGateEvents = new QeGateCache(),
|
|
535
677
|
// Defaulted for the same reason: tests that drive this function directly get
|
|
536
678
|
// the projects surface with no bus (events skipped) and a fresh index.
|
|
537
679
|
projects = { bus: null, index: new MembershipIndex(), log: () => undefined },
|
|
@@ -545,6 +687,8 @@ security = { audit: AuditLog.noop(), authMode: 'off' },
|
|
|
545
687
|
runtime = {}) {
|
|
546
688
|
const { audit } = security;
|
|
547
689
|
const seatHealth = runtime.seatHealth ?? new SeatHealthTracker();
|
|
690
|
+
const deliveryFreeze = runtime.deliveryFreeze ?? new DeliveryFreeze();
|
|
691
|
+
registerDeliveryFreezeRoutes(app, { freeze: deliveryFreeze, audit });
|
|
548
692
|
const openWithOs = runtime.openWithOs ?? openWithSystemDefault;
|
|
549
693
|
const signedIn = runtime.signedIn ?? signedInHeuristic;
|
|
550
694
|
const retryIndex = runtime.retryIndex ?? new RetryIndex();
|
|
@@ -582,7 +726,7 @@ runtime = {}) {
|
|
|
582
726
|
new DeliveryDerivationCache({
|
|
583
727
|
listViews: () => adapter.sessionsDetail(),
|
|
584
728
|
probes: vacuityProbes,
|
|
585
|
-
isDelivered: (runId) => deliveryIndex.
|
|
729
|
+
isDelivered: (runId) => deliveryIndex.isDelivered(runId),
|
|
586
730
|
canDeliver,
|
|
587
731
|
});
|
|
588
732
|
const deliverExec = runtime.deliverExec ?? runDeliverScript;
|
|
@@ -601,6 +745,32 @@ runtime = {}) {
|
|
|
601
745
|
const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
|
|
602
746
|
/** DES-L9: PR number → head branch, through `gh pr view` unless the runtime injected an answerer. */
|
|
603
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
|
+
};
|
|
604
774
|
// The run-DTO joins (DES-UX-001 §8.2/§8.3, DES-UX-002 §7.2): `project_id` from the membership
|
|
605
775
|
// record — `null` = genuinely unfiled, so the field is ALWAYS present on served runs —
|
|
606
776
|
// `retry_of` from the lineage index and `guidance` from the guidance index, each set only
|
|
@@ -648,12 +818,19 @@ runtime = {}) {
|
|
|
648
818
|
const url = deliveryIndex.urlFor(view.session.id);
|
|
649
819
|
if (url !== undefined)
|
|
650
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);
|
|
651
825
|
if (conflictStrand)
|
|
652
826
|
return { delivery: 'stranded' };
|
|
653
827
|
return deliveryCache.read(view);
|
|
654
828
|
};
|
|
655
829
|
const decorateRun = (view) => {
|
|
656
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));
|
|
657
834
|
view.session.project_id = projects.index.projectOf(view.session.id) ?? null;
|
|
658
835
|
// Wave 6 (F-4R2-006): the interactive document this run answered, from the seams' handoff
|
|
659
836
|
// ledgers — `null` = genuinely not a document run, so the field is ALWAYS present on served runs.
|
|
@@ -673,6 +850,10 @@ runtime = {}) {
|
|
|
673
850
|
const guidance = guidanceIndex.guidanceFor(view.session.id);
|
|
674
851
|
if (guidance !== undefined)
|
|
675
852
|
view.session.guidance = guidance;
|
|
853
|
+
// crew#661: the declared skills this run's phases ran WITHOUT (degrade-and-disclose) — ABSENT when none.
|
|
854
|
+
const skillGaps = runtime.runSkillGaps?.gapsFor(view.session.id);
|
|
855
|
+
if (skillGaps !== undefined)
|
|
856
|
+
view.session.skill_gaps = skillGaps;
|
|
676
857
|
// Run launch time (home command-center run metrics): unix SECONDS from the `run.launched`
|
|
677
858
|
// audit entry, ABSENT when the daemon has no launch record for this run (onboarding/campaign
|
|
678
859
|
// runs launched off POST /runs, pre-field runs) — never fabricated, so a bucketed KPI can
|
|
@@ -696,6 +877,17 @@ runtime = {}) {
|
|
|
696
877
|
view.session.delivery = state.delivery;
|
|
697
878
|
if (state.deliverUrl !== undefined)
|
|
698
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;
|
|
884
|
+
// crew#641/#642 (item 5): chat promotion provenance — ABSENT when not a chat-promoted run.
|
|
885
|
+
const chatSeatCount = runTimingIndex.chatSeatCountFor(view.session.id);
|
|
886
|
+
if (chatSeatCount !== undefined)
|
|
887
|
+
view.session.chat_seat_count = chatSeatCount;
|
|
888
|
+
const chatGrounded = runTimingIndex.chatGroundedFor(view.session.id);
|
|
889
|
+
if (chatGrounded !== undefined)
|
|
890
|
+
view.session.chat_grounded = chatGrounded;
|
|
699
891
|
return view;
|
|
700
892
|
};
|
|
701
893
|
// Resolved ONCE and shared by the project routes (which read/write `interactiveRoot`) and the
|
|
@@ -707,13 +899,31 @@ runtime = {}) {
|
|
|
707
899
|
// still gets the ONE actor shape via this accessor.
|
|
708
900
|
const actorOf = (req) => req.actor ?? LOCAL_ACTOR;
|
|
709
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
|
+
};
|
|
710
920
|
// `config.manifest` on the routes below (TH-11): the declaration channel the endpoint manifest
|
|
711
921
|
// reads — type names bind to `wicked-crew-api-types` exports where one exists, structural
|
|
712
922
|
// spellings where the contract has no name, statusCodes list every code the route answers on
|
|
713
923
|
// purpose. Declared on the highest-traffic run-lifecycle routes first; the manifest records
|
|
714
924
|
// null / [] for the rest ("where declared", never invented). See src/api/endpoint-manifest.ts.
|
|
715
925
|
app.get(`${V}/health`, { config: { manifest: { statusCodes: [200] } } }, async () => {
|
|
716
|
-
const ping = await
|
|
926
|
+
const ping = await enginePing();
|
|
717
927
|
// F-E2E-030: what this deployment can keep. A composer reads `capabilities.deliverGate`
|
|
718
928
|
// before it promises "pauses at the deliver gate"; a stub-driven route set without the
|
|
719
929
|
// probe honestly reports no gate.
|
|
@@ -735,9 +945,18 @@ runtime = {}) {
|
|
|
735
945
|
// `baseSkill.finding` and `/diagnostics.skills.findings[]` (one surface, no new field;
|
|
736
946
|
// `HealthWarning.kind` is open); status stays ok because the daemon serves and studio must load
|
|
737
947
|
// and show it — exactly the state-home blocker's precedent above.
|
|
948
|
+
// crew#661: a drafting seam that armed WITHOUT its declared skill rides `warnings` as well — its
|
|
949
|
+
// runs proceed degraded, so "status ok, no warnings" would be the silent answer the issue names.
|
|
738
950
|
const warnings = [
|
|
739
951
|
...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
|
|
740
952
|
...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
|
|
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)] : []),
|
|
741
960
|
];
|
|
742
961
|
return {
|
|
743
962
|
status: 'ok',
|
|
@@ -790,7 +1009,9 @@ runtime = {}) {
|
|
|
790
1009
|
// The skills seam's last outcome (skills keystone): published / fallback / blocked /
|
|
791
1010
|
// config-error, with the `skills.*` findings the ladder produced — the operator's one
|
|
792
1011
|
// read-only answer to "why do launches refuse the snapshot".
|
|
793
|
-
|
|
1012
|
+
// crew#661: plus every subsystem whose phases ARMED without a declared skill (`phaseSkillGaps`,
|
|
1013
|
+
// one `skills.phase-skill` finding each) — the degraded runs' operator-facing record.
|
|
1014
|
+
skills: withPhaseSkillGaps(runtime.skills?.health() ?? disabledSkillsHealth(), runtime.phaseSkills),
|
|
794
1015
|
// Is the governance evidence LANDING (crew#495): the store, the records on it, the dead
|
|
795
1016
|
// letters — with a `governance.deadletter` finding the moment the outbox holds one.
|
|
796
1017
|
governance,
|
|
@@ -809,8 +1030,10 @@ runtime = {}) {
|
|
|
809
1030
|
authMode: security.authMode,
|
|
810
1031
|
}));
|
|
811
1032
|
// The actor audit trail (task #88): who launched/steered/governed what.
|
|
812
|
-
// Read-only (observer trust); newest first; `?runId=` / `?action=` / `?limit=`.
|
|
813
|
-
|
|
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) => {
|
|
814
1037
|
const q = req.query;
|
|
815
1038
|
const first = (v) => (Array.isArray(v) ? v[0] : v)?.trim() || undefined;
|
|
816
1039
|
const runId = first(q.runId);
|
|
@@ -822,11 +1045,18 @@ runtime = {}) {
|
|
|
822
1045
|
if (limitRaw !== undefined && (!Number.isFinite(limit) || limit < 1 || !Number.isInteger(limit))) {
|
|
823
1046
|
return reply.code(400).send({ error: '`limit` must be a positive integer' });
|
|
824
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
|
+
}
|
|
825
1054
|
try {
|
|
826
1055
|
const entries = await audit.read({
|
|
827
1056
|
...(runId !== undefined ? { runId } : {}),
|
|
828
1057
|
...(action !== undefined ? { action } : {}),
|
|
829
1058
|
...(limit !== undefined ? { limit } : {}),
|
|
1059
|
+
...(since !== undefined ? { since } : {}),
|
|
830
1060
|
});
|
|
831
1061
|
return { entries };
|
|
832
1062
|
}
|
|
@@ -851,6 +1081,33 @@ runtime = {}) {
|
|
|
851
1081
|
// disagree about a seat.
|
|
852
1082
|
const rosterWithStanding = runtime.rosterWithStanding ?? rosterWithStandingFactory({ seatHealth, signedIn });
|
|
853
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
|
+
});
|
|
854
1111
|
// Open a file/folder with the OS default application (crew#273) — the studio Files tab's
|
|
855
1112
|
// click-to-open. The open MUST happen daemon-side (the SPA cannot spawn a process), which is
|
|
856
1113
|
// why the path is validated first: absolute, and inside one of the caller-visible roots — the
|
|
@@ -1087,33 +1344,61 @@ runtime = {}) {
|
|
|
1087
1344
|
});
|
|
1088
1345
|
// Registered repos → target-repo picker.
|
|
1089
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
|
+
};
|
|
1090
1372
|
app.post(`${V}/repos`, async (req, reply) => {
|
|
1091
1373
|
const parsed = RegisterRepoSchema.safeParse(req.body);
|
|
1092
1374
|
if (!parsed.success) {
|
|
1093
1375
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
1094
1376
|
}
|
|
1095
|
-
const { name, rootPath, gitUrl } = parsed.data;
|
|
1377
|
+
const { name, rootPath, gitUrl, projectId, channel, actor } = parsed.data;
|
|
1378
|
+
let repo;
|
|
1379
|
+
let runId;
|
|
1096
1380
|
try {
|
|
1097
1381
|
if (gitUrl) {
|
|
1098
1382
|
// Remote: clone to rootPath (if provided) or default ~/.wicked/repos/<name>,
|
|
1099
1383
|
// register, and launch onboarding run.
|
|
1100
|
-
const
|
|
1384
|
+
const cloned = await adapter.cloneAndRegisterRepo(name, gitUrl, rootPath);
|
|
1385
|
+
runId = cloned.runId;
|
|
1101
1386
|
const repos = await adapter.listRepos();
|
|
1102
|
-
|
|
1387
|
+
repo = repos.find((r) => r.id === cloned.repoId);
|
|
1103
1388
|
if (!repo)
|
|
1104
1389
|
return reply.code(500).send({ error: 'Repo registered but could not be retrieved' });
|
|
1105
|
-
return reply.code(201).send({ repo, onboardRunId: runId });
|
|
1106
1390
|
}
|
|
1107
1391
|
else {
|
|
1108
1392
|
// Local: register then launch onboarding run.
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
return reply.code(201).send({ repo, onboardRunId: runId });
|
|
1393
|
+
repo = await adapter.registerRepo(name, rootPath);
|
|
1394
|
+
runId = await adapter.launchOnboardingRun(repo.id, name);
|
|
1112
1395
|
}
|
|
1113
1396
|
}
|
|
1114
1397
|
catch (err) {
|
|
1115
1398
|
return reply.code(400).send({ error: message(err) });
|
|
1116
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 } : {}) });
|
|
1117
1402
|
});
|
|
1118
1403
|
// Return the onboarding run id for a repo (so the UI can navigate to it).
|
|
1119
1404
|
app.get(`${V}/repos/:id/onboard`, async (req, reply) => {
|
|
@@ -1128,13 +1413,18 @@ runtime = {}) {
|
|
|
1128
1413
|
const repo = repos.find((r) => r.id === id);
|
|
1129
1414
|
if (!repo)
|
|
1130
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;
|
|
1131
1420
|
try {
|
|
1132
|
-
|
|
1133
|
-
return reply.code(201).send({ runId });
|
|
1421
|
+
runId = await adapter.launchOnboardingRun(repo.id, repo.name);
|
|
1134
1422
|
}
|
|
1135
1423
|
catch (err) {
|
|
1136
1424
|
return reply.code(400).send({ error: message(err) });
|
|
1137
1425
|
}
|
|
1426
|
+
const projectAttachError = await recordOnboardingLaunch(req, runId, repo.id, body.data);
|
|
1427
|
+
return reply.code(201).send({ runId, ...(projectAttachError !== undefined ? { projectAttachError } : {}) });
|
|
1138
1428
|
});
|
|
1139
1429
|
// Launch a run (replaces POST /sessions). `clisJson` defaults to the roster;
|
|
1140
1430
|
// `sessionId` is minted if the client omits it.
|
|
@@ -1142,7 +1432,7 @@ runtime = {}) {
|
|
|
1142
1432
|
config: {
|
|
1143
1433
|
manifest: {
|
|
1144
1434
|
requestType: 'LaunchRunBody',
|
|
1145
|
-
responseType: '
|
|
1435
|
+
responseType: 'LaunchRunResponse',
|
|
1146
1436
|
// 404/409: unknown project / unknown campaignId (wicked-studio#27) /
|
|
1147
1437
|
// archived-or-synthesized project + busy engine + the state-home blocker
|
|
1148
1438
|
// (`state_home_unregistered`) + a roster with no eligible seat (`no_eligible_seat`,
|
|
@@ -1159,6 +1449,13 @@ runtime = {}) {
|
|
|
1159
1449
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
1160
1450
|
}
|
|
1161
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
|
+
}
|
|
1162
1459
|
// wicked-core#411 / crew#497: the state-home blocker, judged BEFORE anything is resolved or
|
|
1163
1460
|
// committed. While the handed skills snapshot derives a state home with an entry the worker
|
|
1164
1461
|
// Read fence cannot classify, the engine refuses this launch at intake (and an engine before
|
|
@@ -1171,6 +1468,10 @@ runtime = {}) {
|
|
|
1171
1468
|
return reply.code(409).send(stateHomeBlockerBody(stateHome));
|
|
1172
1469
|
}
|
|
1173
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?.();
|
|
1174
1475
|
const input = {
|
|
1175
1476
|
problem: b.problem,
|
|
1176
1477
|
sessionId: b.sessionId ?? randomUUID(),
|
|
@@ -1179,6 +1480,15 @@ runtime = {}) {
|
|
|
1179
1480
|
// (`core/engine-roster.ts`), so a signed-out seat is never convened, never a judge.
|
|
1180
1481
|
clisJson: b.clisJson ?? JSON.stringify(rosterWithStanding()),
|
|
1181
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];
|
|
1182
1492
|
if (b.entityMode !== undefined)
|
|
1183
1493
|
input.entityMode = b.entityMode;
|
|
1184
1494
|
if (b.humanConfirm !== undefined)
|
|
@@ -1192,6 +1502,10 @@ runtime = {}) {
|
|
|
1192
1502
|
input.repoRef = b.repoRef;
|
|
1193
1503
|
if (b.workflow !== undefined)
|
|
1194
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);
|
|
1195
1509
|
if (b.projectId !== undefined) {
|
|
1196
1510
|
input.projectId = b.projectId;
|
|
1197
1511
|
// A project is a CONTEXT (crew#326): a run filed into one should see the project's whole
|
|
@@ -1310,8 +1624,45 @@ runtime = {}) {
|
|
|
1310
1624
|
throw err;
|
|
1311
1625
|
}
|
|
1312
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
|
+
}
|
|
1313
1647
|
try {
|
|
1314
1648
|
const runId = await adapter.launchRun(input);
|
|
1649
|
+
// crew#641/#642 (item 5): when a run is promoted from a chat, record how many seats
|
|
1650
|
+
// participated and whether the chat was grounded — so an intent authored by a single
|
|
1651
|
+
// ungrounded model is distinguishable from one that survived a disagreement.
|
|
1652
|
+
let chatSeatCount;
|
|
1653
|
+
let chatGrounded;
|
|
1654
|
+
// Both stay ABSENT unless the chat actually resolves: a closed or unknown chatId must read as
|
|
1655
|
+
// "provenance unavailable", not as "promoted from an ungrounded zero-seat chat". A valid chat
|
|
1656
|
+
// always has at least one warm seat (`crew-api-types` ChatOpenResponse), so 0 is not a value
|
|
1657
|
+
// `chat_seat_count` can honestly hold.
|
|
1658
|
+
if (b.chatId !== undefined) {
|
|
1659
|
+
const promotedFrom = await adapter.chatSeats(b.chatId).catch(() => null);
|
|
1660
|
+
const scope = chatScopes.engineOf(b.chatId);
|
|
1661
|
+
if (promotedFrom !== null && promotedFrom.length > 0 && scope !== undefined) {
|
|
1662
|
+
chatSeatCount = promotedFrom.length;
|
|
1663
|
+
chatGrounded = scope.codeGraphDb != null;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1315
1666
|
// Who launched it — the engine's LaunchOptions carries no actor field
|
|
1316
1667
|
// (checked, wicked-core-ts 0.6.0), so the crew-side trail is the system
|
|
1317
1668
|
// of record for run provenance (task #88).
|
|
@@ -1344,10 +1695,19 @@ runtime = {}) {
|
|
|
1344
1695
|
// crew#619: the chat↔run link is durable so the retention maps can be rehydrated after a
|
|
1345
1696
|
// daemon restart (rehydration reads `run.launched` entries and keeps non-terminal entries).
|
|
1346
1697
|
...(b.chatId !== undefined ? { chatId: b.chatId } : {}),
|
|
1698
|
+
// crew#641/#642 (item 5): chat promotion provenance — seat count and grounding at launch.
|
|
1699
|
+
...(chatSeatCount !== undefined ? { chatSeatCount } : {}),
|
|
1700
|
+
...(chatGrounded !== undefined ? { chatGrounded } : {}),
|
|
1347
1701
|
// crew#632: launch surface and caller identity — human-readable provenance on the audit trail.
|
|
1348
1702
|
...(b.channel !== undefined ? { channel: b.channel } : {}),
|
|
1349
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 } : {}),
|
|
1350
1706
|
});
|
|
1707
|
+
// Stamp the index live so the field is served without waiting for a daemon restart.
|
|
1708
|
+
if (chatSeatCount !== undefined && chatGrounded !== undefined) {
|
|
1709
|
+
runTimingIndex.setChatPromotion(runId, chatSeatCount, chatGrounded);
|
|
1710
|
+
}
|
|
1351
1711
|
if (b.retryOf !== undefined)
|
|
1352
1712
|
retryIndex.set(runId, b.retryOf);
|
|
1353
1713
|
if (revisesPr !== undefined)
|
|
@@ -1370,10 +1730,15 @@ runtime = {}) {
|
|
|
1370
1730
|
// is always reproducible even if the chat is idle-reclaimed before the run finishes.
|
|
1371
1731
|
if (b.chatId !== undefined)
|
|
1372
1732
|
runtime.linkChatRun?.(b.chatId, runId);
|
|
1373
|
-
return reply.code(201).send({ runId });
|
|
1733
|
+
return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}) });
|
|
1374
1734
|
}
|
|
1375
1735
|
catch (err) {
|
|
1376
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
|
+
}
|
|
1377
1742
|
// The engine's own intake refusal (wicked-core#411 `StateHomeConfigError`) is a configuration
|
|
1378
1743
|
// error, not a malformed request: the same typed 409 the pre-check above answers, over a fresh
|
|
1379
1744
|
// survey — the pre-check saw a clean state home (or an addon that could not classify), the
|
|
@@ -1536,8 +1901,6 @@ runtime = {}) {
|
|
|
1536
1901
|
return reply.code(404).send({ error: 'Run not found' });
|
|
1537
1902
|
return { run: decorateRun(run) };
|
|
1538
1903
|
});
|
|
1539
|
-
/** The full workflow registry, or `[]` on a directly-driven route set whose fake adapter has none. */
|
|
1540
|
-
const listWorkflowsSafe = () => typeof adapter.listWorkflows === 'function' ? adapter.listWorkflows() : [];
|
|
1541
1904
|
// ── Deliver text (crew#524 / F-3R2-014) — the PR title + body a run's delivery carries ──
|
|
1542
1905
|
// `gh pr create --fill` gave wicked-studio#249 a mid-word title and an EMPTY body. The deliver
|
|
1543
1906
|
// script now asks this route for the text composed from the PERSISTED RUN RECORD: the intent,
|
|
@@ -1552,12 +1915,12 @@ runtime = {}) {
|
|
|
1552
1915
|
const run = views.find((v) => v.session.id === id);
|
|
1553
1916
|
if (!run)
|
|
1554
1917
|
return reply.code(404).send({ error: 'Run not found' });
|
|
1555
|
-
const
|
|
1556
|
-
// The
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
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,
|
|
1560
1922
|
revisesPr: retryIndex.revisesPrFor(id) ?? null,
|
|
1923
|
+
...(followUps !== undefined ? { followUps } : {}),
|
|
1561
1924
|
}));
|
|
1562
1925
|
return reply.type('text/plain; charset=utf-8').send(framedDeliverText(text));
|
|
1563
1926
|
});
|
|
@@ -1618,10 +1981,19 @@ runtime = {}) {
|
|
|
1618
1981
|
const existing = deliveryIndex.urlFor(id);
|
|
1619
1982
|
if (existing !== undefined)
|
|
1620
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
|
+
}
|
|
1621
1990
|
const views = await adapter.sessionsDetail();
|
|
1622
1991
|
const run = views.find((v) => v.session.id === id);
|
|
1623
1992
|
if (!run)
|
|
1624
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()));
|
|
1625
1997
|
// crew#418/#432: a run stranded by a recoverable deliver lift failure reads `failed` from the
|
|
1626
1998
|
// engine but IS liftable post-hoc — normalize its status to `completed` so this route
|
|
1627
1999
|
// accepts it, exactly as the run wire and the resume route see it.
|
|
@@ -1685,7 +2057,7 @@ runtime = {}) {
|
|
|
1685
2057
|
// crew#524: the post-hoc lift composes its PR text from the run record it already
|
|
1686
2058
|
// holds (the fallback), and names this daemon so the script can re-ask at delivery.
|
|
1687
2059
|
const origin = boundOrigin(app.server.address());
|
|
1688
|
-
const
|
|
2060
|
+
const workflowName = runIdentityOf(run).name;
|
|
1689
2061
|
const revising = retryIndex.revisesPrFor(id);
|
|
1690
2062
|
let revisesPr = null;
|
|
1691
2063
|
if (revising !== undefined) {
|
|
@@ -1699,14 +2071,20 @@ runtime = {}) {
|
|
|
1699
2071
|
}
|
|
1700
2072
|
revisesPr = { number: revising.number, headRef: again.pr.headRef, url: again.pr.url };
|
|
1701
2073
|
}
|
|
2074
|
+
const followUps = await evaluatorFollowUps(run);
|
|
1702
2075
|
result = await deliverExec(workdir, s.problem ?? undefined, {
|
|
1703
2076
|
runId: id,
|
|
1704
2077
|
apiOrigin: origin,
|
|
1705
|
-
facts: factsFromRun(run, runUrlFor(
|
|
1706
|
-
workflowId:
|
|
2078
|
+
facts: factsFromRun(run, runUrlFor(configuredPublicOrigin(), id), {
|
|
2079
|
+
workflowId: workflowName,
|
|
1707
2080
|
revisesPr: revisesPr === null ? null : { number: revisesPr.number, url: revisesPr.url },
|
|
2081
|
+
...(followUps !== undefined ? { followUps } : {}),
|
|
1708
2082
|
}),
|
|
1709
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),
|
|
1710
2088
|
});
|
|
1711
2089
|
}
|
|
1712
2090
|
finally {
|
|
@@ -1748,7 +2126,19 @@ runtime = {}) {
|
|
|
1748
2126
|
error: `deliver failed (exit ${result.status}): ${deliverErrorTail(result.output)}`,
|
|
1749
2127
|
});
|
|
1750
2128
|
}
|
|
1751
|
-
|
|
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
|
+
}
|
|
1752
2142
|
if (url === null) {
|
|
1753
2143
|
// Exit 0 with no URL should be unreachable (the script re-derives its own success),
|
|
1754
2144
|
// but a delivery nothing can be pointed at is never recorded (crew#317).
|
|
@@ -1826,6 +2216,12 @@ runtime = {}) {
|
|
|
1826
2216
|
}
|
|
1827
2217
|
const b = parsed.data;
|
|
1828
2218
|
const chatId = b.chatId ?? randomUUID();
|
|
2219
|
+
const requestedRefs = [...(b.repoRef !== undefined ? [b.repoRef] : []), ...(b.repoRefs ?? [])];
|
|
2220
|
+
// A named scope's SHAPE first (studio#323 R4, codex on #664): a body whose kind refuses its own
|
|
2221
|
+
// projectId/repoRefs is a 400 — never masked by the project lookup's 404/409 below.
|
|
2222
|
+
const shape = namedKindRefusal(b.scopeKind, b.projectId, [...new Set(requestedRefs)]);
|
|
2223
|
+
if (shape !== null)
|
|
2224
|
+
return reply.code(400).send({ error: shape.error });
|
|
1829
2225
|
// Validate the project BEFORE opening seats: a chat has no launch record for the engine to
|
|
1830
2226
|
// attach against atomically (chats are an in-memory seat pool), so the route validates
|
|
1831
2227
|
// up-front and attaches right after open — the one non-atomic attach, documented in the ADR
|
|
@@ -1866,8 +2262,14 @@ runtime = {}) {
|
|
|
1866
2262
|
const resolution = await resolveChatScope({
|
|
1867
2263
|
chatId,
|
|
1868
2264
|
...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
|
|
1869
|
-
repoRefs:
|
|
1870
|
-
|
|
2265
|
+
repoRefs: requestedRefs,
|
|
2266
|
+
...(b.scopeKind !== undefined ? { kind: b.scopeKind } : {}),
|
|
2267
|
+
}, {
|
|
2268
|
+
...chatScopeDeps(adapter),
|
|
2269
|
+
scratchBase: chatScopes.base,
|
|
2270
|
+
log: (m) => req.log.warn(m),
|
|
2271
|
+
...(runtime.entityCount !== undefined ? { entityCount: runtime.entityCount } : {}),
|
|
2272
|
+
});
|
|
1871
2273
|
if (!resolution.ok) {
|
|
1872
2274
|
chatScopes.release(chatId, token);
|
|
1873
2275
|
return reply.code(resolution.status).send({
|
|
@@ -1895,7 +2297,10 @@ runtime = {}) {
|
|
|
1895
2297
|
// here is NAMED on the response (`refused`) and in the thread (`chatSeatRefused`), so a
|
|
1896
2298
|
// person can see why pi is missing. Explicit `clis` are passed through as asked; the engine
|
|
1897
2299
|
// refuses per seat with its reason, which is copied into `refused` too.
|
|
1898
|
-
|
|
2300
|
+
// A chat is SCOPED — held to read-only roots — when it reads repositories by its kind. `none`
|
|
2301
|
+
// and `system` (studio#323 R4: the platform itself, no repository) read none, so they take the
|
|
2302
|
+
// unscoped admission and open on an engine that predates chat scope.
|
|
2303
|
+
const scoped = scope.kind !== 'none' && scope.kind !== 'system';
|
|
1899
2304
|
const refused = [];
|
|
1900
2305
|
// The standing roster, read ONCE: the default admission below and the engine-drop
|
|
1901
2306
|
// attribution after `chatOpen` both consult it (F-A45-011).
|
|
@@ -2067,12 +2472,17 @@ runtime = {}) {
|
|
|
2067
2472
|
...(b.projectId !== undefined ? { project_id: b.projectId } : {}),
|
|
2068
2473
|
});
|
|
2069
2474
|
}
|
|
2475
|
+
// crew#641: a chat with exactly ONE warm seat cannot disagree with itself — disclosed
|
|
2476
|
+
// prominently so the caller does not have to reason about the refused[] array.
|
|
2477
|
+
const warm = seats.filter((s) => s.ok);
|
|
2478
|
+
const singleSeat = warm.length === 1 ? singleSeatDisclosure(warm[0].cliKey, refused, 'open') : undefined;
|
|
2070
2479
|
return reply.code(201).send({
|
|
2071
2480
|
chatId,
|
|
2072
2481
|
seats,
|
|
2073
2482
|
scope,
|
|
2074
2483
|
refused,
|
|
2075
2484
|
...(projectAttachError !== undefined ? { projectAttachError } : {}),
|
|
2485
|
+
...(singleSeat !== undefined ? { singleSeat } : {}),
|
|
2076
2486
|
});
|
|
2077
2487
|
}
|
|
2078
2488
|
catch (err) {
|
|
@@ -2123,7 +2533,12 @@ runtime = {}) {
|
|
|
2123
2533
|
// warm seats — so the turn can be opened BEFORE the engine call, in the same tick as the
|
|
2124
2534
|
// predicate below: two racing sends can no longer both pass `inFlight` during the await, and a
|
|
2125
2535
|
// `chatDelta` that lands before `chatSend` resolves is already stamped with its `turn_id`.
|
|
2126
|
-
|
|
2536
|
+
// crew#641 (review): the ROSTER and the AUDIENCE are different things and only one of them can
|
|
2537
|
+
// answer "can this chat disagree with itself". Resolved ONCE here: `audience` is who this turn
|
|
2538
|
+
// goes to (the named targets, else everyone warm), `warmRoster` is who is warm at all. A
|
|
2539
|
+
// targeted send to one seat of a two-seat chat is not a degraded chat.
|
|
2540
|
+
const warmRoster = await adapter.chatSeats(id);
|
|
2541
|
+
const audience = parsed.data.targets ?? warmRoster;
|
|
2127
2542
|
if (audience.length === 0) {
|
|
2128
2543
|
return reply.code(409).send({ error: `chat '${id}' has no warm seats — open it first` });
|
|
2129
2544
|
}
|
|
@@ -2141,7 +2556,22 @@ runtime = {}) {
|
|
|
2141
2556
|
chatTurns.reconcile(id, turn.turnId, seats);
|
|
2142
2557
|
chatTranscripts?.appendUser(id, turn.turnId, parsed.data.text, seats);
|
|
2143
2558
|
}
|
|
2144
|
-
|
|
2559
|
+
// crew#641: re-state single-seat degradation on every turn so it is visible in the transcript.
|
|
2560
|
+
// Decided from `warmRoster` — the seats that are WARM — never from `seats`, which is only the
|
|
2561
|
+
// seats this turn reached: a targeted send to one seat of a two-seat chat would otherwise
|
|
2562
|
+
// announce a degradation that does not exist, and a false "you are degraded" sends an operator
|
|
2563
|
+
// after a problem they do not have.
|
|
2564
|
+
// crew#650: `refusedOf` is `undefined` for a chat THIS daemon did not open (a restart drops
|
|
2565
|
+
// the index) — that is "the refusals are unknown", not "the chat gained a seat". The
|
|
2566
|
+
// disclosure is decided by the warm roster alone, and the `undefined` is handed STRAIGHT to
|
|
2567
|
+
// the builder (never `?? []`, review of #658): only it may decide what an unknown record is
|
|
2568
|
+
// allowed to claim.
|
|
2569
|
+
const singleSeat202 = warmRoster.length === 1 ? singleSeatDisclosure(warmRoster[0], chatScopes.refusedOf(id), 'turn') : undefined;
|
|
2570
|
+
return reply.code(202).send({
|
|
2571
|
+
seats,
|
|
2572
|
+
...(turn !== null ? { turnId: turn.turnId } : {}),
|
|
2573
|
+
...(singleSeat202 !== undefined ? { singleSeat: singleSeat202 } : {}),
|
|
2574
|
+
});
|
|
2145
2575
|
}
|
|
2146
2576
|
catch (err) {
|
|
2147
2577
|
// Nothing went out: the reservation is retracted so the next send is not refused for it.
|
|
@@ -2348,11 +2778,11 @@ runtime = {}) {
|
|
|
2348
2778
|
const run = views.find((v) => v.session.id === id);
|
|
2349
2779
|
if (!run)
|
|
2350
2780
|
return reply.code(404).send({ error: 'Run not found' });
|
|
2351
|
-
//
|
|
2352
|
-
//
|
|
2353
|
-
//
|
|
2354
|
-
|
|
2355
|
-
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);
|
|
2356
2786
|
let repo = null;
|
|
2357
2787
|
if (run.session.repo_ref !== null) {
|
|
2358
2788
|
const repos = await adapter.listRepos();
|
|
@@ -2361,8 +2791,7 @@ runtime = {}) {
|
|
|
2361
2791
|
return buildAcceptanceView({
|
|
2362
2792
|
runId: id,
|
|
2363
2793
|
repo,
|
|
2364
|
-
|
|
2365
|
-
gateEvents: qeGateEvents,
|
|
2794
|
+
requirement,
|
|
2366
2795
|
...(qeRunId !== undefined && qeRunId !== '' ? { qeRunId } : {}),
|
|
2367
2796
|
// AW-14 (arch-R13a + R16): the conformance section reads the same wires the standalone
|
|
2368
2797
|
// `/governance/claims` and `/runs/:id/events` routes serve, but run-scoped and resolved
|
|
@@ -2372,55 +2801,122 @@ runtime = {}) {
|
|
|
2372
2801
|
events: (rid) => adapter.runEvents(rid),
|
|
2373
2802
|
});
|
|
2374
2803
|
});
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
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);
|
|
2388
2846
|
if (!parsed.success) {
|
|
2389
|
-
return
|
|
2847
|
+
return { code: 400, body: invalidBody(parsed.error, 'Invalid request body') };
|
|
2390
2848
|
}
|
|
2391
2849
|
const views = await adapter.sessionsDetail();
|
|
2392
2850
|
const run = views.find((v) => v.session.id === id);
|
|
2393
2851
|
if (!run)
|
|
2394
|
-
return
|
|
2852
|
+
return { code: 404, body: { error: 'Run not found' } };
|
|
2395
2853
|
if (run.session.status !== 'awaiting_human') {
|
|
2396
|
-
return
|
|
2397
|
-
|
|
2398
|
-
|
|
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
|
+
}
|
|
2399
2894
|
}
|
|
2400
2895
|
// The steering-author landing (crew#388): decided — and the gate prompt captured — BEFORE
|
|
2401
2896
|
// the confirm, because a terminal-phase approve prunes the gate cache and moves the run out
|
|
2402
|
-
// of `awaiting_human`. The landing itself runs AFTER a successful approve only.
|
|
2403
|
-
// (
|
|
2404
|
-
//
|
|
2405
|
-
const steeringPropose = parsed.data.approve &&
|
|
2406
|
-
typeof adapter.listWorkflows === 'function' &&
|
|
2407
|
-
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);
|
|
2408
2901
|
const gatePrompt = steeringPropose ? gateCache.get(id)?.prompt : undefined;
|
|
2409
2902
|
try {
|
|
2410
2903
|
// All five positionals, explicit `undefined` for the absent ones (DES-L1 PR-2; the
|
|
2411
2904
|
// gate-arms tripwire pins the arity). An engine refusal (no creator to send back to,
|
|
2412
2905
|
// an engine older than the arm) surfaces as the existing 409 below.
|
|
2413
|
-
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);
|
|
2414
2907
|
// WHO approved/rejected — the gate-decision audit (task #88). The engine
|
|
2415
2908
|
// records THAT the gate resolved (interaction_requests / gateDecided);
|
|
2416
2909
|
// only this HTTP layer knows the authenticated principal behind it.
|
|
2417
|
-
audit.record('gate.decided',
|
|
2910
|
+
audit.record('gate.decided', actor, {
|
|
2418
2911
|
runId: id,
|
|
2419
2912
|
detail: {
|
|
2420
2913
|
approve: parsed.data.approve,
|
|
2421
2914
|
...(parsed.data.amend !== undefined ? { amend: parsed.data.amend } : {}),
|
|
2422
2915
|
...(parsed.data.action !== undefined ? { action: parsed.data.action } : {}),
|
|
2423
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,
|
|
2424
2920
|
status,
|
|
2425
2921
|
},
|
|
2426
2922
|
});
|
|
@@ -2433,13 +2929,32 @@ runtime = {}) {
|
|
|
2433
2929
|
// text (an operator who wants different rules rejects and re-authors). A reject lands
|
|
2434
2930
|
// nothing — the run cancels and the proposal stays an artifact.
|
|
2435
2931
|
const landing = steeringPropose
|
|
2436
|
-
? await landSteeringProposal({ adapter, audit, actor
|
|
2932
|
+
? await landSteeringProposal({ adapter, audit, actor }, run, gatePrompt)
|
|
2437
2933
|
: undefined;
|
|
2438
|
-
return
|
|
2934
|
+
return { code: 200, body: { status, ...(landing !== undefined ? { landing } : {}) } };
|
|
2439
2935
|
}
|
|
2440
2936
|
catch (err) {
|
|
2441
|
-
|
|
2937
|
+
if (err instanceof PlanLaunchUnsupportedError) {
|
|
2938
|
+
return { code: 501, body: { error: message(err) } };
|
|
2939
|
+
}
|
|
2940
|
+
return { code: 409, body: { error: message(err) } };
|
|
2442
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);
|
|
2443
2958
|
});
|
|
2444
2959
|
// Cancel a running or paused run (distinct third action, §11.1).
|
|
2445
2960
|
app.post(`${V}/runs/:id/cancel`, async (req, reply) => {
|
|
@@ -2486,9 +3001,11 @@ runtime = {}) {
|
|
|
2486
3001
|
? `run ${id} is cancelled — a terminal run cannot be resumed; relaunch the work as a new run with POST /runs {"retryOf":"${id}"}`
|
|
2487
3002
|
: state.delivery === 'stranded'
|
|
2488
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`
|
|
2489
|
-
: state.delivery === '
|
|
2490
|
-
? `run ${id} is already completed
|
|
2491
|
-
:
|
|
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}"}`;
|
|
2492
3009
|
return reply.code(409).send({ error: why, recovery });
|
|
2493
3010
|
}
|
|
2494
3011
|
try {
|
|
@@ -2496,9 +3013,7 @@ runtime = {}) {
|
|
|
2496
3013
|
// A gated resume IS a gate approval, so it lands a steering-author proposal exactly like
|
|
2497
3014
|
// POST /runs/:id/gate would — the "no side door" rule (task #88) holds for the landing
|
|
2498
3015
|
// write too (crew#388). Prompt captured pre-confirm; see the gate route.
|
|
2499
|
-
const steeringPropose = gated &&
|
|
2500
|
-
typeof adapter.listWorkflows === 'function' &&
|
|
2501
|
-
isSteeringAuthorRun(run, adapter.listWorkflows());
|
|
3016
|
+
const steeringPropose = gated && isSteeringAuthorRun(run);
|
|
2502
3017
|
const gatePrompt = steeringPropose ? gateCache.get(id)?.prompt : undefined;
|
|
2503
3018
|
const status = gated ? await adapter.confirmGate(id, true) : await adapter.resumeRun(id);
|
|
2504
3019
|
// A resume of a gated run IS a gate approval — audit it as one, so the
|
|
@@ -2580,7 +3095,7 @@ runtime = {}) {
|
|
|
2580
3095
|
`this build cannot approve the gate on your behalf (no confirmGate); approve via POST /runs/${id}/gate, then reassign`,
|
|
2581
3096
|
});
|
|
2582
3097
|
}
|
|
2583
|
-
if (gated &&
|
|
3098
|
+
if (gated && isSteeringAuthorRun(run)) {
|
|
2584
3099
|
return reply.code(409).send({
|
|
2585
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`,
|
|
2586
3101
|
});
|
|
@@ -2648,34 +3163,12 @@ runtime = {}) {
|
|
|
2648
3163
|
return reply.code(404).send({ error: 'No open gate for this run' });
|
|
2649
3164
|
}
|
|
2650
3165
|
// DURABLE TRUTH FIRST (DES-PROJECT-001 §5.3): the engine persists the open prompt in
|
|
2651
|
-
// `interaction_requests`, written in the same transaction as the `awaiting_human` pause —
|
|
2652
|
-
//
|
|
2653
|
-
//
|
|
2654
|
-
//
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
// fall through and let the log speak.
|
|
2658
|
-
const durableRows = typeof adapter.interactionRequests === 'function'
|
|
2659
|
-
? await adapter.interactionRequests(id, 'open')
|
|
2660
|
-
: null;
|
|
2661
|
-
const durableGate = durableRows?.find((r) => r.kind === 'gate');
|
|
2662
|
-
if (durableGate !== undefined) {
|
|
2663
|
-
// This path builds the entry inline (not through `fold`), so it must run the SAME refusal
|
|
2664
|
-
// detection (issue #419) — otherwise a gate served from the durable row after a restart would
|
|
2665
|
-
// silently drop the warning that the live/replay paths carry.
|
|
2666
|
-
const refusal = detectRefusal(durableGate.prompt);
|
|
2667
|
-
const entry = {
|
|
2668
|
-
ord: durableGate.ord ?? 0,
|
|
2669
|
-
prompt: durableGate.prompt,
|
|
2670
|
-
lifecycle: 'open',
|
|
2671
|
-
receivedAt: new Date(durableGate.created_at).toISOString(),
|
|
2672
|
-
...(refusal !== undefined ? { refusal } : {}),
|
|
2673
|
-
};
|
|
2674
|
-
gateCache.adopt(id, entry);
|
|
2675
|
-
return { runId: id, ...entry };
|
|
2676
|
-
}
|
|
2677
|
-
const events = await adapter.runEvents(id);
|
|
2678
|
-
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') {
|
|
2679
3172
|
// Now — and only now — 503 is the honest answer: this run really is holding for a human, and
|
|
2680
3173
|
// this build cannot say what it is asking. Distinct cause, distinct message; answering "no
|
|
2681
3174
|
// gate" here would report a capability gap as a fact about the run (the FINDING-050 shape).
|
|
@@ -2683,13 +3176,12 @@ runtime = {}) {
|
|
|
2683
3176
|
error: 'Gate history is unavailable: this wicked-core build has no event-log read binding',
|
|
2684
3177
|
});
|
|
2685
3178
|
}
|
|
2686
|
-
|
|
2687
|
-
if (!replayed) {
|
|
3179
|
+
if (open === null) {
|
|
2688
3180
|
// Parked, with a history that records no open gate. Pre-log runs land here (their prompt is
|
|
2689
3181
|
// genuinely lost), as does a gate whose `awaitingHuman` predates the log's retention.
|
|
2690
3182
|
return reply.code(404).send({ error: 'No open gate for this run' });
|
|
2691
3183
|
}
|
|
2692
|
-
return { runId: id, ...
|
|
3184
|
+
return { runId: id, ...open };
|
|
2693
3185
|
});
|
|
2694
3186
|
// ── Elicitation (DES-002) ────────────────────────────────────────────────────
|
|
2695
3187
|
//
|
|
@@ -2979,6 +3471,74 @@ runtime = {}) {
|
|
|
2979
3471
|
const claims = await adapter.listConformanceClaims();
|
|
2980
3472
|
return { claims };
|
|
2981
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
|
+
});
|
|
2982
3542
|
// Normalize a `?repo=` query: Fastify parses a repeated `?repo=a&repo=b` into a `string[]` (take
|
|
2983
3543
|
// the first), and the value is TRIMMED so it's both validated and PASSED trimmed — a `?repo=%20x%20`
|
|
2984
3544
|
// must not pass the guard then miss the registry lookup on whitespace (Copilot #227).
|
|
@@ -3148,15 +3708,41 @@ runtime = {}) {
|
|
|
3148
3708
|
}
|
|
3149
3709
|
});
|
|
3150
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).
|
|
3151
3728
|
app.get(`${V}/workflows`, async () => {
|
|
3152
|
-
const workflows = adapter.
|
|
3153
|
-
return { workflows };
|
|
3729
|
+
const { workflows, unavailable } = await adapter.workflowCatalog();
|
|
3730
|
+
return { workflows, unavailable };
|
|
3154
3731
|
});
|
|
3155
3732
|
app.get(`${V}/workflows/:id`, async (req, reply) => {
|
|
3156
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
|
+
}
|
|
3157
3742
|
const workflow = adapter.getWorkflow(id);
|
|
3158
|
-
if (!workflow)
|
|
3743
|
+
if (!workflow) {
|
|
3159
3744
|
return reply.code(404).send({ error: `workflow '${id}' not found` });
|
|
3745
|
+
}
|
|
3160
3746
|
// humanGates: the phases that will PAUSE for a person even under humanConfirm:none (core#208).
|
|
3161
3747
|
// Surfaced so an operator can see a workflow's gates BEFORE launching it (FINDING-023).
|
|
3162
3748
|
return { workflow, humanGates: humanGatePhaseIds(workflow) };
|
|
@@ -3478,6 +4064,18 @@ runtime = {}) {
|
|
|
3478
4064
|
return reply.code(500).send({ error: message(err) });
|
|
3479
4065
|
}
|
|
3480
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
|
+
});
|
|
3481
4079
|
app.get(`${V}/repos/:id/git-history`, async (req, reply) => {
|
|
3482
4080
|
const { id } = req.params;
|
|
3483
4081
|
const repos = await adapter.listRepos();
|
|
@@ -3660,6 +4258,22 @@ runtime = {}) {
|
|
|
3660
4258
|
.send({ error: 'workerStallMaxEscalations must be an integer between 1 and 10' });
|
|
3661
4259
|
}
|
|
3662
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
|
+
}
|
|
3663
4277
|
// deliverDefault (crew#393): the repo-scoped launch delivery default. Two values only —
|
|
3664
4278
|
// this knob decides whether completed code runs open PRs, so a typo must be a 400, never
|
|
3665
4279
|
// a silently-dropped key that leaves the operator believing they flipped it.
|
|
@@ -3731,6 +4345,7 @@ runtime = {}) {
|
|
|
3731
4345
|
'workerStallEscalateAction',
|
|
3732
4346
|
'workerStallMaxEscalations',
|
|
3733
4347
|
'deliverDefault',
|
|
4348
|
+
'deliverIdentityLogin',
|
|
3734
4349
|
'baseSkillRef',
|
|
3735
4350
|
'baseSkillPolicy',
|
|
3736
4351
|
];
|
|
@@ -3815,6 +4430,41 @@ runtime = {}) {
|
|
|
3815
4430
|
return estateUpstreamError(reply, err);
|
|
3816
4431
|
}
|
|
3817
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
|
+
});
|
|
3818
4468
|
// POST /proposals/:id/approve → proposal.approve →
|
|
3819
4469
|
// { outcome:'promoted', active_id } — a MEMORY proposal, now an active memory (complete);
|
|
3820
4470
|
// { outcome:'handed_off', payload, landing } — a POLICY proposal, LANDED here as a steering rule.
|
|
@@ -3829,7 +4479,16 @@ runtime = {}) {
|
|
|
3829
4479
|
// 500, never the silent no-op that crew#388 exists to end — and `payload` is still returned for
|
|
3830
4480
|
// backward compatibility. The MEMORY path is UNTOUCHED: a `promoted` outcome returns immediately
|
|
3831
4481
|
// with no extra estate call and no landing.
|
|
3832
|
-
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) => {
|
|
3833
4492
|
// Normalize ONCE and use the trimmed value throughout — an id like `%20pol1%20`
|
|
3834
4493
|
// decodes to a padded, non-empty string that would otherwise ride upstream as-is
|
|
3835
4494
|
// and into `proposal:<id>` derivations.
|
|
@@ -3837,6 +4496,24 @@ runtime = {}) {
|
|
|
3837
4496
|
if (id === '') {
|
|
3838
4497
|
return reply.code(400).send({ error: '`id` is required' });
|
|
3839
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
|
+
}
|
|
3840
4517
|
let approved;
|
|
3841
4518
|
try {
|
|
3842
4519
|
approved = (await estateTool('proposal.approve', { id }));
|
|
@@ -3907,11 +4584,16 @@ runtime = {}) {
|
|
|
3907
4584
|
return { outcome: 'handed_off', payload: approved.payload, landing };
|
|
3908
4585
|
});
|
|
3909
4586
|
// POST /proposals/:id/reject → proposal.reject → { ok: true }.
|
|
3910
|
-
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) => {
|
|
3911
4590
|
const { id } = req.params;
|
|
3912
4591
|
if (id.trim() === '') {
|
|
3913
4592
|
return reply.code(400).send({ error: '`id` is required' });
|
|
3914
4593
|
}
|
|
4594
|
+
if (editInFlight(id.trim())) {
|
|
4595
|
+
return reply.code(409).send({ error: `proposal ${id.trim()} is being accepted` });
|
|
4596
|
+
}
|
|
3915
4597
|
try {
|
|
3916
4598
|
return (await estateTool('proposal.reject', { id }));
|
|
3917
4599
|
}
|
|
@@ -3937,10 +4619,12 @@ runtime = {}) {
|
|
|
3937
4619
|
// applied HERE as post-filters over that complete set (see below) — filtering, not retrieval.
|
|
3938
4620
|
// • list returns { memory_id, scope, content, tier, facets, created_at }; facets ride through to
|
|
3939
4621
|
// MemoryItem.facets, powering the surface's facet filter.
|
|
3940
|
-
// • RETIRE
|
|
3941
|
-
// hard-deletes EVERY memory whose scope equals or descends from
|
|
3942
|
-
// empty prefix (a total-wipe guard)
|
|
3943
|
-
//
|
|
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.
|
|
3944
4628
|
// GET /memory?query=&scope_prefix=&facets=<json>&limit= → memory.list + post-filter → { memories }.
|
|
3945
4629
|
app.get(`${V}/memory`, { config: { manifest: { responseType: 'ListMemoriesResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
|
|
3946
4630
|
const q = req.query;
|
|
@@ -4043,9 +4727,9 @@ runtime = {}) {
|
|
|
4043
4727
|
return estateUpstreamError(reply, err);
|
|
4044
4728
|
}
|
|
4045
4729
|
});
|
|
4046
|
-
// POST /memory/retire { scope_prefix } → memory.erase → { erased }. SUBTREE-scoped (
|
|
4047
|
-
// delete
|
|
4048
|
-
// 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.
|
|
4049
4733
|
app.post(`${V}/memory/retire`, {
|
|
4050
4734
|
config: {
|
|
4051
4735
|
manifest: {
|
|
@@ -4062,7 +4746,7 @@ runtime = {}) {
|
|
|
4062
4746
|
const scopePrefix = parsed.data.scope_prefix.trim();
|
|
4063
4747
|
if (scopePrefix === '') {
|
|
4064
4748
|
return reply.code(400).send({
|
|
4065
|
-
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)',
|
|
4066
4750
|
});
|
|
4067
4751
|
}
|
|
4068
4752
|
try {
|
|
@@ -4078,8 +4762,64 @@ runtime = {}) {
|
|
|
4078
4762
|
return estateUpstreamError(reply, err);
|
|
4079
4763
|
}
|
|
4080
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
|
+
});
|
|
4081
4810
|
// ── Projects (DES-PROJECT-001) — the 9-route experience-plane surface ────────
|
|
4082
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);
|
|
4083
4823
|
// ── Campaigns (crew#342 + TH-9) — the engine's durable Run-DAG scheduler over REST ──────────
|
|
4084
4824
|
// Progress streams as campaign* CoreEvents on the existing allowlist-free /ws relay; these
|
|
4085
4825
|
// routes are launch/resume/cancel + the reads a scoreboard builds from. The GET routes join
|
|
@@ -4097,6 +4837,7 @@ runtime = {}) {
|
|
|
4097
4837
|
// Wave 6 (F-7R2-014): the registered test sets ride beside the campaigns + groups.
|
|
4098
4838
|
testSets,
|
|
4099
4839
|
deliveryUrlFor: (runId) => deliveryIndex.urlFor(runId),
|
|
4840
|
+
deliveryPushedFor: (runId) => deliveryIndex.pushedFor(runId),
|
|
4100
4841
|
vacuity: vacuityProbes,
|
|
4101
4842
|
// A non-probe derivation throw in the rollup is a defect — error level, loud in diagnostics.
|
|
4102
4843
|
logDefect: (m) => app.log.error(m),
|
|
@@ -4150,6 +4891,24 @@ runtime = {}) {
|
|
|
4150
4891
|
audit,
|
|
4151
4892
|
actorOf,
|
|
4152
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
|
+
});
|
|
4153
4912
|
// ── The wicked-interactive bridge, reverse-proxied (DES-MERGE-001 §5.3/§7.2) ──
|
|
4154
4913
|
// Mounted BESIDE the routes above and under the same `${V}` prefix, so it inherits one
|
|
4155
4914
|
// origin, one auth hook, and one CORS posture — the whole point of slice 1.
|
|
@@ -4200,5 +4959,6 @@ runtime = {}) {
|
|
|
4200
4959
|
pool: interactiveBridges,
|
|
4201
4960
|
log: (m) => app.log.warn(m),
|
|
4202
4961
|
});
|
|
4962
|
+
return { decideGate };
|
|
4203
4963
|
}
|
|
4204
4964
|
//# sourceMappingURL=routes.js.map
|