wicked-crew 0.16.11 → 0.16.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -8
- package/dist/api/artifact-outline.d.ts +64 -0
- package/dist/api/artifact-outline.d.ts.map +1 -0
- package/dist/api/artifact-outline.js +254 -0
- package/dist/api/artifact-outline.js.map +1 -0
- package/dist/api/beneath.d.ts +76 -0
- package/dist/api/beneath.d.ts.map +1 -0
- package/dist/api/beneath.js +214 -0
- package/dist/api/beneath.js.map +1 -0
- package/dist/api/capture.d.ts +2 -2
- package/dist/api/chat-citations.d.ts +25 -1
- package/dist/api/chat-citations.d.ts.map +1 -1
- package/dist/api/chat-citations.js +186 -61
- package/dist/api/chat-citations.js.map +1 -1
- package/dist/api/chat-context.d.ts +236 -0
- package/dist/api/chat-context.d.ts.map +1 -0
- package/dist/api/chat-context.js +808 -0
- package/dist/api/chat-context.js.map +1 -0
- package/dist/api/chat-history.d.ts +20 -0
- package/dist/api/chat-history.d.ts.map +1 -1
- package/dist/api/chat-history.js +46 -1
- package/dist/api/chat-history.js.map +1 -1
- package/dist/api/chat-proposals.d.ts +534 -0
- package/dist/api/chat-proposals.d.ts.map +1 -0
- package/dist/api/chat-proposals.js +1562 -0
- package/dist/api/chat-proposals.js.map +1 -0
- package/dist/api/chat-scope.d.ts +41 -0
- package/dist/api/chat-scope.d.ts.map +1 -1
- package/dist/api/chat-scope.js +87 -15
- package/dist/api/chat-scope.js.map +1 -1
- package/dist/api/chat-transcripts.d.ts +27 -2
- package/dist/api/chat-transcripts.d.ts.map +1 -1
- package/dist/api/chat-transcripts.js +105 -4
- package/dist/api/chat-transcripts.js.map +1 -1
- package/dist/api/diagnostics.d.ts +15 -0
- package/dist/api/diagnostics.d.ts.map +1 -1
- package/dist/api/diagnostics.js +56 -0
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/api/elicitation-cache.d.ts +13 -0
- package/dist/api/elicitation-cache.d.ts.map +1 -1
- package/dist/api/elicitation-cache.js +12 -0
- package/dist/api/elicitation-cache.js.map +1 -1
- package/dist/api/eval-sample.d.ts +12 -12
- package/dist/api/frame-guard.d.ts +62 -0
- package/dist/api/frame-guard.d.ts.map +1 -0
- package/dist/api/frame-guard.js +97 -0
- package/dist/api/frame-guard.js.map +1 -0
- package/dist/api/governance-steering.d.ts +4 -4
- package/dist/api/multiscope.d.ts +3 -0
- package/dist/api/multiscope.d.ts.map +1 -1
- package/dist/api/multiscope.js +13 -0
- package/dist/api/multiscope.js.map +1 -1
- package/dist/api/needs.d.ts +6 -0
- package/dist/api/needs.d.ts.map +1 -1
- package/dist/api/needs.js +31 -4
- package/dist/api/needs.js.map +1 -1
- package/dist/api/product.d.ts.map +1 -1
- package/dist/api/product.js +13 -2
- package/dist/api/product.js.map +1 -1
- package/dist/api/project-aggregates.d.ts +6 -6
- package/dist/api/project-aggregates.d.ts.map +1 -1
- package/dist/api/project-aggregates.js +16 -24
- package/dist/api/project-aggregates.js.map +1 -1
- package/dist/api/requirements.d.ts +261 -6
- package/dist/api/requirements.d.ts.map +1 -1
- package/dist/api/requirements.js +344 -64
- package/dist/api/requirements.js.map +1 -1
- package/dist/api/routes.d.ts +156 -4
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +851 -78
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-brief.d.ts +78 -0
- package/dist/api/run-brief.d.ts.map +1 -0
- package/dist/api/run-brief.js +318 -0
- package/dist/api/run-brief.js.map +1 -0
- package/dist/api/run-files.d.ts.map +1 -1
- package/dist/api/run-files.js +5 -1
- package/dist/api/run-files.js.map +1 -1
- package/dist/api/run-timing-index.d.ts +27 -0
- package/dist/api/run-timing-index.d.ts.map +1 -1
- package/dist/api/run-timing-index.js +65 -0
- package/dist/api/run-timing-index.js.map +1 -1
- package/dist/api/seat-images.d.ts +102 -0
- package/dist/api/seat-images.d.ts.map +1 -0
- package/dist/api/seat-images.js +333 -0
- package/dist/api/seat-images.js.map +1 -0
- package/dist/api/server.d.ts +3 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +265 -2
- package/dist/api/server.js.map +1 -1
- package/dist/api/steering-landing.d.ts +48 -1
- package/dist/api/steering-landing.d.ts.map +1 -1
- package/dist/api/steering-landing.js +116 -42
- package/dist/api/steering-landing.js.map +1 -1
- package/dist/api/system-snapshot.d.ts +51 -2
- package/dist/api/system-snapshot.d.ts.map +1 -1
- package/dist/api/system-snapshot.js +151 -15
- package/dist/api/system-snapshot.js.map +1 -1
- package/dist/api/testing.d.ts +4 -4
- package/dist/api/testing.js +2 -2
- package/dist/api/testing.js.map +1 -1
- package/dist/apps/spec.d.ts +3009 -0
- package/dist/apps/spec.d.ts.map +1 -0
- package/dist/apps/spec.js +870 -0
- package/dist/apps/spec.js.map +1 -0
- package/dist/campaigns/routes.js +1 -1
- package/dist/campaigns/routes.js.map +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +12 -0
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +36 -1
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/deliver-text.d.ts.map +1 -1
- package/dist/core/deliver-text.js +22 -10
- package/dist/core/deliver-text.js.map +1 -1
- package/dist/core/estate-mcp-client.d.ts +7 -1
- package/dist/core/estate-mcp-client.d.ts.map +1 -1
- package/dist/core/estate-mcp-client.js +10 -2
- package/dist/core/estate-mcp-client.js.map +1 -1
- package/dist/core/exec.d.ts +12 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +23 -0
- package/dist/core/exec.js.map +1 -1
- package/dist/core/reassign-refusal.d.ts.map +1 -1
- package/dist/core/reassign-refusal.js +23 -3
- package/dist/core/reassign-refusal.js.map +1 -1
- package/dist/core/repoPaths.d.ts +32 -7
- package/dist/core/repoPaths.d.ts.map +1 -1
- package/dist/core/repoPaths.js +56 -9
- package/dist/core/repoPaths.js.map +1 -1
- package/dist/core/requirements-graph-setting.d.ts +32 -0
- package/dist/core/requirements-graph-setting.d.ts.map +1 -0
- package/dist/core/requirements-graph-setting.js +84 -0
- package/dist/core/requirements-graph-setting.js.map +1 -0
- package/dist/core/run-boot-resume.d.ts +4 -1
- package/dist/core/run-boot-resume.d.ts.map +1 -1
- package/dist/core/run-boot-resume.js +6 -1
- package/dist/core/run-boot-resume.js.map +1 -1
- package/dist/core/run-identity.d.ts.map +1 -1
- package/dist/core/run-identity.js +6 -0
- package/dist/core/run-identity.js.map +1 -1
- package/dist/core/types.d.ts +14 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/decisions/chat-recorder.d.ts +14 -3
- package/dist/decisions/chat-recorder.d.ts.map +1 -1
- package/dist/decisions/chat-recorder.js +54 -9
- package/dist/decisions/chat-recorder.js.map +1 -1
- package/dist/editors/csp.d.ts +24 -4
- package/dist/editors/csp.d.ts.map +1 -1
- package/dist/editors/csp.js +35 -8
- package/dist/editors/csp.js.map +1 -1
- package/dist/editors/registry.d.ts +46 -21
- package/dist/editors/registry.d.ts.map +1 -1
- package/dist/editors/registry.js +13 -2
- package/dist/editors/registry.js.map +1 -1
- package/dist/editors/routes.d.ts +4 -1
- package/dist/editors/routes.d.ts.map +1 -1
- package/dist/editors/routes.js +30 -3
- package/dist/editors/routes.js.map +1 -1
- package/dist/editors/visuals.d.ts +51 -0
- package/dist/editors/visuals.d.ts.map +1 -0
- package/dist/editors/visuals.js +164 -0
- package/dist/editors/visuals.js.map +1 -0
- package/dist/editors/webrtc-guard.d.ts +94 -0
- package/dist/editors/webrtc-guard.d.ts.map +1 -0
- package/dist/editors/webrtc-guard.js +506 -0
- package/dist/editors/webrtc-guard.js.map +1 -0
- package/dist/inspect/reading.d.ts +110 -0
- package/dist/inspect/reading.d.ts.map +1 -0
- package/dist/inspect/reading.js +325 -0
- package/dist/inspect/reading.js.map +1 -0
- package/dist/inspect/routes.d.ts +14 -0
- package/dist/inspect/routes.d.ts.map +1 -0
- package/dist/inspect/routes.js +44 -0
- package/dist/inspect/routes.js.map +1 -0
- package/dist/inspect/service.d.ts +161 -0
- package/dist/inspect/service.d.ts.map +1 -0
- package/dist/inspect/service.js +466 -0
- package/dist/inspect/service.js.map +1 -0
- package/dist/interactive/bridge-pool.d.ts +11 -2
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +10 -1
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/doc-binding.d.ts +156 -0
- package/dist/interactive/doc-binding.d.ts.map +1 -0
- package/dist/interactive/doc-binding.js +277 -0
- package/dist/interactive/doc-binding.js.map +1 -0
- package/dist/interactive/doc-delete-routes.d.ts +3 -0
- package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
- package/dist/interactive/doc-delete-routes.js +4 -0
- package/dist/interactive/doc-delete-routes.js.map +1 -1
- package/dist/interactive/draft-events.d.ts +89 -3
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +441 -30
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/draft-skill.d.ts +7 -0
- package/dist/interactive/draft-skill.d.ts.map +1 -1
- package/dist/interactive/draft-skill.js +10 -3
- package/dist/interactive/draft-skill.js.map +1 -1
- package/dist/interactive/edit-events.d.ts +2 -0
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +11 -0
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/interactive/proposal-create.d.ts +143 -0
- package/dist/interactive/proposal-create.d.ts.map +1 -0
- package/dist/interactive/proposal-create.js +318 -0
- package/dist/interactive/proposal-create.js.map +1 -0
- package/dist/interactive/proxy-routes.d.ts +60 -3
- package/dist/interactive/proxy-routes.d.ts.map +1 -1
- package/dist/interactive/proxy-routes.js +217 -3
- package/dist/interactive/proxy-routes.js.map +1 -1
- package/dist/interactive/source-packet.d.ts +126 -0
- package/dist/interactive/source-packet.d.ts.map +1 -0
- package/dist/interactive/source-packet.js +482 -0
- package/dist/interactive/source-packet.js.map +1 -0
- package/dist/learnings/estate.d.ts +16 -0
- package/dist/learnings/estate.d.ts.map +1 -0
- package/dist/learnings/estate.js +68 -0
- package/dist/learnings/estate.js.map +1 -0
- package/dist/learnings/extract.d.ts +56 -0
- package/dist/learnings/extract.d.ts.map +1 -0
- package/dist/learnings/extract.js +252 -0
- package/dist/learnings/extract.js.map +1 -0
- package/dist/learnings/recall.d.ts +43 -0
- package/dist/learnings/recall.d.ts.map +1 -0
- package/dist/learnings/recall.js +104 -0
- package/dist/learnings/recall.js.map +1 -0
- package/dist/learnings/service.d.ts +183 -0
- package/dist/learnings/service.d.ts.map +1 -0
- package/dist/learnings/service.js +571 -0
- package/dist/learnings/service.js.map +1 -0
- package/dist/mcp/ask-server.d.ts.map +1 -1
- package/dist/mcp/ask-server.js +37 -4
- package/dist/mcp/ask-server.js.map +1 -1
- package/dist/mcp/broker.d.ts +12 -1
- package/dist/mcp/broker.d.ts.map +1 -1
- package/dist/mcp/broker.js +24 -5
- package/dist/mcp/broker.js.map +1 -1
- package/dist/mcp/invoke.d.ts +26 -2
- package/dist/mcp/invoke.d.ts.map +1 -1
- package/dist/mcp/invoke.js +51 -5
- package/dist/mcp/invoke.js.map +1 -1
- package/dist/persona/routes.d.ts +92 -0
- package/dist/persona/routes.d.ts.map +1 -0
- package/dist/persona/routes.js +69 -0
- package/dist/persona/routes.js.map +1 -0
- package/dist/persona/store.d.ts +104 -0
- package/dist/persona/store.d.ts.map +1 -0
- package/dist/persona/store.js +176 -0
- package/dist/persona/store.js.map +1 -0
- package/dist/persona/voice.d.ts +4 -0
- package/dist/persona/voice.d.ts.map +1 -0
- package/dist/persona/voice.js +58 -0
- package/dist/persona/voice.js.map +1 -0
- package/dist/projects/routes.d.ts +2 -2
- package/dist/setup/known.d.ts +56 -0
- package/dist/setup/known.d.ts.map +1 -0
- package/dist/setup/known.js +159 -0
- package/dist/setup/known.js.map +1 -0
- package/dist/setup/routes.d.ts +13 -0
- package/dist/setup/routes.d.ts.map +1 -0
- package/dist/setup/routes.js +20 -0
- package/dist/setup/routes.js.map +1 -0
- package/dist/standing-orders/history.d.ts +2 -0
- package/dist/standing-orders/history.d.ts.map +1 -1
- package/dist/standing-orders/history.js +1 -0
- package/dist/standing-orders/history.js.map +1 -1
- package/dist/studio/assets/WickedSans-Regular-fb698rcD.woff2 +0 -0
- package/dist/studio/assets/WickedSans-SemiBold-CD53n1AC.woff2 +0 -0
- package/dist/studio/assets/WickedScript-Regular-CY5juO6I.woff2 +0 -0
- package/dist/studio/assets/WickedSerif-Regular-mdPEjaVZ.woff2 +0 -0
- package/dist/studio/assets/index-BUZ_-Syk.css +32 -0
- package/dist/studio/assets/index-u4O_7N-b.js +986 -0
- package/dist/studio/editors/wicked-deck/editor.json +6 -1
- package/dist/studio/editors/wicked-deck/icon.svg +1 -0
- package/dist/studio/editors/wicked-deck/index.html +1 -1
- package/dist/studio/editors/wicked-doc/editor.json +6 -1
- package/dist/studio/editors/wicked-doc/icon.svg +1 -0
- package/dist/studio/editors/wicked-doc/index.html +1 -1
- package/dist/studio/editors/wicked-insights/editor.json +6 -1
- package/dist/studio/editors/wicked-insights/icon.svg +1 -0
- package/dist/studio/editors/wicked-insights/index.html +1 -1
- package/dist/studio/editors/wicked-page/editor.json +6 -1
- package/dist/studio/editors/wicked-page/icon.svg +1 -0
- package/dist/studio/editors/wicked-page/index.html +1 -1
- package/dist/studio/editors/wicked-video/editor.json +6 -1
- package/dist/studio/editors/wicked-video/icon.svg +1 -0
- package/dist/studio/editors/wicked-video/index.html +1 -1
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +5155 -2707
- package/dist/voice/dashboard.d.ts +376 -0
- package/dist/voice/dashboard.d.ts.map +1 -0
- package/dist/voice/dashboard.js +92 -0
- package/dist/voice/dashboard.js.map +1 -0
- package/dist/voice/inject-input.d.ts +51 -0
- package/dist/voice/inject-input.d.ts.map +1 -0
- package/dist/voice/inject-input.js +126 -0
- package/dist/voice/inject-input.js.map +1 -0
- package/dist/voice/routes.d.ts +41 -0
- package/dist/voice/routes.d.ts.map +1 -0
- package/dist/voice/routes.js +116 -0
- package/dist/voice/routes.js.map +1 -0
- package/dist/voice/service.d.ts +177 -0
- package/dist/voice/service.d.ts.map +1 -0
- package/dist/voice/service.js +400 -0
- package/dist/voice/service.js.map +1 -0
- package/dist/workflow-catalog/build.d.ts +35 -0
- package/dist/workflow-catalog/build.d.ts.map +1 -0
- package/dist/workflow-catalog/build.js +229 -0
- package/dist/workflow-catalog/build.js.map +1 -0
- package/dist/workflow-catalog/catalog-data.d.ts +36 -0
- package/dist/workflow-catalog/catalog-data.d.ts.map +1 -0
- package/dist/workflow-catalog/catalog-data.js +430 -0
- package/dist/workflow-catalog/catalog-data.js.map +1 -0
- package/dist/working-doc/event-lines.d.ts +76 -0
- package/dist/working-doc/event-lines.d.ts.map +1 -0
- package/dist/working-doc/event-lines.js +247 -0
- package/dist/working-doc/event-lines.js.map +1 -0
- package/dist/working-doc/routes.d.ts +1 -1
- package/dist/working-doc/routes.d.ts.map +1 -1
- package/dist/working-doc/routes.js +2 -1
- package/dist/working-doc/routes.js.map +1 -1
- package/dist/working-doc/sections.d.ts +3 -1
- package/dist/working-doc/sections.d.ts.map +1 -1
- package/dist/working-doc/sections.js +4 -2
- package/dist/working-doc/sections.js.map +1 -1
- package/dist/working-doc/service.d.ts +66 -1
- package/dist/working-doc/service.d.ts.map +1 -1
- package/dist/working-doc/service.js +298 -22
- package/dist/working-doc/service.js.map +1 -1
- package/dist/working-doc/store.d.ts +8 -2
- package/dist/working-doc/store.d.ts.map +1 -1
- package/dist/working-doc/store.js.map +1 -1
- package/endpoint-manifest.json +205 -2
- package/package.json +5 -4
- package/workflows/distill-learnings.json +27 -0
- package/workflows/inspect-unit.json +27 -0
- package/dist/studio/assets/index-B-f_VEL2.css +0 -32
- package/dist/studio/assets/index-DhkxIK-0.js +0 -845
package/dist/api/routes.js
CHANGED
|
@@ -3,40 +3,58 @@ import { busSeamsOffWarning, busUnavailableWarning } from '../core/bus-notice.js
|
|
|
3
3
|
import { vaultBinWarning, vaultDiagnostics } from '../core/vault-bin.js';
|
|
4
4
|
import { PlanSchema, toLaunchPlan } from './plan-schema.js';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import {
|
|
6
|
+
import { listRequirementsRead, getRequirement, patchRequirement, readRequirementsGraph, REQUIREMENTS_NOT_GENERATED, } from './requirements.js';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
|
-
import { readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
9
|
+
import { readFileSync, existsSync, mkdirSync, realpathSync } from 'node:fs';
|
|
10
10
|
import { promises as fsp } from 'node:fs';
|
|
11
|
-
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
11
|
+
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { videoPalette } from '../appearance/video-palette.js';
|
|
14
14
|
import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, GovernanceReplayUnsupportedError, PlanLaunchUnsupportedError, SteeringUnsupportedError, addonSupportsQeOverride, addonSupportsReducedAssurance, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
|
|
15
|
-
import { codeGraphDb, codeGraphErrorStatus,
|
|
15
|
+
import { codeGraphDb, codeGraphErrorStatus, repoFolderMissing, repoFolderMissingBody } from '../core/repoPaths.js';
|
|
16
16
|
import { resolveCursorUnit } from '../core/cursor.js';
|
|
17
17
|
/** Create `dir` (private) when missing and answer it — a read root must exist when a run launches. */
|
|
18
18
|
function ensureDir(dir) {
|
|
19
19
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
20
20
|
return dir;
|
|
21
21
|
}
|
|
22
|
+
/** DES-CHAT-ARTIFACTS-001 §5.1: the chat's `open/` and `context/` read roots, made and proven real beneath its scratch
|
|
23
|
+
* root, as the seat sees them; none where the platform cannot open beneath a root, or when they cannot be made. */
|
|
24
|
+
function turnReadRoots(scratchRoot) {
|
|
25
|
+
if (!beneathSupported())
|
|
26
|
+
return [];
|
|
27
|
+
try {
|
|
28
|
+
ensureRealDir(contextReadRoots(scratchRoot).open);
|
|
29
|
+
ensureRealDir(contextReadRoots(scratchRoot).context);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const real = seatPathOf(scratchRoot);
|
|
35
|
+
return [join(real, 'open'), join(real, 'context')];
|
|
36
|
+
}
|
|
22
37
|
import { detectRefusal, isOpenGate, offeredChoices, replayOpenGate } from './gate-cache.js';
|
|
23
38
|
import { acceptanceRequirementOf, buildAcceptanceView } from '../qe/acceptance.js';
|
|
24
39
|
import { runIdentityOf, wireIdentity } from '../core/run-identity.js';
|
|
40
|
+
import { buildWorkflowCatalog } from '../workflow-catalog/build.js';
|
|
25
41
|
import { buildEvidenceBundle, coreUnitId, evidenceFilename } from './evidence.js';
|
|
26
42
|
import { attemptUnavailableReason, capturedAttempts, outputUnavailableReason, resolveUnit, storedAttempt, unitKeysFor } from './unit-output.js';
|
|
27
43
|
import { execCapped, ExecOutputTooLarge } from '../core/exec.js';
|
|
28
|
-
import { callEstateTool, EstateMcpError, memoryStoreInfo } from '../core/estate-mcp-client.js';
|
|
44
|
+
import { callEstateTool, EstateMcpError, ESTATE_INSTALL_HINT, memoryStoreInfo } from '../core/estate-mcp-client.js';
|
|
45
|
+
import { NO_LEARNINGS_WORKFLOWS } from '../learnings/service.js';
|
|
29
46
|
import { SeatHealthTracker } from './seat-health.js';
|
|
30
47
|
import { applyWorkerConfigRoot, signedInHeuristic } from './seat-signin.js';
|
|
31
48
|
import { applyCouncilSettings, isCouncilSize, isCouncilStaggerMs } from '../core/council-settings.js';
|
|
49
|
+
import { applyRequirementsGraphSettings, isRequirementsGraphSettings, REQUIREMENTS_GRAPH_SETTINGS_RULE, } from '../core/requirements-graph-setting.js';
|
|
32
50
|
import { defaultLaunchRoster, markOptIn } from '../core/opt-in-seats.js';
|
|
33
|
-
import { chatSeatAdmission } from './seat-standing.js';
|
|
51
|
+
import { chatSeatAdmission, homeless } from './seat-standing.js';
|
|
34
52
|
import { seatGovernanceDiagnostics, seatGovernanceWarnings, } from './seat-governance.js';
|
|
35
53
|
import { seatWorkDefaultNow } from './seat-work-default.js';
|
|
36
54
|
import { ROSTER_REFRESH_WAIT_MS, rosterWithStandingFactory } from './roster-standing.js';
|
|
37
55
|
import { SeatContainerStatus, gatewayDirOf, isContainerLoginCli, isContainerSeatFloor, isHostSeatFloor, loginDetail, runSeatLogin, seatLoginDepsFor, } from './seat-container-login.js';
|
|
38
56
|
import { ChatTurnIndex } from './chat-turns.js';
|
|
39
|
-
import { ChatScopeIndex, chatScopeDeps, chatScopeStatement, prepareChatScratch, removeChatScratch, namedKindRefusal, resolveChatScope, } from './chat-scope.js';
|
|
57
|
+
import { CHAT_ID, chatMakeDigest, ChatScopeIndex, chatScopeDeps, chatScopeStatement, prepareChatScratch, removeChatScratch, namedKindRefusal, resolveChatScope, } from './chat-scope.js';
|
|
40
58
|
import { ASK_TURN_BUDGET_SECS, AskPathIndex, isAskTurn } from './ask-paths.js';
|
|
41
59
|
import { chatTurnStep, resolveChatWorkflow, servedChatWorkflows, writeCapableStep } from './chat-workflow.js';
|
|
42
60
|
import { reassignRefusedSeats } from '../core/reassign-refusal.js';
|
|
@@ -55,7 +73,7 @@ import { registerTeamRoutes } from '../team/routes.js';
|
|
|
55
73
|
import { registerCampaignRoutes } from '../campaigns/routes.js';
|
|
56
74
|
import { registerGovernanceWikiRoutes } from './governance-wiki.js';
|
|
57
75
|
import { DEFAULT_STEERING_TYPE, STEERING_TYPE_VALUES, STEERING_TYPES, registerGovernanceSteeringRoutes, } from './governance-steering.js';
|
|
58
|
-
import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js';
|
|
76
|
+
import { atProposeGate, draftedSteeringRules, isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js';
|
|
59
77
|
import { inventoryComplete, parseInventoryClaims } from '../core/inventory-claim.js';
|
|
60
78
|
import { ApproveEditSchema, approveEdited, captureLaunchRoots, editInFlight, FileProposalSchema, registerCaptureRoutes } from './capture.js';
|
|
61
79
|
import { registerTestingRoutes } from './testing.js';
|
|
@@ -86,21 +104,31 @@ import { composeDeliverText, configuredPublicOrigin, extractFollowUps, factsFrom
|
|
|
86
104
|
import { deliverIdentityFor, deliverRepoFor, deliverTargetView, isGitHubLogin, pushIdentityOf, readDeliverOriginUrl, resolvePullRequest as resolvePullRequestViaGh, unverifiedTreesFrom, } from '../core/deliver.js';
|
|
87
105
|
import { credentialsMissingIn, deliverCredentialsProbe, missingCredentials } from '../core/deliver-credentials.js';
|
|
88
106
|
import { codebaseArchiveUrl } from './codebase-archive.js';
|
|
89
|
-
import {
|
|
107
|
+
import { projectRepoCandidates } from '../interactive/doc-grounding.js';
|
|
108
|
+
import { artifactStatus, beneathSupported, buildTurnContext, chatMount, ChatMessageContextSchema, contextReadRoots, ensureRealDir, sha256Hex, parseArtifactRef, readGroundingBeneath, seatPathOf, UNFILED_MOUNT, } from './chat-context.js';
|
|
109
|
+
import { createDocInProcess, registerInteractiveProxy } from '../interactive/proxy-routes.js';
|
|
110
|
+
import { CreateLauncher } from '../interactive/proposal-create.js';
|
|
111
|
+
import { buildRunBrief } from './run-brief.js';
|
|
112
|
+
import { readBeneath } from './beneath.js';
|
|
113
|
+
/** A context brief copied into a document's packet, at most (the packet's per-file cap). */
|
|
114
|
+
const BRIEF_COPY_MAX = 512 * 1024;
|
|
90
115
|
import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
|
|
91
116
|
import { registerInteractiveDocChecks } from '../interactive/doc-checks-routes.js';
|
|
92
117
|
import { crewStateHome, stateHomeOfDb } from '../projects/state-home.js';
|
|
118
|
+
import { ChatProposals, LaunchOnceRegistry, RecordingStubDispatcher, runCostFromEvents } from './chat-proposals.js';
|
|
119
|
+
import { CATALOG_SEEDS } from '../workflow-catalog/catalog-data.js';
|
|
93
120
|
import { registerInteractiveDocList } from '../interactive/doc-list-routes.js';
|
|
94
121
|
import { REVIEWS_DIRNAME } from '../interactive/review-ledger.js';
|
|
95
122
|
import { MembershipIndex } from '../projects/membership-index.js';
|
|
96
123
|
import { MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
|
|
97
124
|
import { resolveLinkedIssues } from '../core/linked-issues.js';
|
|
98
125
|
import { AuditLog } from './audit.js';
|
|
99
|
-
import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion, hostSandboxLauncher, seatRosterFile, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
|
|
126
|
+
import { AcpFoldCache, EngineVersionCache, FloorCacheCache, eventsDirOf, installedPackageVersion, hostSandboxLauncher, seatRosterFile, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
|
|
100
127
|
import { GovernanceDiagnostics } from './governance-health.js';
|
|
101
128
|
import { ATTACHMENT_LIMITS, ATTACHMENT_MAX_MESSAGE_BYTES, AttachmentError, ChatAttachmentStore, attachmentWire, attachmentsBlock, boundaryOf, parseMultipart, stageForSeat } from './chat-attachments.js';
|
|
102
129
|
import { CHAT_HISTORY_MAX_LIMIT, ChatHistoryStore, decodeCursor } from './chat-history.js';
|
|
103
|
-
import { PROBE_TIMEOUT_MS, SYSTEM_BUDGET_MS, SYSTEM_STATS_DAYS, SYSTEM_STATS_TTL_MS, WeekFoldCache, assembleSnapshot, buildNeeds, buildClis, buildGovernance, diskRow, estateReadonlyCall, infraRow, judgeGaps, livenessThresholds, loadRow, probeBroker, probeBus, probeEstate, probeGateway, probeInteractive, probeVault, readBridgeLockAsync, readWeek, resolveContainerRuntime, runLiveness, settleWithin, skillsRow, sortRuns, stateHomeRow, unfinished, } from './system-snapshot.js';
|
|
130
|
+
import { PROBE_TIMEOUT_MS, SYSTEM_BUDGET_MS, SYSTEM_STATS_DAYS, SYSTEM_STATS_TTL_MS, WeekFoldCache, assembleSnapshot, buildNeeds, buildClis, buildGovernance, diskRow, estateReadonlyCall, infraRow, judgeGaps, livenessThresholds, loadRow, probeBroker, probeBus, probeEstate, probeGateway, probeInteractive, probeVault, readBridgeLockAsync, readWeek, resolveContainerRuntime, runLiveness, settleWithin, skillsRow, sortRuns, stateHomeRow, reposRow, unfinished, } from './system-snapshot.js';
|
|
131
|
+
import { parseSeatImageStatus, probeSeatImages, SeatImageRebuilder } from './seat-images.js';
|
|
104
132
|
import { RunCostCache } from './needs.js';
|
|
105
133
|
import { governanceStoreFix, legacyHomeOutboxPath } from '../core/governance-store.js';
|
|
106
134
|
import { UsageError, replayOutboxInto } from '../cli/governance.js';
|
|
@@ -109,6 +137,10 @@ import { RetryIndex } from './retry-index.js';
|
|
|
109
137
|
import { SEAT_RECORD_DEFAULT_DAYS, SEAT_RECORD_MAX_DAYS, seatRecord } from './seat-record.js';
|
|
110
138
|
import { jobTitle } from './job-title.js';
|
|
111
139
|
import { DeliveryFreeze, frozenRefusal, isDeliverUnit, registerDeliveryFreezeRoutes } from './delivery-freeze.js';
|
|
140
|
+
import { VoiceService, defaultVoiceDir } from '../voice/service.js';
|
|
141
|
+
import { VoiceDashboardStore } from '../voice/dashboard.js';
|
|
142
|
+
import { registerVoiceRoutes } from '../voice/routes.js';
|
|
143
|
+
import { VoiceInjectInputs } from '../voice/inject-input.js';
|
|
112
144
|
import { GroupIndex } from './group-index.js';
|
|
113
145
|
import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
|
|
114
146
|
import { GuidanceIndex } from './guidance-index.js';
|
|
@@ -173,6 +205,18 @@ function codeGraphDbOr503(repo, reply) {
|
|
|
173
205
|
return null;
|
|
174
206
|
}
|
|
175
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* `true` after answering **409** when the repo's registered folder is gone, BEFORE anything spawns
|
|
210
|
+
* there: Node reports a missing spawn `cwd` as `spawn <binary> ENOENT`, which named an installed
|
|
211
|
+
* binary as the fault (rig, tiny-math). The words are the finding's, the same everywhere.
|
|
212
|
+
*/
|
|
213
|
+
function refusedMissingFolder(repo, reply) {
|
|
214
|
+
const missing = repoFolderMissing(repo);
|
|
215
|
+
if (missing === null)
|
|
216
|
+
return false;
|
|
217
|
+
void reply.code(409).send(repoFolderMissingBody(missing));
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
176
220
|
function message(err) {
|
|
177
221
|
return err instanceof Error ? err.message : String(err);
|
|
178
222
|
}
|
|
@@ -588,7 +632,15 @@ export const GateSchema = z.object({
|
|
|
588
632
|
/** api-types 0.44.0 — the gate this decision answers (its unit `ord`). A mismatch with the run's
|
|
589
633
|
* open gate is a 409 `gate_changed`: the decision was made on a gate that is no longer open. */
|
|
590
634
|
ord: z.number().int().nonnegative().optional(),
|
|
591
|
-
|
|
635
|
+
/** (api-types 0.156.0, DES-VOICE) The person answered by voice and confirmed it on screen: recorded on the
|
|
636
|
+
* `gate.decided` audit line and the decided-gate history ("by voice, confirmed"). It grants nothing. */
|
|
637
|
+
input: z.literal('voice').optional(),
|
|
638
|
+
/** (program DES-LEARN §3.1, api-types 0.160.0) On a steering-author propose gate: the drafted rule ids
|
|
639
|
+
* (`GET /governance/steering/author/:runId/draft`) the person keeps. Only those land; the rest are
|
|
640
|
+
* declined. Absent = every drafted rule lands, as before. 409 `no_draft` unless the run waits at its
|
|
641
|
+
* steering-author propose gate with a readable draft; 400 with anything but a plain approve. */
|
|
642
|
+
keepRules: z.array(z.string().min(1).max(256)).max(500).optional(),
|
|
643
|
+
}).strict().refine((b) => b.keepRules === undefined || (b.approve && (b.action === undefined || b.action === 'approve') && b.plan === undefined), { message: 'keepRules goes with a plain approve (no action other than approve, no plan) of a steering-author propose gate', path: ['keepRules'] }).refine((b) => b.action === undefined
|
|
592
644
|
|| (b.action === 'approve' || b.action === 'edit_plan' || b.action === 'amend_intent' || b.action === OPERATOR_JUDGMENT_ACTION || b.action === SKIP_QE_ACTION || isEscalationAction(b.action)) === b.approve, { message: '`action` disagrees with `approve`: request_changes and reject require approve: false; approve, edit_plan, amend_intent, operator_judgment, skip_qe, extend, targeted, accept_partial, accept_suggestion, rejudge and consent:<id> require approve: true', path: ['action'] }).refine(
|
|
593
645
|
// (operator ruling 2026-10-21) The override's rationale IS its record: a blank one is a 400 here,
|
|
594
646
|
// before the engine is asked.
|
|
@@ -607,10 +659,19 @@ export const GuidanceSchema = z.object({
|
|
|
607
659
|
}).strict();
|
|
608
660
|
/** ~8KB — a guidance note is operator prose, not a document store. */
|
|
609
661
|
const GUIDANCE_MAX_BYTES = 8192;
|
|
662
|
+
/** `POST /runs/:id/cancel` (api-types `CancelRunRequest`): optional, so a bodiless cancel stays valid. */
|
|
663
|
+
const CancelSchema = z.object({
|
|
664
|
+
reason: z.string().trim().max(2000).optional(),
|
|
665
|
+
input: z.literal('voice').optional(),
|
|
666
|
+
}).strict();
|
|
610
667
|
const InjectSchema = z.object({
|
|
611
668
|
message: z.string().min(1),
|
|
612
669
|
/** `"all"` broadcasts to every active worker; any other value is a CLI key. */
|
|
613
670
|
target: z.string().min(1).default('all'),
|
|
671
|
+
/** (api-types 0.161.0, DES-VOICE) The person said this note and confirmed it on screen. A person's only (403 for
|
|
672
|
+
* any other actor); kept on the `run.injected` audit line and stamped on the note's queued/injected frames so the
|
|
673
|
+
* thread says "You (by voice)". It grants nothing a typed note does not. */
|
|
674
|
+
input: z.literal('voice').optional(),
|
|
614
675
|
}).strict();
|
|
615
676
|
// The manual reassign lever (crew#442): `cli` omitted lets the engine's council re-pick, the
|
|
616
677
|
// same shape the automatic stall-watchdog escalation uses for an unknown failover target.
|
|
@@ -740,6 +801,9 @@ export const ChatOpenSchema = z.object({
|
|
|
740
801
|
/** (api-types 0.141.0, crew#1157) The read-only workflow every turn runs (`analyze`); see
|
|
741
802
|
* `chat-workflow.ts`. Omitted ⇒ a plain chat. */
|
|
742
803
|
workflow: z.string().min(1).max(64).optional(),
|
|
804
|
+
/** (api-types 0.169.0, DES-CHAT-ARTIFACTS-001 §6.3) "Ask about this" on a document no open chat holds: seed the
|
|
805
|
+
* chat's repositories from the document's recorded binding, after an access check. */
|
|
806
|
+
seedFromArtifact: z.string().max(200).optional(),
|
|
743
807
|
}).strict();
|
|
744
808
|
/**
|
|
745
809
|
* The `singleSeat` disclosure (crew#641, hole closed in crew#650) — built in ONE place so the 201
|
|
@@ -837,6 +901,17 @@ runtime = {}) {
|
|
|
837
901
|
const seatHealth = runtime.seatHealth ?? new SeatHealthTracker();
|
|
838
902
|
const deliveryFreeze = runtime.deliveryFreeze ?? new DeliveryFreeze();
|
|
839
903
|
registerDeliveryFreezeRoutes(app, { freeze: deliveryFreeze, audit });
|
|
904
|
+
const voice = runtime.voice ?? {
|
|
905
|
+
service: new VoiceService({ dir: defaultVoiceDir() }),
|
|
906
|
+
dashboard: (() => { try {
|
|
907
|
+
return VoiceDashboardStore.under(crewStateHome());
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
return null;
|
|
911
|
+
} })(),
|
|
912
|
+
};
|
|
913
|
+
registerVoiceRoutes(app, { voice: voice.service, dashboard: voice.dashboard, audit });
|
|
914
|
+
const voiceInputs = runtime.voiceInputs ?? new VoiceInjectInputs();
|
|
840
915
|
const openWithOs = runtime.openWithOs ?? openWithSystemDefault;
|
|
841
916
|
const signedIn = runtime.signedIn ?? signedInHeuristic;
|
|
842
917
|
const retryIndex = runtime.retryIndex ?? new RetryIndex();
|
|
@@ -845,10 +920,35 @@ runtime = {}) {
|
|
|
845
920
|
const guidanceIndex = runtime.guidanceIndex ?? new GuidanceIndex();
|
|
846
921
|
const chatScopes = runtime.chatScopes ?? new ChatScopeIndex();
|
|
847
922
|
const chatTurns = runtime.chatTurns ?? new ChatTurnIndex();
|
|
923
|
+
// DES-LEARN §3.3: the voice a chat in this project is told (the project's override, else the person's).
|
|
924
|
+
const voiceFor = (projectId) => {
|
|
925
|
+
const v = runtime.personaVoice?.(projectId ?? null) ?? null;
|
|
926
|
+
return v === null ? {} : { voice: v };
|
|
927
|
+
};
|
|
848
928
|
const askPaths = runtime.askPaths ?? new AskPathIndex();
|
|
849
929
|
const chatTranscripts = runtime.chatTranscripts;
|
|
850
930
|
const chatAttachments = runtime.chatAttachments ?? new ChatAttachmentStore();
|
|
851
931
|
const chatHistory = runtime.chatHistory ?? (chatTranscripts !== undefined ? new ChatHistoryStore(chatTranscripts, chatAttachments) : null);
|
|
932
|
+
// DES-CHAT-ARTIFACTS-001 §14 (A2): what a chat can do with artifacts. Production: `proposals` on and every operation
|
|
933
|
+
// off — each later slice turns its own on and registers its launcher. The TEST configuration (§13 A2) turns every
|
|
934
|
+
// operation's resolution on and registers a recording stub that starts nothing.
|
|
935
|
+
const chatArtifactsStub = runtime.chatArtifactsStub === true;
|
|
936
|
+
// A3: a real `create` exists whenever the draft seam can launch a proposal-bound draft (the launcher is registered
|
|
937
|
+
// below, once the bridge pool exists). Production advertises it; the test configuration keeps the recording stub only
|
|
938
|
+
// for the operations whose slices have not landed.
|
|
939
|
+
const realCreate = runtime.docBindings !== undefined && runtime.launchBoundDraft !== undefined;
|
|
940
|
+
const stubOps = realCreate ? ['refine', 'theme', 'demo'] : ['create', 'refine', 'theme', 'demo'];
|
|
941
|
+
const chatArtifactFlags = chatArtifactsStub
|
|
942
|
+
? { context: true, proposals: true, create: true, refine: true, theme: true, demo: true, stub: true, stubOps }
|
|
943
|
+
: { context: true, proposals: true, create: realCreate, refine: false, theme: false, demo: false };
|
|
944
|
+
const proposalLaunchers = new LaunchOnceRegistry();
|
|
945
|
+
if (chatArtifactsStub) {
|
|
946
|
+
// Under the registered `chats` entry (a new top-level state-home entry would make core refuse every launch), and
|
|
947
|
+
// not a `.jsonl` there (the boot sweep clears orphaned transcripts by that suffix).
|
|
948
|
+
const stub = new RecordingStubDispatcher(join(crewStateHome(), 'chats', 'proposal-stub', 'dispatches.ndjson'));
|
|
949
|
+
for (const kind of stubOps)
|
|
950
|
+
proposalLaunchers.register(kind, stub);
|
|
951
|
+
}
|
|
852
952
|
const deliveryIndex = runtime.deliveryIndex ?? new DeliveryIndex();
|
|
853
953
|
// Wave 6: the doc↔run binding and the registered test sets — `createServer` injects the real
|
|
854
954
|
// ones; a directly-driven route set gets an empty index (every run `document_id: null`, no sets).
|
|
@@ -1157,6 +1257,15 @@ runtime = {}) {
|
|
|
1157
1257
|
const chatId = runTimingIndex.chatIdFor(view.session.id);
|
|
1158
1258
|
if (chatId !== undefined)
|
|
1159
1259
|
view.session.chat_id = chatId;
|
|
1260
|
+
// DES-CHAT-ARTIFACTS-001 (A3): the proposal a run answers, the artifact it makes, and a landing that failed.
|
|
1261
|
+
const proposalOf = runTimingIndex.proposalOf(view.session.id);
|
|
1262
|
+
if (proposalOf !== undefined) {
|
|
1263
|
+
view.session.proposal_id = proposalOf.proposalId;
|
|
1264
|
+
view.session.artifact_ref = proposalOf.artifactRef;
|
|
1265
|
+
}
|
|
1266
|
+
const landingFailed = runTimingIndex.landingFailedOf(view.session.id);
|
|
1267
|
+
if (landingFailed !== undefined)
|
|
1268
|
+
view.session.landing_failed = landingFailed;
|
|
1160
1269
|
// crew#552: the onboarding run a chained capture was launched from — ABSENT otherwise.
|
|
1161
1270
|
const chainedFrom = runTimingIndex.chainedFromOf(view.session.id);
|
|
1162
1271
|
if (chainedFrom !== undefined)
|
|
@@ -1325,6 +1434,15 @@ runtime = {}) {
|
|
|
1325
1434
|
workingDoc: app.hasRoute({ method: 'GET', url: `${V}/runs/:id/working-doc` }),
|
|
1326
1435
|
// (api-types 0.149.0) Power mode: the routes are served AND the engine runs it.
|
|
1327
1436
|
powerMode: powerSupported(),
|
|
1437
|
+
// (api-types 0.156.0, DES-VOICE) Voice mode's routes are served: local speech to text, the model with
|
|
1438
|
+
// consent, and the per-person dashboard. Whether transcription can run now is `GET /voice/status`.
|
|
1439
|
+
voice: app.hasRoute({ method: 'POST', url: `${V}/voice/transcribe` }),
|
|
1440
|
+
// (api-types 0.168.0, wf-catalog) The categorised workflow catalog with the operator's usage.
|
|
1441
|
+
workflowCatalog: app.hasRoute({ method: 'GET', url: `${V}/workflows/catalog` }),
|
|
1442
|
+
// (api-types 0.169.0, DES-CHAT-ARTIFACTS-001 §14) Per operation: A1 serves `context` (the chat sees the open
|
|
1443
|
+
// artifact and the runs the person names; `seedFromArtifact`); A2 (0.170.0) `proposals`. The operations are later
|
|
1444
|
+
// slices (all on, with `stub`, in the test configuration).
|
|
1445
|
+
chatArtifacts: { ...chatArtifactFlags },
|
|
1328
1446
|
};
|
|
1329
1447
|
// wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
|
|
1330
1448
|
// daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
|
|
@@ -1359,6 +1477,7 @@ runtime = {}) {
|
|
|
1359
1477
|
// can never turn this GET into an event-log re-reader or a `--version` spawner per request.
|
|
1360
1478
|
const acpFoldCache = new AcpFoldCache();
|
|
1361
1479
|
const engineVersionCache = new EngineVersionCache();
|
|
1480
|
+
const floorCacheCache = new FloorCacheCache();
|
|
1362
1481
|
// The governance block (crew#495): the store the engine's emit seam writes to (as the adapter
|
|
1363
1482
|
// exported it — `null` on a boot that resolved none), its record count through the engine
|
|
1364
1483
|
// binding when the addon has one, and the dead-letter outbox folded on change. Built at boot so
|
|
@@ -1370,13 +1489,14 @@ runtime = {}) {
|
|
|
1370
1489
|
// `dbPath` is a readonly field of the real adapter; a stub-driven route set may lack it,
|
|
1371
1490
|
// and diagnostics over an unknown store honestly reports no stores and no ACP record.
|
|
1372
1491
|
const dbPath = typeof adapter.dbPath === 'string' && adapter.dbPath !== '' ? adapter.dbPath : null;
|
|
1373
|
-
const [stores, byCli, engineBinaries, governance] = await Promise.all([
|
|
1492
|
+
const [stores, byCli, engineBinaries, governance, floorCache] = await Promise.all([
|
|
1374
1493
|
dbPath !== null ? listStoreFiles(dbPath) : Promise.resolve([]),
|
|
1375
1494
|
dbPath !== null
|
|
1376
1495
|
? acpFoldCache.get(eventsDirOf(dbPath))
|
|
1377
1496
|
: Promise.resolve({}),
|
|
1378
1497
|
engineVersionCache.get(),
|
|
1379
1498
|
governanceDiagnostics.health(),
|
|
1499
|
+
floorCacheCache.get(),
|
|
1380
1500
|
]);
|
|
1381
1501
|
const uptimeMs = Math.round(process.uptime() * 1000);
|
|
1382
1502
|
const addr = app.server.address();
|
|
@@ -1423,6 +1543,8 @@ runtime = {}) {
|
|
|
1423
1543
|
// crew#1068: the wicked-vault CLI handed to the engine, with a remedy when it has none.
|
|
1424
1544
|
// `null` on a route set driven without the real adapter.
|
|
1425
1545
|
vault: adapter.vaultBin != null ? vaultDiagnostics(adapter.vaultBin) : null,
|
|
1546
|
+
// crew#1147: the repo-checks floor cache's disk usage (cached 60 s); `null` on an addon without it.
|
|
1547
|
+
floorCache,
|
|
1426
1548
|
};
|
|
1427
1549
|
};
|
|
1428
1550
|
app.get(`${V}/diagnostics`, { config: { manifest: { responseType: 'DiagnosticsResponse', statusCodes: [200] } } }, async () => diagnosticsNow());
|
|
@@ -1948,6 +2070,8 @@ runtime = {}) {
|
|
|
1948
2070
|
const repo = repos.find((r) => r.id === id);
|
|
1949
2071
|
if (!repo)
|
|
1950
2072
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
2073
|
+
if (refusedMissingFolder(repo, reply))
|
|
2074
|
+
return reply;
|
|
1951
2075
|
const body = OnboardBodySchema.safeParse(req.body ?? {});
|
|
1952
2076
|
if (!body.success)
|
|
1953
2077
|
return reply.code(400).send(invalidBody(body.error, 'Invalid request body'));
|
|
@@ -2039,6 +2163,20 @@ runtime = {}) {
|
|
|
2039
2163
|
const garden = gardenRefusalBody();
|
|
2040
2164
|
if (garden !== null)
|
|
2041
2165
|
return reply.code(422).send(garden);
|
|
2166
|
+
// A run on a repo whose folder is gone could only fail at its first worktree or floor, with the
|
|
2167
|
+
// spawn's ENOENT for a reason: refused here by name, before the engine sees it.
|
|
2168
|
+
// A registry that cannot be read here refuses nothing: the engine judges the ref as before.
|
|
2169
|
+
if (b.repoRef !== undefined) {
|
|
2170
|
+
let target;
|
|
2171
|
+
try {
|
|
2172
|
+
target = (await adapter.listRepos()).find((r) => r.id === b.repoRef);
|
|
2173
|
+
}
|
|
2174
|
+
catch {
|
|
2175
|
+
target = undefined;
|
|
2176
|
+
}
|
|
2177
|
+
if (target !== undefined && refusedMissingFolder(target, reply))
|
|
2178
|
+
return reply;
|
|
2179
|
+
}
|
|
2042
2180
|
// crew#645: an expired login reads signed out BEFORE routing — wait (bounded) for every seat's
|
|
2043
2181
|
// missing or stale login check, so the standing below is the seats' own answer.
|
|
2044
2182
|
if (b.clisJson === undefined)
|
|
@@ -2198,6 +2336,12 @@ runtime = {}) {
|
|
|
2198
2336
|
else {
|
|
2199
2337
|
input.power = true;
|
|
2200
2338
|
input.humanConfirm = 'none';
|
|
2339
|
+
// DES-LEARN §3.3 / wicked-core#1216: the orchestrator talks to the person in their personality
|
|
2340
|
+
// for this project (the project's override, else theirs). The engine hands it to the
|
|
2341
|
+
// orchestrator only; reviewers, judges and QE never hear it.
|
|
2342
|
+
const { voice } = voiceFor(b.projectId);
|
|
2343
|
+
if (voice !== undefined)
|
|
2344
|
+
input.voice = voice;
|
|
2201
2345
|
}
|
|
2202
2346
|
}
|
|
2203
2347
|
// QE-IN-APP-WORKFLOWS: the operator's explicit skip (with its reason) or force — forwarded only
|
|
@@ -2289,6 +2433,20 @@ runtime = {}) {
|
|
|
2289
2433
|
catch (err) {
|
|
2290
2434
|
req.log.warn(`linked-issue resolution failed; launching on the intent alone: ${message(err)}`);
|
|
2291
2435
|
}
|
|
2436
|
+
// Run learnings: the approved learnings that apply to this repo/project ride in the brief, in a
|
|
2437
|
+
// marked block the deliver text takes back out (src/learnings/recall.ts). Workflow launches only,
|
|
2438
|
+
// for the linked-issues reason above; best-effort: an unreadable store launches without it.
|
|
2439
|
+
if (runtime.recallLearnings !== undefined && !NO_LEARNINGS_WORKFLOWS.has(b.workflow)) {
|
|
2440
|
+
try {
|
|
2441
|
+
const recalled = await runtime.recallLearnings(input.problem, b.repoRef ?? null, b.projectId ?? null);
|
|
2442
|
+
input.problem = recalled.problem;
|
|
2443
|
+
if (recalled.recalled > 0)
|
|
2444
|
+
req.log.info(`[learnings] ${input.sessionId}: ${recalled.recalled} approved learning(s) in the brief`);
|
|
2445
|
+
}
|
|
2446
|
+
catch (err) {
|
|
2447
|
+
req.log.warn(`[learnings] the memory store could not be read; launching without approved learnings: ${message(err)}`);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2292
2450
|
}
|
|
2293
2451
|
// DES-RUNS-REVIEW-001 §3: a hand launch of the run review (`/workflow-review-recent-work`) is prepared like a
|
|
2294
2452
|
// scheduled one: its snapshot written, repo-less, one seat, its review directory as the only write root.
|
|
@@ -2313,6 +2471,13 @@ runtime = {}) {
|
|
|
2313
2471
|
}
|
|
2314
2472
|
deliver = 'none';
|
|
2315
2473
|
}
|
|
2474
|
+
// DES-CHAT-ARTIFACTS-001 §4.3 (A3): one live job per chat, work and artifact runs together. A work launch filed under
|
|
2475
|
+
// a chat whose proposal's document attempt has no outcome yet is refused, as Go is refused the other way round.
|
|
2476
|
+
if (b.chatId !== undefined && chatArtifactFlags.proposals) {
|
|
2477
|
+
const live = await chatProposals.liveJobOf(b.chatId).catch(() => null);
|
|
2478
|
+
if (live !== null)
|
|
2479
|
+
return reply.code(409).send({ code: 'chat_busy', error: `This chat already has a job running (${live}). Wait, or stop that run.` });
|
|
2480
|
+
}
|
|
2316
2481
|
try {
|
|
2317
2482
|
const runId = await adapter.launchRun(input);
|
|
2318
2483
|
// crew#641/#642 (item 5): when a run is promoted from a chat, record how many seats
|
|
@@ -2654,6 +2819,47 @@ runtime = {}) {
|
|
|
2654
2819
|
await backfillEndedAt([run]);
|
|
2655
2820
|
return { run: decorateRun(run) };
|
|
2656
2821
|
});
|
|
2822
|
+
// Run learnings: what a run proposed to the approve queue (its end card, "3 learnings proposed · Review").
|
|
2823
|
+
// Proposals are attributed by provenance (`run_id`); a distil seat still reading the run says so.
|
|
2824
|
+
app.get(`${V}/runs/:id/learnings`, { config: { manifest: { responseType: 'RunLearningsResponse', statusCodes: [200, 404, 501, 502] } } }, async (req, reply) => {
|
|
2825
|
+
const { id } = req.params;
|
|
2826
|
+
if (runtime.runLearnings === undefined)
|
|
2827
|
+
return reply.code(501).send({ error: 'run learnings are not wired on this daemon' });
|
|
2828
|
+
if (!(await adapter.sessions()).includes(id))
|
|
2829
|
+
return reply.code(404).send({ error: 'Run not found' });
|
|
2830
|
+
try {
|
|
2831
|
+
const body = await runtime.runLearnings.view(id);
|
|
2832
|
+
return body;
|
|
2833
|
+
}
|
|
2834
|
+
catch (err) {
|
|
2835
|
+
return estateUpstreamError(reply, err);
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
// (program DES-LEARN §3.1) A steering-author run's DRAFTED rules, read before the person decides: the
|
|
2839
|
+
// same sources and the same normalization the landing uses, so the ids a person keeps are the ids
|
|
2840
|
+
// `POST /runs/:id/gate {keepRules}` names. `ready: false` until the propose step has run and its gate
|
|
2841
|
+
// is the run's open gate. Read-only.
|
|
2842
|
+
app.get(`${V}/governance/steering/author/:runId/draft`, { config: { manifest: { responseType: 'SteeringDraftResponse', statusCodes: [200, 404, 500] } } }, async (req, reply) => {
|
|
2843
|
+
const { runId } = req.params;
|
|
2844
|
+
const views = await adapter.sessionsDetail();
|
|
2845
|
+
const run = views.find((v) => v.session.id === runId);
|
|
2846
|
+
if (!run)
|
|
2847
|
+
return reply.code(404).send({ error: 'Run not found' });
|
|
2848
|
+
if (!isSteeringAuthorRun(run))
|
|
2849
|
+
return reply.code(404).send({ error: `run ${runId} is not a steering-author run`, code: 'not_steering_author' });
|
|
2850
|
+
const open = run.session.status === 'awaiting_human' ? await gateOfRecord(run, undefined) : null;
|
|
2851
|
+
const prompt = open !== null && !('refusal' in open) ? open.prompt : undefined;
|
|
2852
|
+
const draft = await draftedSteeringRules(adapter, run, prompt);
|
|
2853
|
+
if (draft !== null && 'unreadable' in draft)
|
|
2854
|
+
return reply.code(500).send({ error: draft.unreadable });
|
|
2855
|
+
const waiting = open !== null && !('refusal' in open) && atProposeGate(run);
|
|
2856
|
+
return {
|
|
2857
|
+
runId,
|
|
2858
|
+
ready: draft !== null && waiting,
|
|
2859
|
+
rules: draft?.rules ?? [],
|
|
2860
|
+
...(draft !== null ? { source: draft.source } : {}),
|
|
2861
|
+
};
|
|
2862
|
+
});
|
|
2657
2863
|
// ── Inventory claims (crew#721) — whether each enumerating step's list is complete ──
|
|
2658
2864
|
// A step that lists things a later step acts on ends its reply with a `wicked-inventory` block
|
|
2659
2865
|
// (wicked-garden core/refs/inventory-report.md). This reads them off every unit's captured
|
|
@@ -3062,7 +3268,7 @@ runtime = {}) {
|
|
|
3062
3268
|
manifest: {
|
|
3063
3269
|
requestType: 'ChatOpenBody',
|
|
3064
3270
|
responseType: 'ChatOpenResponse',
|
|
3065
|
-
statusCodes: [201, 400, 404, 409, 500, 501],
|
|
3271
|
+
statusCodes: [201, 400, 403, 404, 409, 500, 501],
|
|
3066
3272
|
},
|
|
3067
3273
|
},
|
|
3068
3274
|
}, async (req, reply) => {
|
|
@@ -3073,11 +3279,87 @@ runtime = {}) {
|
|
|
3073
3279
|
const b = parsed.data;
|
|
3074
3280
|
const chatId = b.chatId ?? randomUUID();
|
|
3075
3281
|
const requestedRefs = [...(b.repoRef !== undefined ? [b.repoRef] : []), ...(b.repoRefs ?? [])];
|
|
3282
|
+
// The scope kind as the open resolves it: the request's, unless an Ask-about-this seed names the repositories.
|
|
3283
|
+
let scopeKind = b.scopeKind;
|
|
3076
3284
|
// A named scope's SHAPE first (studio#323 R4, codex on #664): a body whose kind refuses its own
|
|
3077
|
-
// projectId/repoRefs is a 400 — never masked by the project lookup's 404/409 below.
|
|
3078
|
-
|
|
3285
|
+
// projectId/repoRefs is a 400 — never masked by the project lookup's 404/409 below, nor by a seed's reads.
|
|
3286
|
+
// (A seed asked to fill an empty `repo`/`repos` list is checked once it has filled it, below.)
|
|
3287
|
+
const seedFills = b.seedFromArtifact !== undefined && requestedRefs.length === 0 && (scopeKind === 'repo' || scopeKind === 'repos');
|
|
3288
|
+
const shape = seedFills ? null : namedKindRefusal(scopeKind, b.projectId, [...new Set(requestedRefs)]);
|
|
3079
3289
|
if (shape !== null)
|
|
3080
3290
|
return reply.code(400).send({ error: shape.error });
|
|
3291
|
+
// DES-CHAT-ARTIFACTS-001 §6.3 (A1): "Ask about this" on a document no open chat holds. Access first (a person,
|
|
3292
|
+
// a document that exists), then the scope rule: a chat in no project, or a project chat whose project is the
|
|
3293
|
+
// document's mount and holds every repository the document was made about. Only then are the chat's
|
|
3294
|
+
// repositories seeded from that binding — and only when the request names none and no kind that refuses them.
|
|
3295
|
+
if (b.seedFromArtifact !== undefined) {
|
|
3296
|
+
const seed = parseArtifactRef(b.seedFromArtifact);
|
|
3297
|
+
if (seed === null)
|
|
3298
|
+
return reply.code(400).send({ code: 'seed_invalid', error: 'seedFromArtifact must be <mount>/<doc>' });
|
|
3299
|
+
if (actorOf(req).kind !== 'human') {
|
|
3300
|
+
return reply.code(403).send({ code: 'seed_refused', error: 'only a person can open a chat about a document' });
|
|
3301
|
+
}
|
|
3302
|
+
// Scope before any read: a project chat may be seeded only from a document in that project.
|
|
3303
|
+
const chatMountOf = chatMount(b.projectId);
|
|
3304
|
+
if (chatMountOf !== null && seed.mount !== chatMountOf) {
|
|
3305
|
+
return reply.code(400).send({
|
|
3306
|
+
code: 'seed_out_of_scope',
|
|
3307
|
+
error: `${seed.ref} is ${seed.mount === UNFILED_MOUNT ? 'Unfiled' : `in project ${seed.mount}`}, not in project ${b.projectId}; open the chat in its project, or in no project`,
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
let root;
|
|
3311
|
+
try {
|
|
3312
|
+
root = await projectDocsRoot(adapter, projectSettings, seed.projectId);
|
|
3313
|
+
}
|
|
3314
|
+
catch (err) {
|
|
3315
|
+
return reply.code(500).send({ error: `cannot read ${seed.ref}: ${message(err)}` });
|
|
3316
|
+
}
|
|
3317
|
+
const status = root === null ? 'missing' : artifactStatus(root, seed.doc);
|
|
3318
|
+
if (status === 'unsupported') {
|
|
3319
|
+
return reply.code(501).send({ code: 'seed_unsupported', error: 'this platform cannot read a document without following links in its folders' });
|
|
3320
|
+
}
|
|
3321
|
+
if (root === null || status === 'missing') {
|
|
3322
|
+
return reply.code(404).send({ code: 'seed_not_found', error: `no document ${seed.ref} on this daemon` });
|
|
3323
|
+
}
|
|
3324
|
+
// The repositories the document was grounded on, as its draft launch resolved them (inferred from the brief or
|
|
3325
|
+
// the project's one repository too); the create-time request's list only for a binding that predates that.
|
|
3326
|
+
// Read beneath the docs root like every A1 read (crew#1274).
|
|
3327
|
+
const grounding = readGroundingBeneath(root, seed.doc);
|
|
3328
|
+
if (grounding !== null && 'unreadable' in grounding) {
|
|
3329
|
+
return reply.code(409).send({ code: 'seed_binding_unreadable', error: `what ${seed.ref} was made about cannot be read; open the chat without it` });
|
|
3330
|
+
}
|
|
3331
|
+
const bound = grounding?.repoRefs ?? [];
|
|
3332
|
+
if (b.projectId !== undefined && bound.length > 0) {
|
|
3333
|
+
const members = new Set((await projectRepoCandidates(adapter, b.projectId)).map((r) => r.repoRef));
|
|
3334
|
+
const outside = bound.filter((r) => !members.has(r));
|
|
3335
|
+
if (outside.length > 0) {
|
|
3336
|
+
return reply.code(400).send({
|
|
3337
|
+
code: 'seed_out_of_scope',
|
|
3338
|
+
error: `${seed.ref} was made about ${outside.join(', ')}, which project ${b.projectId} does not hold; open the chat in no project`,
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
// Seeded: the chat reads the repositories the document was made about, unless the request named its own or a
|
|
3343
|
+
// kind that reads every repository (`everything`) or names its repositories by project (`project`). A chat in
|
|
3344
|
+
// no project that asked for no repository (`system`, `none`) becomes the document's repository scope; a
|
|
3345
|
+
// project chat keeps the kind it asked for.
|
|
3346
|
+
const seedable = b.projectId === undefined
|
|
3347
|
+
? scopeKind === undefined || scopeKind === 'repo' || scopeKind === 'repos' || scopeKind === 'system' || scopeKind === 'none'
|
|
3348
|
+
: scopeKind === undefined || scopeKind === 'repo' || scopeKind === 'repos';
|
|
3349
|
+
if (requestedRefs.length === 0 && bound.length > 0 && seedable) {
|
|
3350
|
+
// Only where the binding becomes the chat's repository list does a chat's limit apply (an `everything` or
|
|
3351
|
+
// `project` chat, or one that names its own repositories, reads its own scope whatever the binding holds).
|
|
3352
|
+
if (bound.length > 32) {
|
|
3353
|
+
return reply.code(400).send({ code: 'seed_too_many_repos', error: `${seed.ref} was made about ${bound.length} repositories; a chat reads at most 32 — open it in its project or across all repositories` });
|
|
3354
|
+
}
|
|
3355
|
+
requestedRefs.push(...bound);
|
|
3356
|
+
if (scopeKind === 'system' || scopeKind === 'none')
|
|
3357
|
+
scopeKind = undefined;
|
|
3358
|
+
}
|
|
3359
|
+
const reshaped = namedKindRefusal(scopeKind, b.projectId, [...new Set(requestedRefs)]);
|
|
3360
|
+
if (reshaped !== null)
|
|
3361
|
+
return reply.code(400).send({ error: reshaped.error });
|
|
3362
|
+
}
|
|
3081
3363
|
// Validate the project BEFORE opening seats: a chat has no launch record for the engine to
|
|
3082
3364
|
// attach against atomically (chats are an in-memory seat pool), so the route validates
|
|
3083
3365
|
// up-front and attaches right after open — the one non-atomic attach, documented in the ADR
|
|
@@ -3114,7 +3396,7 @@ runtime = {}) {
|
|
|
3114
3396
|
return reply.code(resolved.status).send({ error: resolved.error });
|
|
3115
3397
|
workflow = resolved.workflow;
|
|
3116
3398
|
}
|
|
3117
|
-
const statementOpts = { analysis: workflow !== undefined };
|
|
3399
|
+
const statementOpts = { analysis: workflow !== undefined, make: chatProposals.digest() };
|
|
3118
3400
|
// A chat id this daemon already holds a scope for is LIVE: re-preparing its scratch root and
|
|
3119
3401
|
// re-opening it would overwrite the statement its seats are reading and (on a changed scope)
|
|
3120
3402
|
// evict them mid-conversation (Copilot, #518). Close it first, or let the daemon mint the id.
|
|
@@ -3140,7 +3422,7 @@ runtime = {}) {
|
|
|
3140
3422
|
chatId,
|
|
3141
3423
|
...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
|
|
3142
3424
|
repoRefs: requestedRefs,
|
|
3143
|
-
...(
|
|
3425
|
+
...(scopeKind !== undefined ? { kind: scopeKind } : {}),
|
|
3144
3426
|
};
|
|
3145
3427
|
const resolution = await resolveScope(scopeRequest, req);
|
|
3146
3428
|
if (!resolution.ok) {
|
|
@@ -3148,6 +3430,7 @@ runtime = {}) {
|
|
|
3148
3430
|
return reply.code(resolution.status).send({
|
|
3149
3431
|
error: resolution.error,
|
|
3150
3432
|
...(resolution.missing !== undefined ? { missing: resolution.missing } : {}),
|
|
3433
|
+
...(resolution.code !== undefined ? { code: resolution.code, path: resolution.path ?? null } : {}),
|
|
3151
3434
|
});
|
|
3152
3435
|
}
|
|
3153
3436
|
const { scope, engine } = resolution;
|
|
@@ -3162,7 +3445,7 @@ runtime = {}) {
|
|
|
3162
3445
|
if (!chatScopes.owns(chatId, token)) {
|
|
3163
3446
|
return reply.code(409).send({ error: `chat ${chatId} was closed while it was being opened; open it again` });
|
|
3164
3447
|
}
|
|
3165
|
-
prepareChatScratch(chatId, scope, inForceAtOpen, statementOpts);
|
|
3448
|
+
prepareChatScratch(chatId, scope, inForceAtOpen, { ...statementOpts, ...voiceFor(scope.projectId) });
|
|
3166
3449
|
}
|
|
3167
3450
|
catch (err) {
|
|
3168
3451
|
// The helper removes only what it created itself (a pre-existing root is preserved) —
|
|
@@ -3322,6 +3605,8 @@ runtime = {}) {
|
|
|
3322
3605
|
startedOn: b.startedOn,
|
|
3323
3606
|
cli: b.primary ?? clis[0] ?? null,
|
|
3324
3607
|
actorId: security.authMode === 'required' ? actorOf(req).id : null,
|
|
3608
|
+
// A2: an ended chat's proposal frames still name its project.
|
|
3609
|
+
projectId: scope.projectId ?? null,
|
|
3325
3610
|
});
|
|
3326
3611
|
}
|
|
3327
3612
|
catch (err) {
|
|
@@ -3387,7 +3672,78 @@ runtime = {}) {
|
|
|
3387
3672
|
targets: z.array(z.string().min(1)).min(1).max(8).optional(),
|
|
3388
3673
|
/** (api-types 0.137.0) Attachment ids from `POST /chats/:id/attachments`. */
|
|
3389
3674
|
attachments: z.array(z.string().min(1).max(64)).optional(),
|
|
3675
|
+
/** (api-types 0.169.0, DES-CHAT-ARTIFACTS-001 §5.1) What studio shows beside the message. */
|
|
3676
|
+
context: ChatMessageContextSchema.optional(),
|
|
3390
3677
|
}).strict();
|
|
3678
|
+
// DES-CHAT-ARTIFACTS-001 §5.1 (A1): what a turn's block reads — the docs roots, the decorated runs (project,
|
|
3679
|
+
// document and chat ids), unit outputs and the acceptance line. One set of deps, so the block and the run pages
|
|
3680
|
+
// read the same records.
|
|
3681
|
+
const chatContextDeps = {
|
|
3682
|
+
docsRoot: (projectId) => projectDocsRoot(adapter, projectSettings, projectId),
|
|
3683
|
+
runs: async () => (await adapter.sessionsDetail()).map((v) => decorateRun(v)),
|
|
3684
|
+
workOutput: async (unitId) => (typeof adapter.workOutput === 'function' ? adapter.workOutput(unitId) : null),
|
|
3685
|
+
acceptanceLine: async (view) => (await acceptanceViewOf(view, {})).summary.line,
|
|
3686
|
+
docStyle: (root, doc) => {
|
|
3687
|
+
const g = readGroundingBeneath(root, doc);
|
|
3688
|
+
return g !== null && 'style' in g ? g.style : undefined;
|
|
3689
|
+
},
|
|
3690
|
+
};
|
|
3691
|
+
// DES-CHAT-ARTIFACTS-001 §4 (A2): proposals. Resolved from crew's own records (the same reads the turn block uses);
|
|
3692
|
+
// records go to the live transcript, or the kept history copy once the chat has ended.
|
|
3693
|
+
// A dollar figure only from a run whose EVERY dispatched attempt of every agent step reported a price (DES §4.2).
|
|
3694
|
+
const proposalCosts = async (view) => runCostFromEvents(view, (await adapter.runEvents(view.session.id)));
|
|
3695
|
+
const chatProposals = new ChatProposals({
|
|
3696
|
+
flags: () => chatArtifactFlags,
|
|
3697
|
+
catalog: CATALOG_SEEDS,
|
|
3698
|
+
records: (chatId) => chatHistory?.messages(chatId) ?? chatTranscripts?.read(chatId) ?? [],
|
|
3699
|
+
append: (chatId, record) => {
|
|
3700
|
+
if (chatHistory?.stateOf(chatId)?.status === 'closed')
|
|
3701
|
+
return chatHistory.appendKept(chatId, record);
|
|
3702
|
+
return chatTranscripts?.appendProposalRecord(chatId, record) ?? false;
|
|
3703
|
+
},
|
|
3704
|
+
instanceOf: (chatId) => chatHistory?.stateOf(chatId)?.instance ?? null,
|
|
3705
|
+
projectOf: (chatId) => chatScopes.get(chatId)?.projectId ?? chatHistory?.stateOf(chatId)?.projectId ?? null,
|
|
3706
|
+
chat: (chatId) => {
|
|
3707
|
+
const scope = chatScopes.get(chatId);
|
|
3708
|
+
if (scope === undefined)
|
|
3709
|
+
return null;
|
|
3710
|
+
const path = askPaths.get(chatId);
|
|
3711
|
+
return {
|
|
3712
|
+
projectId: scope.projectId ?? null,
|
|
3713
|
+
repos: scope.repos.map((r) => ({ id: r.id, name: r.name })),
|
|
3714
|
+
lead: path?.pa ?? path?.primary ?? null,
|
|
3715
|
+
seats: path?.eligible ?? [],
|
|
3716
|
+
analysis: path?.workflow !== undefined,
|
|
3717
|
+
};
|
|
3718
|
+
},
|
|
3719
|
+
rosterEligible: () => rosterWithStanding()
|
|
3720
|
+
.filter((s) => s.enabled_for_council && s.seat_eligible_for_work !== false && s.auth !== 'signed_out' && s.optIn !== true)
|
|
3721
|
+
.map((s) => ({ key: String(s.key), name: s.display_name || String(s.key) })),
|
|
3722
|
+
runs: async () => (await adapter.sessionsDetail()).map((v) => decorateRun(v)),
|
|
3723
|
+
chatOfRun: (runId) => runTimingIndex.chatIdFor(runId),
|
|
3724
|
+
isAskPath: (runId) => runTimingIndex.isAskPath(runId),
|
|
3725
|
+
docsRoot: (projectId) => projectDocsRoot(adapter, projectSettings, projectId),
|
|
3726
|
+
projectName: async (projectId) => (await adapter.projectGet(projectId).catch(() => null))?.name ?? null,
|
|
3727
|
+
projectRepos: async (projectId) => (await projectRepoCandidates(adapter, projectId, (m) => app.log.warn(m))).map((r) => ({ id: r.repoRef, name: r.name })),
|
|
3728
|
+
attachments: (chatId) => chatAttachments.list(chatId).map((a) => ({ id: a.id, name: a.name, bytes: a.bytes, mediaType: a.mediaType })),
|
|
3729
|
+
repoIds: async () => new Set((await adapter.listRepos()).map((r) => r.id)),
|
|
3730
|
+
workflowSteps: async (name, projectId) => {
|
|
3731
|
+
// The preset a launch in this project resolves first (a project row wins over a global one), then the def.
|
|
3732
|
+
const presets = await adapter.listPresets(projectId ?? undefined).catch(() => []);
|
|
3733
|
+
const preset = presets.find((p) => p.name === name && p.scope !== 'global') ?? presets.find((p) => p.name === name);
|
|
3734
|
+
if (preset !== undefined && preset.steps.length > 0)
|
|
3735
|
+
return preset.steps.length;
|
|
3736
|
+
const phases = adapter.getWorkflow(name)?.phases;
|
|
3737
|
+
return phases !== undefined && phases.length > 0 ? phases.length : null;
|
|
3738
|
+
},
|
|
3739
|
+
runCostUsd: proposalCosts,
|
|
3740
|
+
registry: proposalLaunchers,
|
|
3741
|
+
broadcast: (frame) => runtime.broadcast?.(frame),
|
|
3742
|
+
audit: (event, actor, detail) => {
|
|
3743
|
+
audit.record(event, actor, { detail });
|
|
3744
|
+
},
|
|
3745
|
+
log: (m) => app.log.warn(m),
|
|
3746
|
+
});
|
|
3391
3747
|
// F-RECON-017: a message sent while a targeted seat is still answering the previous one is
|
|
3392
3748
|
// REFUSED, not queued. The engine queues silently (the worker gets the text the instant the
|
|
3393
3749
|
// previous turn's final block lands) and its reply frames carry no message correlation, so the
|
|
@@ -3416,6 +3772,22 @@ runtime = {}) {
|
|
|
3416
3772
|
return reply.code(409).send({ error: `chat '${id}' has no eligible seat — sign a seat in and re-seat it (POST /chats/:id/seats)` });
|
|
3417
3773
|
}
|
|
3418
3774
|
const text = parsed.data.text;
|
|
3775
|
+
// DES-CHAT-ARTIFACTS-001 §5.1 (A1): the per-turn block reads crew's records (documents, runs) for the chat's own
|
|
3776
|
+
// person. A message that carries `context` from anyone else (another actor under `auth: required`, or an agent,
|
|
3777
|
+
// system or plugin actor) is refused before anything is reserved or read; one without `context` from them still
|
|
3778
|
+
// goes through, with no block.
|
|
3779
|
+
const msgContext = parsed.data.context;
|
|
3780
|
+
const sender = actorOf(req);
|
|
3781
|
+
const ownsChat = chatHistory === null || chatHistory.visibleTo(id, security.authMode === 'required' ? sender.id : null);
|
|
3782
|
+
const contextAllowed = sender.kind === 'human' && ownsChat;
|
|
3783
|
+
if (msgContext !== undefined && !contextAllowed) {
|
|
3784
|
+
return reply.code(403).send({
|
|
3785
|
+
code: 'chat_context_refused',
|
|
3786
|
+
error: sender.kind !== 'human'
|
|
3787
|
+
? `only a person can send what they are looking at with a chat message (you are a ${sender.kind} actor). Nothing was sent.`
|
|
3788
|
+
: `chat '${id}' was opened by someone else; only its owner can send what they are looking at into it. Nothing was sent.`,
|
|
3789
|
+
});
|
|
3790
|
+
}
|
|
3419
3791
|
// DES-SYSTEM-CHAT: the attachments this message carries — every id this chat's, within the caps,
|
|
3420
3792
|
// refused before anything is reserved or sent.
|
|
3421
3793
|
let attached = [];
|
|
@@ -3463,6 +3835,8 @@ runtime = {}) {
|
|
|
3463
3835
|
// crew#826: the turn lives as long as its answer step may (`budget_secs`), not 3× the pool knob.
|
|
3464
3836
|
let turn = null;
|
|
3465
3837
|
let turnId = randomUUID();
|
|
3838
|
+
// A1: what this turn's seats were given beside the words (recorded with the turn).
|
|
3839
|
+
let turnBinding = null;
|
|
3466
3840
|
// What an ACCEPTED message records — the engine has it, so a persistence failure here is
|
|
3467
3841
|
// logged, never a retraction of a turn that is already being answered (codex on #808 r2, 9).
|
|
3468
3842
|
const persistAccepted = (preface, after) => {
|
|
@@ -3470,7 +3844,9 @@ runtime = {}) {
|
|
|
3470
3844
|
after();
|
|
3471
3845
|
if (preface !== null)
|
|
3472
3846
|
chatTranscripts?.appendSystem(id, turnId, preface);
|
|
3473
|
-
chatTranscripts?.appendUser(id, turnId, text, voice, attached.map(attachmentWire));
|
|
3847
|
+
chatTranscripts?.appendUser(id, turnId, text, voice, attached.map(attachmentWire), contextAllowed);
|
|
3848
|
+
if (turnBinding !== null)
|
|
3849
|
+
chatTranscripts?.appendContext(id, turnId, turnBinding);
|
|
3474
3850
|
// The recorder's audience is the turn's AS THE TURN INDEX HOLDS IT NOW: the eligible roster
|
|
3475
3851
|
// for a random pick, or the PA the relay narrowed it to during the engine awaits above
|
|
3476
3852
|
// (`path.started` / `path.repicked` may land before this record exists — ASK-C2, codex on
|
|
@@ -3567,6 +3943,31 @@ runtime = {}) {
|
|
|
3567
3943
|
});
|
|
3568
3944
|
}
|
|
3569
3945
|
const pending = await considerations.pendingPreface(id, { inForce: rules });
|
|
3946
|
+
// DES-CHAT-ARTIFACTS-001 §5.1 (A1): the turn's block — scope checked before anything is read or copied; the
|
|
3947
|
+
// copies and briefs land in the scratch root's `open/` and `context/`, read roots of the chat's run.
|
|
3948
|
+
// Built in memory here (it awaits); its files are written only after the last check below, with no await between.
|
|
3949
|
+
let contextBlock = '';
|
|
3950
|
+
let publishContext = () => undefined;
|
|
3951
|
+
if (scope !== undefined && contextAllowed) {
|
|
3952
|
+
try {
|
|
3953
|
+
const built = await buildTurnContext({
|
|
3954
|
+
chatId: id,
|
|
3955
|
+
scratchRoot: scope.cwd,
|
|
3956
|
+
projectId: scope.projectId ?? null,
|
|
3957
|
+
text,
|
|
3958
|
+
context: msgContext,
|
|
3959
|
+
records: chatTranscripts?.read(id) ?? [],
|
|
3960
|
+
attachmentIds: chatAttachments.list(id).map((a) => a.id),
|
|
3961
|
+
}, chatContextDeps);
|
|
3962
|
+
contextBlock = built.block;
|
|
3963
|
+
turnBinding = built.binding;
|
|
3964
|
+
publishContext = built.publish;
|
|
3965
|
+
}
|
|
3966
|
+
catch (err) {
|
|
3967
|
+
askPaths.release(id, lease);
|
|
3968
|
+
return reply.code(500).send({ error: `could not prepare what this message is about: ${message(err)}. Nothing was sent.` });
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3570
3971
|
// The last await before delivery may also have closed/reopened the chat. From this check
|
|
3571
3972
|
// through publication and the send there is no await, so no executing turn's files change.
|
|
3572
3973
|
if (askPaths.get(id)?.generation !== lease || chatScopes.get(id) !== storedScope) {
|
|
@@ -3574,7 +3975,7 @@ runtime = {}) {
|
|
|
3574
3975
|
}
|
|
3575
3976
|
if (storedScope !== undefined && scope !== undefined && engine !== undefined &&
|
|
3576
3977
|
(JSON.stringify(scope) !== JSON.stringify(storedScope) || JSON.stringify(engine) !== JSON.stringify(chatScopes.engineOf(id)))) {
|
|
3577
|
-
prepareChatScratch(id, scope, rules, { analysis: path.workflow !== undefined });
|
|
3978
|
+
prepareChatScratch(id, scope, rules, { analysis: path.workflow !== undefined, make: chatProposals.digest(), ...voiceFor(scope.projectId) });
|
|
3578
3979
|
if (chatScopes.refresh(id, storedScope, scope, engine)) {
|
|
3579
3980
|
chatTranscripts?.registerRoots(id, scope.repos.map((r) => ({ absRoot: resolve(r.rootPath), name: r.name })));
|
|
3580
3981
|
}
|
|
@@ -3595,6 +3996,22 @@ runtime = {}) {
|
|
|
3595
3996
|
return reply.code(500).send({ error: `could not stage the attachments for the seat: ${message(err)}` });
|
|
3596
3997
|
}
|
|
3597
3998
|
}
|
|
3999
|
+
// A1: the copies and briefs land now — after the last await and the generation check, before the send — and the
|
|
4000
|
+
// block rides after the person's words (and their files), labelled as data.
|
|
4001
|
+
if (contextBlock !== '') {
|
|
4002
|
+
try {
|
|
4003
|
+
publishContext();
|
|
4004
|
+
}
|
|
4005
|
+
catch (err) {
|
|
4006
|
+
askPaths.release(id, lease);
|
|
4007
|
+
return reply.code(500).send({ error: `could not hand the seat what this message is about: ${message(err)}. Nothing was sent.` });
|
|
4008
|
+
}
|
|
4009
|
+
seatText = `${seatText}\n\n${contextBlock}`;
|
|
4010
|
+
}
|
|
4011
|
+
// A2: the per-message digest of what the chat can make now (advertised operations only; never in an analysis).
|
|
4012
|
+
const makeDigest = path.workflow === undefined ? chatMakeDigest(chatProposals.digest()) : '';
|
|
4013
|
+
if (makeDigest !== '')
|
|
4014
|
+
seatText = `${seatText}\n\n${makeDigest}`;
|
|
3598
4015
|
voice = path.pa !== null ? [path.pa] : [];
|
|
3599
4016
|
// A random pick reserves every eligible seat until the relay names the PA.
|
|
3600
4017
|
audience = voice.length > 0 ? voice : path.eligible;
|
|
@@ -3604,7 +4021,8 @@ runtime = {}) {
|
|
|
3604
4021
|
// chat (crew#1157) never delivers: it is bound to its repository read-only, with no deliver.
|
|
3605
4022
|
const repoRef = scope?.kind === 'repos' && scope.repos.length === 1 ? scope.repos[0].id : undefined;
|
|
3606
4023
|
const delivers = repoRef !== undefined && path.workflow === undefined;
|
|
3607
|
-
|
|
4024
|
+
// A2: the per-message digest of what the chat can make now (advertised operations only).
|
|
4025
|
+
const statementOpts = { analysis: path.workflow !== undefined, make: chatProposals.digest(), ...voiceFor(scope?.projectId) };
|
|
3608
4026
|
const projectId = scope?.projectId;
|
|
3609
4027
|
if (path.runId === undefined) {
|
|
3610
4028
|
const stepId = 'answer-1';
|
|
@@ -3631,8 +4049,20 @@ runtime = {}) {
|
|
|
3631
4049
|
// statement tells the seats to read them at their paths, so the boundary must let them.
|
|
3632
4050
|
// DES-SYSTEM-CHAT: plus the chat's attachments directory, from launch (a run cannot gain a
|
|
3633
4051
|
// read root later), so a file attached on any later turn is readable too.
|
|
3634
|
-
|
|
3635
|
-
|
|
4052
|
+
// DES-CHAT-ARTIFACTS-001 §5.1 (A1): and the directories the turn blocks copy artifacts (`open/`) and write run
|
|
4053
|
+
// briefs (`context/`) into, from launch, so a later turn's copies and briefs are readable too.
|
|
4054
|
+
...(readRoots.length > 0 || attachRoot !== null || scope !== undefined
|
|
4055
|
+
? {
|
|
4056
|
+
extraReadRoots: [
|
|
4057
|
+
...readRoots,
|
|
4058
|
+
...(attachRoot !== null ? [ensureDir(attachRoot)] : []),
|
|
4059
|
+
// At their real paths: a seat in a container sees a read root only there (see `seatPathOf`). Named from the
|
|
4060
|
+
// trusted scratch root's real path, never a child's (a child is the seat's to change; and no seat of this
|
|
4061
|
+
// chat exists yet: this is its first launch). Left out where the platform cannot write them safely, and
|
|
4062
|
+
// when they cannot be made: a plain chat still launches.
|
|
4063
|
+
...(scope !== undefined ? turnReadRoots(scope.cwd) : []),
|
|
4064
|
+
],
|
|
4065
|
+
}
|
|
3636
4066
|
: {}),
|
|
3637
4067
|
};
|
|
3638
4068
|
const runId = await adapter.launchRun(input);
|
|
@@ -3715,6 +4145,56 @@ runtime = {}) {
|
|
|
3715
4145
|
return reply.code(400).send({ error: message(err) });
|
|
3716
4146
|
}
|
|
3717
4147
|
});
|
|
4148
|
+
// DES-CHAT-ARTIFACTS-001 §4.3 (A2): the ONLY way a proposal starts work. Capability, person and owner are checked
|
|
4149
|
+
// first on every call (a worker or system actor is refused even on an `accepting` proposal); the service then takes
|
|
4150
|
+
// the proposal's lock and looks for what already happened before any state check.
|
|
4151
|
+
const ProposalPatchValue = z.union([z.string().max(200), z.number(), z.boolean()]);
|
|
4152
|
+
const ProposalDecisionSchema = z.object({
|
|
4153
|
+
decision: z.enum(['go', 'not_now']),
|
|
4154
|
+
edits: z.object({
|
|
4155
|
+
title: z.string().max(400).optional(),
|
|
4156
|
+
brief: z.string().max(16000).optional(),
|
|
4157
|
+
sources: z.array(z.string().min(1).max(300)).max(32).optional(),
|
|
4158
|
+
repos: z.array(z.string().min(1).max(200)).max(16).optional(),
|
|
4159
|
+
project: z.string().max(200).nullable().optional(),
|
|
4160
|
+
ask: z.string().max(8000).optional(),
|
|
4161
|
+
allow: z.array(z.string().min(1).max(128)).max(64).optional(),
|
|
4162
|
+
name: z.string().max(200).optional(),
|
|
4163
|
+
patch: z.record(z.string().max(40), ProposalPatchValue).optional(),
|
|
4164
|
+
url: z.string().max(2048).optional(),
|
|
4165
|
+
audience: z.string().max(800).optional(),
|
|
4166
|
+
show: z.string().max(4000).optional(),
|
|
4167
|
+
helpers: z.array(z.string().min(1).max(64)).max(8).optional(),
|
|
4168
|
+
}).strict().optional(),
|
|
4169
|
+
}).strict();
|
|
4170
|
+
app.post(`${V}/chats/:id/proposals/:pid`, { config: { manifest: { requestType: 'ChatProposalDecisionBody', responseType: 'ChatProposalDecisionResponse', statusCodes: [200, 400, 403, 404, 409, 502] } } }, async (req, reply) => {
|
|
4171
|
+
const { id, pid } = req.params;
|
|
4172
|
+
if (!chatArtifactFlags.proposals)
|
|
4173
|
+
return reply.code(404).send({ error: 'this daemon does not answer chat proposals' });
|
|
4174
|
+
const actor = actorOf(req);
|
|
4175
|
+
if (!isHumanOperator(actor)) {
|
|
4176
|
+
audit.record('chat.proposal.refused', actor, { detail: { chatId: id, pid, reason: 'not_a_person' } });
|
|
4177
|
+
return reply.code(403).send({
|
|
4178
|
+
code: 'proposal_refused',
|
|
4179
|
+
error: `only a person answers a proposal (you are ${actor.kind}/${actor.trust}). Nothing was started.`,
|
|
4180
|
+
});
|
|
4181
|
+
}
|
|
4182
|
+
const viewer = security.authMode === 'required' ? actor.id : null;
|
|
4183
|
+
if (!CHAT_ID.test(id) || chatHistory === null || !chatHistory.visibleTo(id, viewer)) {
|
|
4184
|
+
return reply.code(404).send({ error: `no chat ${id} on this daemon` });
|
|
4185
|
+
}
|
|
4186
|
+
// The instance these checks passed against: nothing is recorded on a chat that reuses the id meanwhile.
|
|
4187
|
+
const instance = chatHistory.stateOf(id)?.instance ?? null;
|
|
4188
|
+
const parsed = ProposalDecisionSchema.safeParse(req.body);
|
|
4189
|
+
if (!parsed.success)
|
|
4190
|
+
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
4191
|
+
// Optional fields arrive as absent, never as `undefined` (the contract's exact optional shape).
|
|
4192
|
+
const edits = parsed.data.edits === undefined
|
|
4193
|
+
? undefined
|
|
4194
|
+
: Object.fromEntries(Object.entries(parsed.data.edits).filter(([, v]) => v !== undefined));
|
|
4195
|
+
const out = await chatProposals.decide(id, pid, { decision: parsed.data.decision, ...(edits !== undefined ? { edits } : {}) }, actor, instance);
|
|
4196
|
+
return reply.code(out.code).send(out.body);
|
|
4197
|
+
});
|
|
3718
4198
|
const ChatSeatsSchema = z.object({
|
|
3719
4199
|
clis: z.array(z.string().min(1)).min(1).max(8),
|
|
3720
4200
|
}).strict();
|
|
@@ -4617,6 +5097,21 @@ runtime = {}) {
|
|
|
4617
5097
|
// holds nothing after a restart and another gate's words after a late frame.
|
|
4618
5098
|
const steeringPropose = parsed.data.approve && isSteeringAuthorRun(run);
|
|
4619
5099
|
const gatePrompt = steeringPropose ? decided.prompt : undefined;
|
|
5100
|
+
// DES-LEARN §3.1: `keepRules` names drafted rules: on a steering-author propose gate only, and only
|
|
5101
|
+
// ids of THIS draft. A draft that changed since the person read it is a 409, the gate left open.
|
|
5102
|
+
let keptSnapshot;
|
|
5103
|
+
if (parsed.data.keepRules !== undefined) {
|
|
5104
|
+
const draft = steeringPropose && atProposeGate(run) ? await draftedSteeringRules(adapter, run, gatePrompt) : null;
|
|
5105
|
+
if (draft === null || 'unreadable' in draft) {
|
|
5106
|
+
return { code: 409, body: { error: 'keepRules answers a steering-author propose gate with a readable draft; this gate has none', code: 'no_draft' } };
|
|
5107
|
+
}
|
|
5108
|
+
const ids = new Set(draft.rules.map((r) => r.id));
|
|
5109
|
+
const unknown = parsed.data.keepRules.filter((k) => !ids.has(k));
|
|
5110
|
+
if (unknown.length > 0) {
|
|
5111
|
+
return { code: 409, body: { error: `the draft changed since it was read (no drafted rule ${unknown.join(', ')}): read it again`, code: 'draft_changed' } };
|
|
5112
|
+
}
|
|
5113
|
+
keptSnapshot = draft.snapshot;
|
|
5114
|
+
}
|
|
4620
5115
|
try {
|
|
4621
5116
|
// All five positionals, explicit `undefined` for the absent ones (DES-L1 PR-2; the
|
|
4622
5117
|
// gate-arms tripwire pins the arity). An engine refusal (no creator to send back to,
|
|
@@ -4646,6 +5141,7 @@ runtime = {}) {
|
|
|
4646
5141
|
...(parsed.data.amendScope !== undefined ? { amendScope: parsed.data.amendScope } : {}),
|
|
4647
5142
|
...(parsed.data.plan !== undefined ? { planSteps: parsed.data.plan.steps.length } : {}),
|
|
4648
5143
|
...(parsed.data.ord !== undefined ? { ord: parsed.data.ord } : {}),
|
|
5144
|
+
...(parsed.data.input !== undefined ? { input: parsed.data.input } : {}),
|
|
4649
5145
|
...extraDetail,
|
|
4650
5146
|
status,
|
|
4651
5147
|
},
|
|
@@ -4672,7 +5168,10 @@ runtime = {}) {
|
|
|
4672
5168
|
// text (an operator who wants different rules rejects and re-authors). A reject lands
|
|
4673
5169
|
// nothing — the run cancels and the proposal stays an artifact.
|
|
4674
5170
|
const landing = steeringPropose
|
|
4675
|
-
? await landSteeringProposal({ adapter, audit, actor }, run, gatePrompt
|
|
5171
|
+
? await landSteeringProposal({ adapter, audit, actor }, run, gatePrompt, {
|
|
5172
|
+
...(parsed.data.keepRules !== undefined ? { keep: parsed.data.keepRules } : {}),
|
|
5173
|
+
...(keptSnapshot !== undefined ? { snapshot: keptSnapshot } : {}),
|
|
5174
|
+
})
|
|
4676
5175
|
: undefined;
|
|
4677
5176
|
return {
|
|
4678
5177
|
code: 200,
|
|
@@ -4709,15 +5208,25 @@ runtime = {}) {
|
|
|
4709
5208
|
return reply.code(out.code).send(out.body);
|
|
4710
5209
|
});
|
|
4711
5210
|
// Cancel a running or paused run (distinct third action, §11.1).
|
|
5211
|
+
// (api-types 0.161.0, DES-VOICE) An optional body: the person's `reason` (said or typed) and `input: "voice"`, both
|
|
5212
|
+
// kept on the `run.cancelled` audit line. A bodiless cancel is unchanged; voice is a person's only (403).
|
|
4712
5213
|
app.post(`${V}/runs/:id/cancel`, async (req, reply) => {
|
|
4713
5214
|
const { id } = req.params;
|
|
5215
|
+
// A bodiless cancel arrives as undefined, null or '' (an empty text/plain body): all three are no body.
|
|
5216
|
+
const parsed = CancelSchema.safeParse(req.body === undefined || req.body === null || req.body === '' ? {} : req.body);
|
|
5217
|
+
if (!parsed.success)
|
|
5218
|
+
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
5219
|
+
const reason = parsed.data.reason !== undefined && parsed.data.reason !== '' ? parsed.data.reason : null;
|
|
5220
|
+
const spoken = parsed.data.input === 'voice';
|
|
5221
|
+
if (spoken && !isHumanOperator(actorOf(req)))
|
|
5222
|
+
return reply.code(403).send({ error: 'Only a person speaks: input "voice" is a person\'s stop' });
|
|
4714
5223
|
const views = await adapter.sessionsDetail();
|
|
4715
5224
|
const run = views.find((v) => v.session.id === id);
|
|
4716
5225
|
if (!run)
|
|
4717
5226
|
return reply.code(404).send({ error: 'Run not found' });
|
|
4718
5227
|
try {
|
|
4719
5228
|
const status = await adapter.cancelRun(id);
|
|
4720
|
-
audit.record('run.cancelled', actorOf(req), { runId: id, detail: { status } });
|
|
5229
|
+
audit.record('run.cancelled', actorOf(req), { runId: id, detail: { status, ...(reason !== null ? { reason } : {}), ...(spoken ? { input: 'voice' } : {}) } });
|
|
4721
5230
|
return reply.send({ status });
|
|
4722
5231
|
}
|
|
4723
5232
|
catch (err) {
|
|
@@ -4738,6 +5247,15 @@ runtime = {}) {
|
|
|
4738
5247
|
// but its work is complete and liftable — normalize to `completed` so the terminal branch
|
|
4739
5248
|
// below refuses resume with the DELIVER recovery (POST /runs/:id/deliver), not a re-entry.
|
|
4740
5249
|
const conflictStrand = normalizeStranded(run);
|
|
5250
|
+
// DES-CHAT-ARTIFACTS-001 §6.2 (A3): a chat proposal's FAILED attempt is never resumed from its cursor: its seam flight
|
|
5251
|
+
// is gone (a resume would finish without landing anything) and its retry may already run. Try again is the path.
|
|
5252
|
+
// A still-live bound run's own gate is approved as any run's.
|
|
5253
|
+
if (run.session.status === 'failed' && runtime.docBindings?.isBound(id) === true) {
|
|
5254
|
+
return reply.code(409).send({
|
|
5255
|
+
error: `run ${id} answered a chat proposal: it cannot be resumed. Use Try again on its card in the chat (a new attempt at the same document).`,
|
|
5256
|
+
recovery: 'retry',
|
|
5257
|
+
});
|
|
5258
|
+
}
|
|
4741
5259
|
// A TERMINAL run is refused LOUDLY, with the actual recovery named (crew#311 defect 2).
|
|
4742
5260
|
// The engine's `resume_run` no-ops on completed/cancelled and answers the status token, so
|
|
4743
5261
|
// this route used to reply 200 {"status":"cancelled"} — on the exact runs an operator was
|
|
@@ -4818,12 +5336,27 @@ runtime = {}) {
|
|
|
4818
5336
|
if (!parsed.success) {
|
|
4819
5337
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
4820
5338
|
}
|
|
5339
|
+
const spoken = parsed.data.input === 'voice';
|
|
5340
|
+
if (spoken && !isHumanOperator(actorOf(req)))
|
|
5341
|
+
return reply.code(403).send({ error: 'Only a person speaks: input "voice" is a person\'s note' });
|
|
4821
5342
|
const ids = await adapter.sessions();
|
|
4822
5343
|
if (!ids.includes(id))
|
|
4823
5344
|
return reply.code(404).send({ error: 'Run not found' });
|
|
4824
5345
|
try {
|
|
4825
|
-
|
|
4826
|
-
|
|
5346
|
+
// Remembered BEFORE the engine call (its queued frame can reach /ws before the call returns), and forgotten if
|
|
5347
|
+
// the engine refuses it: a send that never happened labels nothing and evicts nothing.
|
|
5348
|
+
const token = spoken ? voiceInputs.remember(id, parsed.data.message, parsed.data.target) : null;
|
|
5349
|
+
try {
|
|
5350
|
+
await adapter.injectWorkerMessage(id, parsed.data.message, parsed.data.target);
|
|
5351
|
+
}
|
|
5352
|
+
catch (err) {
|
|
5353
|
+
if (token !== null)
|
|
5354
|
+
voiceInputs.forget(id, token);
|
|
5355
|
+
throw err;
|
|
5356
|
+
}
|
|
5357
|
+
if (token !== null)
|
|
5358
|
+
voiceInputs.settle(id);
|
|
5359
|
+
audit.record('run.injected', actorOf(req), { runId: id, detail: { target: parsed.data.target, ...(spoken ? { input: 'voice' } : {}) } });
|
|
4827
5360
|
// DC-S4a host: the operator's mid-run steering text (review S9).
|
|
4828
5361
|
if (decisions !== null) {
|
|
4829
5362
|
decisions.track(ingestDecision(decisions, {
|
|
@@ -5399,8 +5932,12 @@ runtime = {}) {
|
|
|
5399
5932
|
/** Guards against stale-tab submissions: must match the current elicitation's id. */
|
|
5400
5933
|
elicitationId: z.string().min(1),
|
|
5401
5934
|
action: z.enum(['accept', 'decline', 'cancel']),
|
|
5402
|
-
/** Required when action is "accept"
|
|
5403
|
-
|
|
5935
|
+
/** Required when action is "accept"; must be absent for decline/cancel. `response` answers a free-text or
|
|
5936
|
+
* single-choice question; `values` (api-types 0.158.0) a multi-select one. */
|
|
5937
|
+
content: z.union([
|
|
5938
|
+
z.object({ response: z.string().min(1) }).strict(),
|
|
5939
|
+
z.object({ values: z.array(z.string().min(1)).min(1).max(100) }).strict(),
|
|
5940
|
+
]).optional(),
|
|
5404
5941
|
})
|
|
5405
5942
|
.strict()
|
|
5406
5943
|
.superRefine((val, ctx) => {
|
|
@@ -5420,9 +5957,9 @@ runtime = {}) {
|
|
|
5420
5957
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
5421
5958
|
}
|
|
5422
5959
|
const body = parsed.data;
|
|
5423
|
-
// 1b. Accept requires content
|
|
5424
|
-
// the 400 is deterministic regardless of run-existence / cache state (P2).
|
|
5425
|
-
if (body.action === 'accept' &&
|
|
5960
|
+
// 1b. Accept requires content (a response, or a multi-select's values) — validated here, before any state
|
|
5961
|
+
// query, so the 400 is deterministic regardless of run-existence / cache state (P2).
|
|
5962
|
+
if (body.action === 'accept' && body.content === undefined) {
|
|
5426
5963
|
return reply.code(400).send({ error: 'action:accept requires content.response' });
|
|
5427
5964
|
}
|
|
5428
5965
|
// 2. Existence check (IDs only — cheaper than sessionsDetail).
|
|
@@ -5441,20 +5978,40 @@ runtime = {}) {
|
|
|
5441
5978
|
.send({ error: 'Elicitation superseded; fetch the current prompt and resubmit' });
|
|
5442
5979
|
}
|
|
5443
5980
|
// 5. Accept-specific validation.
|
|
5981
|
+
let response = null;
|
|
5982
|
+
let picked = null;
|
|
5444
5983
|
if (body.action === 'accept') {
|
|
5445
|
-
|
|
5446
|
-
if (typeof body.content?.response !== 'string') {
|
|
5984
|
+
const refuse = (error) => {
|
|
5447
5985
|
elicitationCache.restoreIfUnchanged(id, taken.entry, taken.gen);
|
|
5448
|
-
return reply.code(400).send({ error
|
|
5986
|
+
return reply.code(400).send({ error });
|
|
5987
|
+
};
|
|
5988
|
+
const multi = taken.entry.multi;
|
|
5989
|
+
if (multi !== undefined) {
|
|
5990
|
+
// 5a. A multi-select is answered with `values`: distinct options, min..=max of them, sent in option order.
|
|
5991
|
+
if (body.content === undefined || !('values' in body.content))
|
|
5992
|
+
return refuse('this question takes content.values (pick several)');
|
|
5993
|
+
const values = body.content.values;
|
|
5994
|
+
const options = taken.entry.options ?? [];
|
|
5995
|
+
if (values.some((v) => !options.includes(v)))
|
|
5996
|
+
return refuse('values must be among the allowed options');
|
|
5997
|
+
if (new Set(values).size !== values.length)
|
|
5998
|
+
return refuse('values must name each option once');
|
|
5999
|
+
if (values.length < multi.min || values.length > multi.max)
|
|
6000
|
+
return refuse(`pick ${multi.min} to ${multi.max} of the options`);
|
|
6001
|
+
picked = options.filter((o) => values.includes(o));
|
|
6002
|
+
response = JSON.stringify(picked);
|
|
5449
6003
|
}
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
!
|
|
5453
|
-
|
|
5454
|
-
|
|
6004
|
+
else {
|
|
6005
|
+
// 5b. content.response must be present; `values` answers only a multi-select.
|
|
6006
|
+
if (body.content === undefined || !('response' in body.content))
|
|
6007
|
+
return refuse('action:accept requires content.response');
|
|
6008
|
+
// 5c. Enum check: if the schema constrained the response, honour it.
|
|
6009
|
+
if (taken.entry.options !== null && !taken.entry.options.includes(body.content.response)) {
|
|
6010
|
+
return refuse('response must be one of the allowed options');
|
|
6011
|
+
}
|
|
6012
|
+
response = body.content.response;
|
|
5455
6013
|
}
|
|
5456
6014
|
}
|
|
5457
|
-
const response = body.action === 'accept' ? body.content.response : null;
|
|
5458
6015
|
// 6. Forward to the actor.
|
|
5459
6016
|
try {
|
|
5460
6017
|
await adapter.resolveElicitation(id, taken.entry.elicitationId, body.action, response);
|
|
@@ -5471,12 +6028,12 @@ runtime = {}) {
|
|
|
5471
6028
|
});
|
|
5472
6029
|
// DC-S4a host: a free-text answer is words; a picked option (or a decline) is a `choice`.
|
|
5473
6030
|
if (decisions !== null) {
|
|
5474
|
-
const
|
|
6031
|
+
const hasOptions = taken.entry.options !== null;
|
|
5475
6032
|
decisions.track(ingestDecision(decisions, {
|
|
5476
6033
|
host: 'elicitation',
|
|
5477
6034
|
actor: actorOf(req),
|
|
5478
|
-
words: response !== null && !
|
|
5479
|
-
...(response !== null &&
|
|
6035
|
+
words: response !== null && !hasOptions ? response : '',
|
|
6036
|
+
...(picked !== null ? { choice: picked.join(', ') } : response !== null && hasOptions ? { choice: response } : body.action !== 'accept' ? { choice: body.action } : {}),
|
|
5480
6037
|
projectId: projects.index.projectOf(id) ?? null,
|
|
5481
6038
|
runId: id,
|
|
5482
6039
|
elicitationId: taken.entry.elicitationId,
|
|
@@ -5536,7 +6093,9 @@ runtime = {}) {
|
|
|
5536
6093
|
.map((t) => t.trim())
|
|
5537
6094
|
.filter(Boolean);
|
|
5538
6095
|
const wanted = names.length > 0 ? new Set(names) : null;
|
|
5539
|
-
|
|
6096
|
+
// DES-VOICE: a note said by voice reads "You (by voice)" on a reloaded thread too (this daemon's memory).
|
|
6097
|
+
const stamped = voiceInputs.stampHistory(id, events);
|
|
6098
|
+
const filtered = wanted ? stamped.filter((e) => wanted.has(e.type)) : stamped;
|
|
5540
6099
|
return { runId: id, total: events.length, returned: filtered.length, events: filtered };
|
|
5541
6100
|
});
|
|
5542
6101
|
// ── Governance reads (crew#40) ──────────────────────────────────────────────
|
|
@@ -5929,6 +6488,46 @@ runtime = {}) {
|
|
|
5929
6488
|
const { workflows, unavailable } = await adapter.workflowCatalog();
|
|
5930
6489
|
return { workflows, unavailable };
|
|
5931
6490
|
});
|
|
6491
|
+
// wf-catalog (api-types 0.168.0): the workflows as a categorised catalog with the requesting
|
|
6492
|
+
// operator's usage. Metadata is the ONE data module (`workflow-catalog/catalog-data.ts`); what a
|
|
6493
|
+
// workflow is and does stays on its def; usage is derived from the runs and the launch trail.
|
|
6494
|
+
app.get(`${V}/workflows/catalog`, { config: { manifest: { responseType: 'WorkflowCatalogResponse', statusCodes: [200, 400] } } }, async (req, reply) => {
|
|
6495
|
+
const raw = req.query?.projectId;
|
|
6496
|
+
if (raw !== undefined && (typeof raw !== 'string' || raw.length > 200)) {
|
|
6497
|
+
return reply.code(400).send({ error: '`projectId` must be a string' });
|
|
6498
|
+
}
|
|
6499
|
+
const projectId = typeof raw === 'string' && raw !== '' ? raw : null;
|
|
6500
|
+
const { workflows } = await adapter.workflowCatalog();
|
|
6501
|
+
let presets = [];
|
|
6502
|
+
try {
|
|
6503
|
+
presets = await adapter.listPresets(projectId ?? undefined);
|
|
6504
|
+
}
|
|
6505
|
+
catch (err) {
|
|
6506
|
+
// An unknown project: the global presets (built-in attribution and saved ones still hold, agy r1).
|
|
6507
|
+
// An engine without presets lists the served defs only. Either way, said once in the log.
|
|
6508
|
+
app.log.warn(`workflows/catalog: the presets could not be read${projectId !== null ? ` for project ${projectId}` : ''} (${message(err)}); listing ${projectId !== null ? 'the global ones' : 'the served workflows only'}`);
|
|
6509
|
+
if (projectId !== null)
|
|
6510
|
+
presets = await adapter.listPresets().catch(() => []);
|
|
6511
|
+
}
|
|
6512
|
+
const views = await adapter.sessionsDetail();
|
|
6513
|
+
// codex r1: date terminal runs that have no `run.ended` entry from their event log, as GET /runs
|
|
6514
|
+
// does, so the median does not depend on which read came first. Only THIS principal's completed
|
|
6515
|
+
// runs can reach a median, so only they are read (Copilot).
|
|
6516
|
+
const me = actorOf(req).id;
|
|
6517
|
+
await backfillEndedAt(views.filter((v) => v.session.status === 'completed' && runTimingIndex.principalFor(v.session.id) === me));
|
|
6518
|
+
const runs = views.map((view) => ({
|
|
6519
|
+
id: view.session.id,
|
|
6520
|
+
workflow: runIdentityOf(view).name,
|
|
6521
|
+
projectId: projects.index.projectOf(view.session.id) ?? null,
|
|
6522
|
+
status: view.session.status,
|
|
6523
|
+
createdAt: runTimingIndex.createdAtFor(view.session.id),
|
|
6524
|
+
endedAt: runTimingIndex.endedAtFor(view.session.id),
|
|
6525
|
+
retryOf: retryIndex.retryOfFor(view.session.id),
|
|
6526
|
+
principal: runTimingIndex.principalFor(view.session.id),
|
|
6527
|
+
}));
|
|
6528
|
+
const body = buildWorkflowCatalog({ defs: workflows, presets, runs, actorId: me, projectId });
|
|
6529
|
+
return body;
|
|
6530
|
+
});
|
|
5932
6531
|
app.get(`${V}/workflows/:id`, async (req, reply) => {
|
|
5933
6532
|
const { id } = req.params;
|
|
5934
6533
|
const workflow = adapter.getWorkflow(id);
|
|
@@ -5970,31 +6569,27 @@ runtime = {}) {
|
|
|
5970
6569
|
return reply.code(500).send({ error: message(err) });
|
|
5971
6570
|
}
|
|
5972
6571
|
});
|
|
5973
|
-
// ── Domain-model browser (crew#44)
|
|
5974
|
-
//
|
|
5975
|
-
//
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
catch (err) {
|
|
5983
|
-
if (err.code === 'ENOENT')
|
|
5984
|
-
return { graph: null };
|
|
5985
|
-
return reply.code(500).send({ error: message(err) });
|
|
5986
|
-
}
|
|
5987
|
-
});
|
|
6572
|
+
// ── Domain-model browser (crew#44) — RETIRED ──────────────────────────────
|
|
6573
|
+
// This read `<daemon cwd>/.wicked-estate/requirements/requirements_graph.json`. A requirements
|
|
6574
|
+
// graph lives in its repository's estate store now (operator ruling 2026-10-31) and has a
|
|
6575
|
+
// repository: `GET /repos/:id/domain-graph` and `GET /repos/:id/requirements` read it. Answering
|
|
6576
|
+
// from a file here would serve a stale graph forever, so the route says where to go instead.
|
|
6577
|
+
app.get(`${V}/domain-graph`, async (_req, reply) => reply.code(410).send({
|
|
6578
|
+
error: 'The daemon-wide domain graph is retired: a requirements graph belongs to a repository and lives in its estate store. Read GET /api/v1/repos/:id/domain-graph or GET /api/v1/repos/:id/requirements.',
|
|
6579
|
+
code: 'movedToRepository',
|
|
6580
|
+
}));
|
|
5988
6581
|
// ── Per-repo domain graph ─────────────────────────────────────────────────
|
|
5989
|
-
//
|
|
5990
|
-
// the
|
|
6582
|
+
// The graph is read from the repo's estate store through the engine (operator ruling 2026-10-31:
|
|
6583
|
+
// never a JSON file in the repo). Coverage stats come from the live estate store via
|
|
6584
|
+
// `wicked-core coverage --json` (not a cached file).
|
|
5991
6585
|
app.get(`${V}/repos/:id/domain-graph`, async (req, reply) => {
|
|
5992
6586
|
const { id } = req.params;
|
|
5993
6587
|
const repos = await adapter.listRepos();
|
|
5994
6588
|
const repo = repos.find((r) => r.id === id);
|
|
5995
6589
|
if (!repo)
|
|
5996
6590
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
5997
|
-
|
|
6591
|
+
if (refusedMissingFolder(repo, reply))
|
|
6592
|
+
return reply;
|
|
5998
6593
|
const dbPath = codeGraphDbOr503(repo, reply);
|
|
5999
6594
|
if (dbPath === null)
|
|
6000
6595
|
return reply;
|
|
@@ -6023,13 +6618,12 @@ runtime = {}) {
|
|
|
6023
6618
|
}
|
|
6024
6619
|
}
|
|
6025
6620
|
try {
|
|
6026
|
-
const
|
|
6027
|
-
return { graph:
|
|
6621
|
+
const read = await readRequirementsGraph(repo, adapter);
|
|
6622
|
+
return { graph: read.graph, coverage };
|
|
6028
6623
|
}
|
|
6029
6624
|
catch (err) {
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
return reply.code(500).send({ error: message(err) });
|
|
6625
|
+
const status = codeGraphErrorStatus(err);
|
|
6626
|
+
return reply.code(status ?? 500).send({ error: message(err) });
|
|
6033
6627
|
}
|
|
6034
6628
|
});
|
|
6035
6629
|
// ── Repo code graph (via wicked-estate graph-view) ──────────────────────────
|
|
@@ -6056,8 +6650,9 @@ runtime = {}) {
|
|
|
6056
6650
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid query'));
|
|
6057
6651
|
}
|
|
6058
6652
|
let page;
|
|
6653
|
+
let coverage = null;
|
|
6059
6654
|
try {
|
|
6060
|
-
page = await
|
|
6655
|
+
({ page, coverage } = await listRequirementsRead(repo, parsed.data, adapter));
|
|
6061
6656
|
}
|
|
6062
6657
|
catch (err) {
|
|
6063
6658
|
// wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
|
|
@@ -6067,7 +6662,12 @@ runtime = {}) {
|
|
|
6067
6662
|
return reply.code(status).send({ error: message(err) });
|
|
6068
6663
|
}
|
|
6069
6664
|
if (page === null) {
|
|
6070
|
-
|
|
6665
|
+
// Not generated yet is a state, typed by `code`, not a refusal (lane repo-page). It says how
|
|
6666
|
+
// far the repository is ("23% real requirements · 4397 placeholders · target 80%";
|
|
6667
|
+
// wicked-core#1233), from the very engine read the page used, when it has a store to count.
|
|
6668
|
+
return reply
|
|
6669
|
+
.code(404)
|
|
6670
|
+
.send(coverage === null ? REQUIREMENTS_NOT_GENERATED : { ...REQUIREMENTS_NOT_GENERATED, coverage });
|
|
6071
6671
|
}
|
|
6072
6672
|
// Overrides keyed by ids the corpus no longer mints (an estate id-scheme migration re-keys
|
|
6073
6673
|
// method/field SymbolIds) would otherwise vanish without a trace — the count is on the page
|
|
@@ -6092,7 +6692,7 @@ runtime = {}) {
|
|
|
6092
6692
|
}
|
|
6093
6693
|
let detail;
|
|
6094
6694
|
try {
|
|
6095
|
-
detail = await getRequirement(repo, decoded);
|
|
6695
|
+
detail = await getRequirement(repo, decoded, adapter);
|
|
6096
6696
|
}
|
|
6097
6697
|
catch (err) {
|
|
6098
6698
|
// wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
|
|
@@ -6137,7 +6737,7 @@ runtime = {}) {
|
|
|
6137
6737
|
}
|
|
6138
6738
|
let detail;
|
|
6139
6739
|
try {
|
|
6140
|
-
detail = await patchRequirement(repo, decodedKey, parsed.data);
|
|
6740
|
+
detail = await patchRequirement(repo, decodedKey, parsed.data, adapter);
|
|
6141
6741
|
}
|
|
6142
6742
|
catch (err) {
|
|
6143
6743
|
// wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
|
|
@@ -6150,12 +6750,14 @@ runtime = {}) {
|
|
|
6150
6750
|
return reply.code(404).send({ error: 'Requirement not found' });
|
|
6151
6751
|
return { requirement: detail };
|
|
6152
6752
|
});
|
|
6153
|
-
app.get(`${V}/repos/:id/graph`, { config: { manifest: { responseType: 'RepoGraphResponse', statusCodes: [200, 404, 500, 503] } } }, async (req, reply) => {
|
|
6753
|
+
app.get(`${V}/repos/:id/graph`, { config: { manifest: { responseType: 'RepoGraphResponse', statusCodes: [200, 404, 409, 500, 503] } } }, async (req, reply) => {
|
|
6154
6754
|
const { id } = req.params;
|
|
6155
6755
|
const repos = await adapter.listRepos();
|
|
6156
6756
|
const repo = repos.find((r) => r.id === id);
|
|
6157
6757
|
if (!repo)
|
|
6158
6758
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
6759
|
+
if (refusedMissingFolder(repo, reply))
|
|
6760
|
+
return reply;
|
|
6159
6761
|
const dbPath = codeGraphDbOr503(repo, reply);
|
|
6160
6762
|
if (dbPath === null)
|
|
6161
6763
|
return reply;
|
|
@@ -6231,6 +6833,8 @@ runtime = {}) {
|
|
|
6231
6833
|
const repo = repos.find((r) => r.id === id);
|
|
6232
6834
|
if (!repo)
|
|
6233
6835
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
6836
|
+
if (refusedMissingFolder(repo, reply))
|
|
6837
|
+
return reply;
|
|
6234
6838
|
const dbPath = codeGraphDbOr503(repo, reply);
|
|
6235
6839
|
if (dbPath === null)
|
|
6236
6840
|
return reply;
|
|
@@ -6269,14 +6873,24 @@ runtime = {}) {
|
|
|
6269
6873
|
const credentials = await deliverCredentialsProbe(originUrl, process.env, undefined, identity);
|
|
6270
6874
|
return reply.send({ repo: repo.id, ...deliverTargetView(originUrl), credentials });
|
|
6271
6875
|
});
|
|
6876
|
+
// `?limit=` (1..500, default 20): the repo page shows the latest 20; its "Show all" asks for more.
|
|
6877
|
+
const GitHistoryQuerySchema = z.object({
|
|
6878
|
+
limit: z.coerce.number().int().min(1).max(500).default(20),
|
|
6879
|
+
}).strict();
|
|
6272
6880
|
app.get(`${V}/repos/:id/git-history`, async (req, reply) => {
|
|
6273
6881
|
const { id } = req.params;
|
|
6274
6882
|
const repos = await adapter.listRepos();
|
|
6275
6883
|
const repo = repos.find((r) => r.id === id);
|
|
6276
6884
|
if (!repo)
|
|
6277
6885
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
6886
|
+
if (refusedMissingFolder(repo, reply))
|
|
6887
|
+
return reply;
|
|
6888
|
+
const query = GitHistoryQuerySchema.safeParse(req.query);
|
|
6889
|
+
if (!query.success) {
|
|
6890
|
+
return reply.code(400).send(invalidBody(query.error, 'Invalid query'));
|
|
6891
|
+
}
|
|
6278
6892
|
try {
|
|
6279
|
-
const { stdout } = await execCapped('git', ['log', '--pretty=format:%H\x1f%h\x1f%s\x1f%an\x1f%ar', '-n',
|
|
6893
|
+
const { stdout } = await execCapped('git', ['log', '--pretty=format:%H\x1f%h\x1f%s\x1f%an\x1f%ar', '-n', String(query.data.limit)], { timeout: 10_000, cwd: repo.root_path });
|
|
6280
6894
|
const commits = stdout.trim().split('\n').filter(Boolean).map((line) => {
|
|
6281
6895
|
const parts = line.split('\x1f');
|
|
6282
6896
|
return {
|
|
@@ -6307,6 +6921,8 @@ runtime = {}) {
|
|
|
6307
6921
|
const repo = repos.find((r) => r.id === id);
|
|
6308
6922
|
if (!repo)
|
|
6309
6923
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
6924
|
+
if (refusedMissingFolder(repo, reply))
|
|
6925
|
+
return reply;
|
|
6310
6926
|
try {
|
|
6311
6927
|
const { stdout } = await execCapped('git', ['shortlog', '-sne', '-n', '--no-merges', 'HEAD'], { timeout: 10_000, cwd: repo.root_path });
|
|
6312
6928
|
// Output: " 42\tFull Name <email@example.com>"
|
|
@@ -6620,6 +7236,12 @@ runtime = {}) {
|
|
|
6620
7236
|
.code(400)
|
|
6621
7237
|
.send({ error: 'councilSpawnStaggerMs must be an integer between 0 and 30000' });
|
|
6622
7238
|
}
|
|
7239
|
+
// Requirements-graph minimum coverage (operator ruling 2026-10-31): `{ minCoveragePct: 1..100 }`.
|
|
7240
|
+
if (Object.hasOwn(patch, 'requirementsGraph') && !isRequirementsGraphSettings(patch.requirementsGraph)) {
|
|
7241
|
+
return reply
|
|
7242
|
+
.code(400)
|
|
7243
|
+
.send({ error: REQUIREMENTS_GRAPH_SETTINGS_RULE });
|
|
7244
|
+
}
|
|
6623
7245
|
if (Object.hasOwn(patch, 'worker_config_root')) {
|
|
6624
7246
|
const root = patch.worker_config_root;
|
|
6625
7247
|
if (typeof root !== 'string' || (root !== '' && !isAbsolute(root))) {
|
|
@@ -6841,6 +7463,7 @@ runtime = {}) {
|
|
|
6841
7463
|
'baseSkillPolicy',
|
|
6842
7464
|
'councilSize',
|
|
6843
7465
|
'councilSpawnStaggerMs',
|
|
7466
|
+
'requirementsGraph',
|
|
6844
7467
|
];
|
|
6845
7468
|
const safe = {};
|
|
6846
7469
|
for (const key of allowed) {
|
|
@@ -6872,6 +7495,8 @@ runtime = {}) {
|
|
|
6872
7495
|
applyWorkerConfigRoot(settings.worker_config_root);
|
|
6873
7496
|
// …and the council size + stagger, read by the engine per council (wicked-core#888).
|
|
6874
7497
|
applyCouncilSettings(settings);
|
|
7498
|
+
// …and the requirements-graph minimum, read by the engine's coverage check and domain-graph step.
|
|
7499
|
+
applyRequirementsGraphSettings(settings);
|
|
6875
7500
|
// Re-judge and re-export the BASE skill (crew#554) — the engine reads WICKED_BASE_SKILL_REF at
|
|
6876
7501
|
// intake, per launch, so this alone makes a changed name or policy live at the next launch.
|
|
6877
7502
|
// Only when the patch named either key: an unrelated settings write must not re-log the seam.
|
|
@@ -6904,6 +7529,11 @@ runtime = {}) {
|
|
|
6904
7529
|
if (err instanceof EstateMcpError && err.code === -32602) {
|
|
6905
7530
|
return reply.code(400).send({ error: err.message });
|
|
6906
7531
|
}
|
|
7532
|
+
// A spawn that failed with ENOENT (the OS code, not text in the message) means estate is not
|
|
7533
|
+
// installed; every other error — EACCES on a path that names ENOENT included — keeps its own message.
|
|
7534
|
+
if (err instanceof EstateMcpError && err.spawnCode === 'ENOENT') {
|
|
7535
|
+
return reply.code(502).send({ error: `wicked-estate is not installed: ${ESTATE_INSTALL_HINT}` });
|
|
7536
|
+
}
|
|
6907
7537
|
return reply.code(502).send({ error: message(err) });
|
|
6908
7538
|
};
|
|
6909
7539
|
// GET /proposals?kind_type=&state= → proposal.list → { proposals: Proposal[] }.
|
|
@@ -7730,6 +8360,71 @@ runtime = {}) {
|
|
|
7730
8360
|
// F-043: the bridge emits where this daemon's interactive seams read — one bus per daemon.
|
|
7731
8361
|
busDataDir: runtime.interactiveBridgeBusDataDir ?? null,
|
|
7732
8362
|
});
|
|
8363
|
+
// The seat images row (`seat_images`) and its fix: the engine's `seat-image status --json`, read
|
|
8364
|
+
// per request, and one background `seat-image build` at a time.
|
|
8365
|
+
const coreExe = () => process.env['WICKED_CORE_EXE'] || null;
|
|
8366
|
+
const seatImageRebuilder = runtime.seatImageRebuilder ?? new SeatImageRebuilder(coreExe);
|
|
8367
|
+
const seatImageStatus = runtime.seatImageStatus ??
|
|
8368
|
+
(async () => {
|
|
8369
|
+
const exe = coreExe();
|
|
8370
|
+
if (exe === null)
|
|
8371
|
+
throw new Error('WICKED_CORE_EXE is not set');
|
|
8372
|
+
try {
|
|
8373
|
+
const out = await execCapped(exe, ['seat-image', 'status', '--json'], { timeout: PROBE_TIMEOUT_MS });
|
|
8374
|
+
return parseSeatImageStatus(out.stdout);
|
|
8375
|
+
}
|
|
8376
|
+
catch (err) {
|
|
8377
|
+
const e = err;
|
|
8378
|
+
throw new Error(loginDetail(typeof e.stderr === 'string' && e.stderr !== '' ? e.stderr : (e.message ?? 'failed')));
|
|
8379
|
+
}
|
|
8380
|
+
});
|
|
8381
|
+
// The roster's seats (on the container floor every seat runs in an image); `null` when unreadable.
|
|
8382
|
+
const rosterSeatClis = () => {
|
|
8383
|
+
try {
|
|
8384
|
+
return rosterWithStanding().map((r) => String(r.key));
|
|
8385
|
+
}
|
|
8386
|
+
catch {
|
|
8387
|
+
return null;
|
|
8388
|
+
}
|
|
8389
|
+
};
|
|
8390
|
+
// ── POST /system/seat-images/rebuild — the seat images row's fix (api-types 0.162.0) ──
|
|
8391
|
+
// A person's only: it builds images in the container runtime for minutes. One at a time; the
|
|
8392
|
+
// progress reads on the `seat_images` row.
|
|
8393
|
+
app.post(`${V}/system/seat-images/rebuild`, { config: { manifest: { responseType: 'RebuildSeatImagesResponse', statusCodes: [202, 403, 409, 503] } } }, async (req, reply) => {
|
|
8394
|
+
if (!isHumanOperator(actorOf(req)))
|
|
8395
|
+
return reply.code(403).send({ error: 'Only a person can rebuild the seat images' });
|
|
8396
|
+
if (!isContainerSeatFloor()) {
|
|
8397
|
+
const out = { started: false, reason: 'seats run on the host floor: there are no seat images to rebuild', rebuild: null };
|
|
8398
|
+
return reply.code(503).send(out);
|
|
8399
|
+
}
|
|
8400
|
+
// A rebuild already running is 409 before anything else is asked of a busy runtime.
|
|
8401
|
+
const active = seatImageRebuilder.current();
|
|
8402
|
+
if (active?.running === true) {
|
|
8403
|
+
const out = { started: false, reason: 'a rebuild is already running', rebuild: active };
|
|
8404
|
+
return reply.code(409).send(out);
|
|
8405
|
+
}
|
|
8406
|
+
// The roster's seats the engine has a seat image for (it refuses a name it does not know).
|
|
8407
|
+
const roster = rosterSeatClis();
|
|
8408
|
+
if (roster === null) {
|
|
8409
|
+
const out = { started: false, reason: 'the roster could not be read', rebuild: active };
|
|
8410
|
+
return reply.code(503).send(out);
|
|
8411
|
+
}
|
|
8412
|
+
const st = await settleWithin(seatImageStatus, PROBE_TIMEOUT_MS);
|
|
8413
|
+
if (st.kind !== 'ok' || st.value === null) {
|
|
8414
|
+
const why = st.kind === 'timeout' ? 'the engine did not answer its seat image status in time' : st.kind === 'error' ? homeless(st.error) : 'the engine gave no seat image status';
|
|
8415
|
+
const out = { started: false, reason: why, rebuild: seatImageRebuilder.current() };
|
|
8416
|
+
return reply.code(503).send(out);
|
|
8417
|
+
}
|
|
8418
|
+
const known = new Set(st.value.seats.map((x) => x.cli));
|
|
8419
|
+
const r = await seatImageRebuilder.start(roster.filter((c) => known.has(c)));
|
|
8420
|
+
if (r.started) {
|
|
8421
|
+
audit.record('system.seat_images.rebuild', actorOf(req), { detail: { clis: r.state.clis } });
|
|
8422
|
+
const out = { started: true, rebuild: r.state };
|
|
8423
|
+
return reply.code(202).send(out);
|
|
8424
|
+
}
|
|
8425
|
+
const out = { started: false, reason: r.reason, rebuild: r.state };
|
|
8426
|
+
return reply.code(r.state?.running === true ? 409 : 503).send(out);
|
|
8427
|
+
});
|
|
7733
8428
|
// ── GET /system — the Nerve Center snapshot (DES-SYSTEM-VIEW, api-types 0.136.0) ──
|
|
7734
8429
|
// One derived, READ-ONLY answer: the CLIs with their 7-day record, every unfinished run with a
|
|
7735
8430
|
// COMPUTED liveness, the infrastructure as real bounded probes, the governance instruments and
|
|
@@ -7785,6 +8480,8 @@ runtime = {}) {
|
|
|
7785
8480
|
toReviewCache = { at: Date.now(), value }; // a failed read is never kept
|
|
7786
8481
|
return value;
|
|
7787
8482
|
}, PROBE_TIMEOUT_MS);
|
|
8483
|
+
// The registered repositories' folders (repo_folder_missing): a missing one is a fixable item.
|
|
8484
|
+
const reposP = settleWithin(() => adapter.listRepos(), PROBE_TIMEOUT_MS);
|
|
7788
8485
|
const gatesByYouP = settleWithin(async () => {
|
|
7789
8486
|
const since = Date.now() - SYSTEM_STATS_DAYS * 86_400_000;
|
|
7790
8487
|
const entries = await audit.readAll({ action: 'gate.decided', since });
|
|
@@ -7822,9 +8519,10 @@ runtime = {}) {
|
|
|
7822
8519
|
}),
|
|
7823
8520
|
probeVault({ vault: adapter.vaultBin != null ? vaultDiagnostics(adapter.vaultBin) : null, exec: systemExec }),
|
|
7824
8521
|
diskRow(stateHome),
|
|
8522
|
+
probeSeatImages({ containerFloor, clis: rosterSeatClis(), status: seatImageStatus, rebuild: () => seatImageRebuilder.current() }),
|
|
7825
8523
|
]);
|
|
7826
|
-
const [views, settings, openGates, diag, ping, rules, gatesByYou, infraProbed] = await Promise.all([
|
|
7827
|
-
viewsP, settingsP, gatesP, diagP, pingP, rulesP, gatesByYouP, infraP,
|
|
8524
|
+
const [views, settings, openGates, diag, ping, rules, gatesByYou, infraProbed, reposRead] = await Promise.all([
|
|
8525
|
+
viewsP, settingsP, gatesP, diagP, pingP, rulesP, gatesByYouP, infraP, reposP,
|
|
7828
8526
|
]);
|
|
7829
8527
|
// The broker probe refreshed each container seat's sign-in reading: the CLI cards read the
|
|
7830
8528
|
// roster AFTER it, so one snapshot never says "broker: signed in" beside "claude: signed out".
|
|
@@ -7952,6 +8650,8 @@ runtime = {}) {
|
|
|
7952
8650
|
stateHomeRow(d === null ? null : (d.stateHome ?? null)),
|
|
7953
8651
|
storesRow,
|
|
7954
8652
|
seatFloorRow,
|
|
8653
|
+
infraProbed[7],
|
|
8654
|
+
reposRow(reposRead),
|
|
7955
8655
|
infraProbed[6],
|
|
7956
8656
|
loadRow(),
|
|
7957
8657
|
];
|
|
@@ -8013,14 +8713,80 @@ runtime = {}) {
|
|
|
8013
8713
|
toReview: toReview.kind === 'ok' ? toReview.value : null,
|
|
8014
8714
|
});
|
|
8015
8715
|
});
|
|
8716
|
+
// DES-CHAT-ARTIFACTS-001 §5.2 (A3): a create's `chat_id` + `proposal_id` counts only through the caller's accepted record.
|
|
8717
|
+
const proposalGrant = (chatId, pid, actorId) => (CHAT_ID.test(chatId) ? chatProposals.grantFor(chatId, pid, actorId) : null);
|
|
8016
8718
|
registerInteractiveProxy(app, adapter, {
|
|
8017
8719
|
settings: projectSettings,
|
|
8018
8720
|
pool: interactiveBridges,
|
|
8019
8721
|
...(runtime.docGrounding !== undefined ? { grounding: runtime.docGrounding } : {}),
|
|
8020
8722
|
// crew#631: supply the standing roster so doc/demo creates can refuse an unavailable seat.
|
|
8021
8723
|
roster: () => rosterWithStanding(),
|
|
8724
|
+
proposalGrant,
|
|
8725
|
+
actorOf: (req) => actorOf(req).id,
|
|
8022
8726
|
log: (m) => app.log.warn(m),
|
|
8023
8727
|
});
|
|
8728
|
+
// A3: the real `create` launcher (§4.3 steps 2-3): one document and one run per proposal identity.
|
|
8729
|
+
if (realCreate) {
|
|
8730
|
+
const bindings = runtime.docBindings;
|
|
8731
|
+
proposalLaunchers.register('create', new CreateLauncher({
|
|
8732
|
+
bindings,
|
|
8733
|
+
runExists: async (runId) => (await adapter.sessions()).includes(runId),
|
|
8734
|
+
docsRoot: (projectId) => projectDocsRoot(adapter, projectSettings, projectId),
|
|
8735
|
+
createDoc: (projectId, body, actorId) => createDocInProcess(adapter, {
|
|
8736
|
+
settings: projectSettings,
|
|
8737
|
+
pool: interactiveBridges,
|
|
8738
|
+
...(runtime.docGrounding !== undefined ? { grounding: runtime.docGrounding } : {}),
|
|
8739
|
+
roster: () => rosterWithStanding(),
|
|
8740
|
+
proposalGrant,
|
|
8741
|
+
log: (m) => app.log.warn(m),
|
|
8742
|
+
}, projectId, body, actorId),
|
|
8743
|
+
launchBound: runtime.launchBoundDraft,
|
|
8744
|
+
...(runtime.fenceBoundDraft !== undefined ? { fence: runtime.fenceBoundDraft } : {}),
|
|
8745
|
+
records: (chatId) => chatHistory?.messages(chatId) ?? chatTranscripts?.read(chatId) ?? [],
|
|
8746
|
+
instanceOf: (chatId) => chatHistory?.stateOf(chatId)?.instance ?? null,
|
|
8747
|
+
chatIdOf: (chatId, instance) => (chatHistory === null ? chatId : chatHistory.idOfInstance(chatId, instance)),
|
|
8748
|
+
repos: async () => (await adapter.listRepos()).map((r) => ({ id: r.id, name: typeof r.name === 'string' && r.name.trim() !== '' ? r.name.trim() : basename(r.root_path), rootPath: r.root_path })),
|
|
8749
|
+
graphFor: async (projectId, chatId) => {
|
|
8750
|
+
if (projectId === null)
|
|
8751
|
+
return chatScopes.engineOf(chatId)?.codeGraphDb ?? null;
|
|
8752
|
+
const decision = await resolveProjectGraphBinding(adapter, projectId, undefined).catch(() => ({ binding: null }));
|
|
8753
|
+
return decision.binding?.dbPath ?? null;
|
|
8754
|
+
},
|
|
8755
|
+
clisJsonFor: (seats) => {
|
|
8756
|
+
const roster = rosterWithStanding();
|
|
8757
|
+
const picked = seats.map((k) => roster.find((s) => s.key === k));
|
|
8758
|
+
return picked.some((s) => s === undefined) ? null : JSON.stringify(picked);
|
|
8759
|
+
},
|
|
8760
|
+
styleOf: (entry) => {
|
|
8761
|
+
const seed = CATALOG_SEEDS.find((c) => c.id === entry);
|
|
8762
|
+
return seed !== undefined && seed.launch.kind === 'document' ? seed.launch.style : 'web';
|
|
8763
|
+
},
|
|
8764
|
+
packetDeps: {
|
|
8765
|
+
runs: chatContextDeps.runs,
|
|
8766
|
+
brief: (view) => buildRunBrief(view, { workOutput: chatContextDeps.workOutput, ...(chatContextDeps.acceptanceLine !== undefined ? { acceptanceLine: chatContextDeps.acceptanceLine } : {}) }),
|
|
8767
|
+
docsRoot: chatContextDeps.docsRoot,
|
|
8768
|
+
attachment: (chatId, id) => {
|
|
8769
|
+
const hit = chatAttachments.resolve(chatId, [id])[0];
|
|
8770
|
+
return hit === undefined ? null : { name: hit.name, path: hit.path };
|
|
8771
|
+
},
|
|
8772
|
+
// The exact brief a turn was given, from the live chat's scratch root, beneath it (no link followed), only when
|
|
8773
|
+
// its bytes still hash to what the turn recorded (A1's binding). Gone after the chat ends or the daemon restarts.
|
|
8774
|
+
contextBrief: (chatId, relPath, sha) => {
|
|
8775
|
+
const cwd = chatScopes.get(chatId)?.cwd;
|
|
8776
|
+
if (cwd === undefined)
|
|
8777
|
+
return null;
|
|
8778
|
+
try {
|
|
8779
|
+
const data = readBeneath(realpathSync(cwd), relPath, BRIEF_COPY_MAX).data;
|
|
8780
|
+
return sha256Hex(data) === sha ? data : null;
|
|
8781
|
+
}
|
|
8782
|
+
catch {
|
|
8783
|
+
return null;
|
|
8784
|
+
}
|
|
8785
|
+
},
|
|
8786
|
+
},
|
|
8787
|
+
log: (m) => app.log.warn(m),
|
|
8788
|
+
}));
|
|
8789
|
+
}
|
|
8024
8790
|
// ── Governed doc delete (crew#338) — the one door that changes BOTH stores ──
|
|
8025
8791
|
// `DELETE /projects/:id/interactive/docs/:doc` retires the doc on the bridge AND drops crew's
|
|
8026
8792
|
// handoff-ledger rows for it (the draft leg keys by document id, so a stale row claims the
|
|
@@ -8035,6 +8801,13 @@ runtime = {}) {
|
|
|
8035
8801
|
// the operator's real ~/.wicked-crew ledgers. The real sweep always arrives from
|
|
8036
8802
|
// `createServer`.
|
|
8037
8803
|
dropDocLedgerRows: runtime.dropDocLedgerRows ?? (() => ({ ok: true, removed_keys: [] })),
|
|
8804
|
+
// A3: a document a chat proposal is still drafting (or landing) is not retired under it.
|
|
8805
|
+
busyReason: (projectId, doc) => {
|
|
8806
|
+
const pending = runtime.docBindings?.busy(`${projectId === 'default' ? UNFILED_MOUNT : projectId}/${doc}`) ?? null;
|
|
8807
|
+
return pending !== null && (pending.launchingAt !== undefined || pending.launchedAt !== undefined || pending.emittedAt !== undefined)
|
|
8808
|
+
? `a chat proposal is still drafting this document (run ${pending.runId.slice(0, 8)}); wait for it to land, or stop that run first`
|
|
8809
|
+
: null;
|
|
8810
|
+
},
|
|
8038
8811
|
log: (m) => app.log.warn(m),
|
|
8039
8812
|
});
|
|
8040
8813
|
// ── A demo run's video as an editable document (the video editor re-cuts it through E1's per-doc API) ──
|
|
@@ -8113,6 +8886,6 @@ runtime = {}) {
|
|
|
8113
8886
|
const runEvalsPrepare = async (input) => {
|
|
8114
8887
|
await prepareRunEvalsLaunch(input, () => runAndRecord(defaultEvalDeps, { project: null, baseline: false, id: randomUUID(), now: new Date() }), runtime.evalRunsDir);
|
|
8115
8888
|
};
|
|
8116
|
-
return { decideGate, decisions, chatRecorder, considerations, search, decorateRun, runEvalsPrepare };
|
|
8889
|
+
return { decideGate, decisions, chatRecorder, considerations, search, decorateRun, runEvalsPrepare, chatProposals, proposalLaunchers };
|
|
8117
8890
|
}
|
|
8118
8891
|
//# sourceMappingURL=routes.js.map
|