@iowarp/clio-coder 0.3.7 → 0.3.8
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/CHANGELOG.md +31 -0
- package/README.md +7 -2
- package/dist/{acp-SK4MD6MM.js → acp-U67UHUK2.js} +13 -13
- package/dist/{agents-2FN2K6ME.js → agents-YU6SGALZ.js} +35 -33
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-QIYZWM5I.js → auth-5ZPJOIVG.js} +32 -24
- package/dist/builtins-C6JMZVV6.js +17 -0
- package/dist/{chunk-YD734TPH.js → chunk-26LEYJZH.js} +2 -2
- package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
- package/dist/{chunk-WJHBC77E.js → chunk-2HFZQUHL.js} +5 -5
- package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
- package/dist/{chunk-EOOQZZDE.js → chunk-4SPRNWDE.js} +18 -18
- package/dist/{chunk-EBEFWSGL.js → chunk-5DHKRSMQ.js} +7 -7
- package/dist/{chunk-OBMAI2DP.js → chunk-5H3GB5BO.js} +11 -9
- package/dist/{chunk-JTSEDYVQ.js → chunk-5Q2VVUKB.js} +7 -7
- package/dist/{chunk-VQNODYQ4.js → chunk-7RFXX52T.js} +294 -3704
- package/dist/{chunk-XEGB6BCN.js → chunk-7RGZWPB6.js} +5 -5
- package/dist/{chunk-GH5622CP.js → chunk-A2NJGIB3.js} +2 -2
- package/dist/{chunk-CEYBNUGC.js → chunk-A3WNZD3P.js} +323 -27
- package/dist/{chunk-D4MDIG46.js → chunk-B5CSFE7B.js} +7 -7
- package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
- package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
- package/dist/{chunk-KCMKRQX4.js → chunk-E77JEWSD.js} +42 -49
- package/dist/{chunk-J7PIKKWC.js → chunk-EMYUUSFG.js} +7 -7
- package/dist/{chunk-JRIO5UD2.js → chunk-EQ63NRB7.js} +5 -5
- package/dist/{chunk-UVDSQ6LW.js → chunk-FBVTI2TJ.js} +74 -28
- package/dist/{chunk-BMWK7ZIZ.js → chunk-FHJEP5SW.js} +13 -13
- package/dist/{chunk-FOT2FX5J.js → chunk-GN57SG4G.js} +7 -7
- package/dist/{chunk-SROCI7ZU.js → chunk-GPIEI3LY.js} +5 -5
- package/dist/{chunk-UFQ3F4FW.js → chunk-GU2UIAFZ.js} +4 -4
- package/dist/{chunk-UND3GU2L.js → chunk-H7IXIC72.js} +2 -2
- package/dist/{chunk-VREKEFLL.js → chunk-HLE42MG7.js} +3 -3
- package/dist/{chunk-465YSENW.js → chunk-IFBNV6H6.js} +3 -3
- package/dist/{chunk-LADCF22A.js → chunk-IGWKHNIQ.js} +113 -54
- package/dist/chunk-IIZWH4XA.js +172 -0
- package/dist/{chunk-UHXRNZ2J.js → chunk-IJ7RPIYJ.js} +4 -4
- package/dist/{chunk-M4AKACEO.js → chunk-J3YUBZWY.js} +2 -2
- package/dist/{chunk-AB4XIIVB.js → chunk-JOZYP4GM.js} +6 -6
- package/dist/{chunk-ZGH7FGS5.js → chunk-K4XHGFR5.js} +742 -12
- package/dist/{chunk-EELBMBT6.js → chunk-KTYTFRMB.js} +62 -5
- package/dist/chunk-LU7P4LHA.js +33 -0
- package/dist/{chunk-DQA7QLMD.js → chunk-ME6CCNFO.js} +9 -24
- package/dist/{chunk-KZ2H5X4G.js → chunk-MXKJU4JB.js} +88 -14
- package/dist/{chunk-DMD2AGVS.js → chunk-N22QMJKY.js} +20 -18
- package/dist/chunk-NMPKI6XL.js +3006 -0
- package/dist/chunk-NUGM5KR6.js +165 -0
- package/dist/{chunk-5C77SEEY.js → chunk-P43ETTHK.js} +2 -2
- package/dist/{chunk-JNXPYBB4.js → chunk-PMDBGQSJ.js} +2 -2
- package/dist/chunk-PT7HYKEM.js +165 -0
- package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
- package/dist/{chunk-C4JBQ5SR.js → chunk-TANS5ZJS.js} +6 -6
- package/dist/{chunk-YTYFXUI3.js → chunk-TB5666IT.js} +9 -9
- package/dist/{chunk-UUANF5CR.js → chunk-TLQJPP24.js} +375 -687
- package/dist/{chunk-OB5HIGJY.js → chunk-TT36MB5S.js} +2 -1
- package/dist/{chunk-DJNLUABN.js → chunk-TTHACPOM.js} +135 -17
- package/dist/{chunk-6M7VS3J3.js → chunk-TYPGUK6W.js} +4 -4
- package/dist/{chunk-THKY7CD7.js → chunk-U6MBIEMB.js} +108 -24
- package/dist/{chunk-PD3MESLB.js → chunk-VAWNZU7Z.js} +4 -4
- package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
- package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
- package/dist/{chunk-MXI6J5JF.js → chunk-VWZOAB7K.js} +10 -10
- package/dist/{chunk-QCTRSGHQ.js → chunk-WLFILSD5.js} +36 -36
- package/dist/{chunk-5UJ6ECTS.js → chunk-WNIJTQQK.js} +6 -6
- package/dist/{chunk-4DWFMQDR.js → chunk-WSB3FPX7.js} +68 -20
- package/dist/{chunk-3HAPLH5M.js → chunk-WWCZ5F23.js} +112 -10
- package/dist/{chunk-X2KV5FXT.js → chunk-WXY7KU3G.js} +2 -2
- package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
- package/dist/{chunk-DOOEX22V.js → chunk-XWSF374K.js} +4 -4
- package/dist/{chunk-6TUKSZVF.js → chunk-YS5VLNH5.js} +8 -8
- package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
- package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
- package/dist/cli/index.js +31 -31
- package/dist/{clio-WBVQEBKO.js → clio-QVTYJ57A.js} +8 -8
- package/dist/code-nav-FGGFIE7L.js +2 -2
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-F7OEATSO.js → components-ZFA3SAER.js} +8 -8
- package/dist/{config-TRBL3RCF.js → config-LW5IJFQN.js} +58 -53
- package/dist/{configure-OLCVPHNM.js → configure-7XIZCOU4.js} +26 -21
- package/dist/{context-XEWE3MOJ.js → context-L3WL3X7K.js} +47 -41
- package/dist/{context-WFPKQSM6.js → context-N52ZA626.js} +22 -22
- package/dist/{context-MJIJ6GOX.js → context-Y6Y7QPR6.js} +12 -12
- package/dist/{context-clear-KNOS2JPB.js → context-clear-MBQRLSDQ.js} +47 -41
- package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
- package/dist/{context-working-set-EUXAZI6N.js → context-working-set-GS6DSO7F.js} +14 -14
- package/dist/{dispatch-runner-B7MTOVKL.js → dispatch-runner-22ZCNOM3.js} +83 -55
- package/dist/{docs-FLJTIDSE.js → docs-7LQ23DLM.js} +8 -8
- package/dist/{doctor-RN4YKO2X.js → doctor-M7YEDGAE.js} +24 -20
- package/dist/{eval-RUBJVSNQ.js → eval-BEC2WHDA.js} +71 -19
- package/dist/{evidence-JZNBUOQZ.js → evidence-REJUMSKM.js} +62 -56
- package/dist/{evolve-FJVC4KKI.js → evolve-PY5ZBA5K.js} +41 -35
- package/dist/{extensions-IQL36S7K.js → extensions-HVKU65YU.js} +6 -6
- package/dist/{fleet-BDKYJFCP.js → fleet-7WZEWRFA.js} +72 -67
- package/dist/{fleet-commands-ZFIWZSB3.js → fleet-commands-UVHWM76J.js} +10 -10
- package/dist/{fleet-graph-Y6HPXIVF.js → fleet-graph-6ULH7PES.js} +13 -13
- package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
- package/dist/{fleet-validate-BIYREGIK.js → fleet-validate-72PC4SLA.js} +17 -17
- package/dist/{init-LQUB5COQ.js → init-OG3TPGQG.js} +61 -56
- package/dist/{library-NJAHIGG4.js → library-CNTMPLRF.js} +18 -18
- package/dist/{memory-OG6HOYKM.js → memory-6IS7F275.js} +43 -37
- package/dist/{models-5ZG5XY7J.js → models-ENRJDA5W.js} +33 -28
- package/dist/{monitor-TJ7AMTGB.js → monitor-XLDVO7TN.js} +56 -38
- package/dist/{orchestrator-WZYB54DM.js → orchestrator-6KSPYRHA.js} +421 -303
- package/dist/{paths-XUC7GS6E.js → paths-DBXMZMDU.js} +5 -5
- package/dist/registry-LG64LTF4.js +11 -0
- package/dist/{reset-PXQT45IY.js → reset-RZ4ER727.js} +10 -10
- package/dist/{run-FQ74YF62.js → run-Y2CNK5RU.js} +79 -74
- package/dist/{share-FW7SVCL3.js → share-A55GYP6Z.js} +16 -16
- package/dist/{skills-7E7IRB3R.js → skills-ALC5J6AT.js} +19 -19
- package/dist/{skills-eval-LI75W6OK.js → skills-eval-JPBEBYQU.js} +52 -47
- package/dist/support-MIETYA5E.js +38 -0
- package/dist/{targets-4CIFKCTW.js → targets-VGNXIR3S.js} +42 -33
- package/dist/{terminal-lease-WUZY7ZV5.js → terminal-lease-WOBR64YA.js} +3 -3
- package/dist/{uninstall-7FV7IP4E.js → uninstall-ZJF5H5ZN.js} +8 -8
- package/dist/{upgrade-K2HVIVMQ.js → upgrade-FUSUAGHR.js} +27 -25
- package/dist/{usage-GTZELZQX.js → usage-N4MKVHKD.js} +51 -46
- package/dist/{verifiers-RLAHT27O.js → verifiers-YAWOJ3H2.js} +13 -13
- package/dist/{verify-BX3BRKH5.js → verify-LTDHYBGY.js} +9 -9
- package/dist/{wiki-generate-ASIFASCN.js → wiki-generate-6M7GHTBJ.js} +66 -61
- package/dist/worker/entry.js +68 -61
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/artifact-versions.md +6 -5
- package/docs/built-in-agents.md +1 -1
- package/docs/commands-and-modes.md +2 -2
- package/docs/configuration-and-targets.md +8 -3
- package/docs/context-engine.md +1 -1
- package/docs/documentation-guide.md +1 -1
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +1 -1
- package/docs/evidence-and-memory.md +70 -6
- package/docs/evolution.md +1 -1
- package/docs/extensions-and-sharing.md +1 -1
- package/docs/fleet-dispatch.md +34 -9
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +1 -1
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +2 -2
- package/docs/proactive-memory.md +1 -1
- package/docs/prompt-envelope-and-tools.md +4 -2
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +41 -38
- package/docs/safety-model.md +1 -1
- package/docs/scientific-validation.md +1 -1
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +1 -1
- package/docs/trace-store.md +1 -1
- package/docs/tui-design.md +1 -1
- package/docs/worker-dispatch-mechanics.md +1 -1
- package/package.json +1 -2
- package/src/cli/argv.ts +5 -0
- package/src/cli/configure.ts +107 -23
- package/src/cli/doctor.ts +5 -1
- package/src/cli/evidence.ts +30 -25
- package/src/cli/fleet-preflight.ts +2 -12
- package/src/cli/shared.ts +1 -0
- package/src/cli/targets.ts +4 -1
- package/src/cli/validate-model.ts +60 -5
- package/src/core/bus-events.ts +25 -0
- package/src/core/commit-attribution.ts +4 -4
- package/src/core/path-boundary.ts +100 -0
- package/src/domains/agents/extension.ts +2 -11
- package/src/domains/agents/fleet-contract.ts +30 -12
- package/src/domains/agents/recipe.ts +7 -1
- package/src/domains/agents/registry.ts +73 -5
- package/src/domains/agents/result-contract.ts +128 -17
- package/src/domains/agents/write-boundary.ts +15 -50
- package/src/domains/context/project-rules.ts +51 -1
- package/src/domains/dispatch/assignment-reconcile.ts +22 -5
- package/src/domains/dispatch/assignment-store.ts +151 -14
- package/src/domains/dispatch/contract.ts +15 -1
- package/src/domains/dispatch/delegation-plan.ts +2 -5
- package/src/domains/dispatch/execution-role.ts +9 -1
- package/src/domains/dispatch/extension.ts +156 -67
- package/src/domains/dispatch/fleet-run.ts +57 -3
- package/src/domains/dispatch/gate-role-prompts.ts +38 -0
- package/src/domains/dispatch/index.ts +3 -1
- package/src/domains/dispatch/intent-requirements.ts +40 -0
- package/src/domains/dispatch/intent.ts +84 -8
- package/src/domains/dispatch/path-scope.ts +370 -0
- package/src/domains/dispatch/receipt-integrity.ts +2 -1
- package/src/domains/dispatch/types.ts +14 -7
- package/src/domains/dispatch/validation.ts +6 -3
- package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
- package/src/domains/dispatch/write-boundary.ts +201 -22
- package/src/domains/eval/metrics/evidence.ts +79 -2
- package/src/domains/eval/runners/clio-run.ts +12 -2
- package/src/domains/evidence/build.ts +69 -11
- package/src/domains/evidence/index.ts +21 -0
- package/src/domains/evidence/provenance.ts +46 -11
- package/src/domains/evidence/trust-projection.ts +274 -0
- package/src/domains/evidence/trust-status.ts +145 -17
- package/src/domains/evidence/types.ts +4 -0
- package/src/domains/extensions/discovery.ts +88 -1
- package/src/domains/extensions/resources.ts +20 -8
- package/src/domains/extensions/state.ts +6 -2
- package/src/domains/extensions/types.ts +4 -1
- package/src/domains/lifecycle/doctor.ts +140 -1
- package/src/domains/prompts/contract.ts +3 -5
- package/src/domains/providers/extension.ts +30 -2
- package/src/domains/resources/common-loader.ts +3 -0
- package/src/domains/resources/prompts/loader.ts +119 -14
- package/src/domains/safety/policy-engine.ts +5 -5
- package/src/domains/safety/run-effects.ts +64 -1
- package/src/domains/safety/scope.ts +7 -12
- package/src/engine/acp/server.ts +4 -1
- package/src/engine/prompt-templates.ts +18 -1
- package/src/engine/worker-runtime.ts +6 -3
- package/src/interactive/dispatch-board.ts +53 -3
- package/src/interactive/interactive-event-projection.ts +14 -0
- package/src/interactive/overlays/settings.ts +141 -47
- package/src/interactive/slash-commands.ts +7 -2
- package/src/interactive/view/artifacts.ts +42 -9
- package/src/interactive/view/view-overlay.ts +15 -3
- package/src/interactive/worker-receipts.ts +14 -2
- package/src/interactive/worker-stream.ts +8 -0
- package/src/tools/dispatch-admission.ts +12 -13
- package/src/tools/dispatch-arguments.ts +27 -0
- package/src/tools/dispatch-plan.ts +29 -0
- package/src/tools/dispatch-runner.ts +48 -13
- package/src/tools/monitor.ts +13 -0
- package/src/tools/worker-evidence.ts +19 -13
- package/src/worker/spec-contract.ts +2 -1
|
@@ -123,6 +123,7 @@ import { AssignmentRegistry, applyActiveRouteSelection, asAssignmentId } from ".
|
|
|
123
123
|
import type { AssignmentAttemptStartEvent } from "./assignment-events.js";
|
|
124
124
|
import { reconcileOrphanAssignments } from "./assignment-reconcile.js";
|
|
125
125
|
import {
|
|
126
|
+
assignmentProcessOwnerAlive,
|
|
126
127
|
cancelStoredAssignment,
|
|
127
128
|
failQueuedAssignment,
|
|
128
129
|
getStoredAssignment,
|
|
@@ -167,6 +168,7 @@ import { routeFactVerdict } from "./fleet-preflight.js";
|
|
|
167
168
|
import { competeStanceLiner, isBoundedGateRolePrompt } from "./gate-role-prompts.js";
|
|
168
169
|
import { classifyHeartbeat, DEFAULT_HEARTBEAT_SPEC, type HeartbeatSpec, type HeartbeatStatus } from "./heartbeat.js";
|
|
169
170
|
import { hostVerificationRejection, runHostVerification } from "./host-verification.js";
|
|
171
|
+
import { renderDispatchIntentRequirements } from "./intent-requirements.js";
|
|
170
172
|
import { adaptJointRouteInput } from "./joint-route-adapter.js";
|
|
171
173
|
import {
|
|
172
174
|
configuredJointNodes,
|
|
@@ -181,6 +183,12 @@ import {
|
|
|
181
183
|
resultContractWasDue,
|
|
182
184
|
runStatusForOutcome,
|
|
183
185
|
} from "./outcome.js";
|
|
186
|
+
import {
|
|
187
|
+
type DispatchPathScope,
|
|
188
|
+
declaredScopeReplacementDiagnostic,
|
|
189
|
+
declaredScopeReplacementNotice,
|
|
190
|
+
resolveDispatchPathScope,
|
|
191
|
+
} from "./path-scope.js";
|
|
184
192
|
import { deriveEnvelopePhaseDurations, deriveRunPhaseDurations, recordRunTimingBestEffort } from "./phase-timing.js";
|
|
185
193
|
import { createFleetPlacementPreviewResolver, createFleetPlacementResolver } from "./placement.js";
|
|
186
194
|
import {
|
|
@@ -614,6 +622,14 @@ function recordWorkerRunEffect(recorder: RunEffectsRecorder, event: Record<strin
|
|
|
614
622
|
*/
|
|
615
623
|
const DISPATCH_DRAIN_GRACE_MS = 2000;
|
|
616
624
|
|
|
625
|
+
/**
|
|
626
|
+
* How many finalized runs keep their observed write targets in memory. The
|
|
627
|
+
* write boundary reads a run's record while closing the window it ran in, which
|
|
628
|
+
* is the same wave, so a few hundred runs of headroom is far more than the
|
|
629
|
+
* lookup ever reaches back for.
|
|
630
|
+
*/
|
|
631
|
+
const RUN_WRITE_TARGET_HISTORY = 512;
|
|
632
|
+
|
|
617
633
|
async function awaitEventDrain(drained: Promise<void>, graceMs = DISPATCH_DRAIN_GRACE_MS): Promise<boolean> {
|
|
618
634
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
619
635
|
const grace = new Promise<false>((resolve) => {
|
|
@@ -751,9 +767,14 @@ function pickOrchestratorScope(safety: SafetyContract): ScopeSpec {
|
|
|
751
767
|
return safety.scopes.workspace;
|
|
752
768
|
}
|
|
753
769
|
|
|
754
|
-
function pickWorkerScope(
|
|
770
|
+
function pickWorkerScope(
|
|
771
|
+
safety: SafetyContract,
|
|
772
|
+
requestedActions: ReadonlyArray<ActionClass>,
|
|
773
|
+
pathScope: DispatchPathScope,
|
|
774
|
+
): ScopeSpec {
|
|
755
775
|
if (requestedActions.every((action) => action === "read")) return safety.scopes.readonly;
|
|
756
|
-
return safety.scopes.workspace;
|
|
776
|
+
if (pathScope.writeBoundaries.length === 0) return safety.scopes.workspace;
|
|
777
|
+
return { ...safety.scopes.workspace, allowedWriteRoots: pathScope.writeBoundaries };
|
|
757
778
|
}
|
|
758
779
|
|
|
759
780
|
function deriveRequestedActions(tools: ReadonlyArray<ToolName>, safety: SafetyContract): ReadonlyArray<ActionClass> {
|
|
@@ -993,6 +1014,25 @@ function hasPersonaOverride(req: DispatchRequest): boolean {
|
|
|
993
1014
|
return typeof req.systemPrompt === "string" && req.systemPrompt.trim().length > 0;
|
|
994
1015
|
}
|
|
995
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* Whether this request's system prompt is a caller-authored persona rather
|
|
1019
|
+
* than one of the coordinator's own bounded gate-role prompts.
|
|
1020
|
+
*
|
|
1021
|
+
* A reviewer, a compete judge, and a council synthesis all carry a system
|
|
1022
|
+
* prompt the coordinator wrote, pinned by {@link isBoundedGateRolePrompt} to
|
|
1023
|
+
* one exact text under one gate role and read-only autonomy. That is the
|
|
1024
|
+
* topology speaking, not an operator reshaping a recipe, so it is not the
|
|
1025
|
+
* thing the shadow and internal audiences are protected from. The ACP
|
|
1026
|
+
* delegation path already draws the line here; without the same line, a
|
|
1027
|
+
* council that seats the builtin read-only `researcher` (a shadow recipe, and
|
|
1028
|
+
* the agent every unnamed council seats) refused `--synthesis judge` outright,
|
|
1029
|
+
* before a single member ran.
|
|
1030
|
+
*/
|
|
1031
|
+
function hasCallerPersonaOverride(req: DispatchRequest): boolean {
|
|
1032
|
+
if (!hasPersonaOverride(req)) return false;
|
|
1033
|
+
return !isBoundedGateRolePrompt({ role: req.gate?.role, autonomy: req.autonomy, systemPrompt: req.systemPrompt });
|
|
1034
|
+
}
|
|
1035
|
+
|
|
996
1036
|
function personaOverrideFor(req: DispatchRequest, staticCompositionHash: string | null): RunPersonaOverride | null {
|
|
997
1037
|
if (!hasPersonaOverride(req) || staticCompositionHash === null) return null;
|
|
998
1038
|
return { promptHash: staticCompositionHash };
|
|
@@ -1003,9 +1043,8 @@ function personaOverrideFor(req: DispatchRequest, staticCompositionHash: string
|
|
|
1003
1043
|
* flows through dynamic messages, never through the system prompt. The system
|
|
1004
1044
|
* prompt itself is stable per recipe and tool surface with one exception: the
|
|
1005
1045
|
* operator-editable layer (`additionalFragments`) folds in path-scoped project
|
|
1006
|
-
* rules selected
|
|
1007
|
-
*
|
|
1008
|
-
* runs of one recipe when a rule's glob matches one task and not the other.
|
|
1046
|
+
* rules selected from the canonical request scope, so `staticCompositionHash`
|
|
1047
|
+
* can differ between two runs of one recipe when their scoped rules differ.
|
|
1009
1048
|
* A local model's worker prefix cache misses on that flip; the trade was made
|
|
1010
1049
|
* deliberately in #96 to keep rules scoped rather than shipped wholesale.
|
|
1011
1050
|
*/
|
|
@@ -1074,35 +1113,6 @@ function workerProjectContextIncludesVerification(body: string): boolean {
|
|
|
1074
1113
|
return body.includes("\nVerification expectations:\n");
|
|
1075
1114
|
}
|
|
1076
1115
|
|
|
1077
|
-
/**
|
|
1078
|
-
* Path-like tokens in free text: a slash-separated segment, or a bare
|
|
1079
|
-
* `name.ext` token. The model-facing dispatch tool has no structured
|
|
1080
|
-
* path/file field, so a worker's task text is the only working-context
|
|
1081
|
-
* signal available at compile time; this is best-effort recall, not a
|
|
1082
|
-
* parser. A false-positive token is harmless because `selectActiveRules`
|
|
1083
|
-
* only activates a rule when the token matches that rule's own glob.
|
|
1084
|
-
*/
|
|
1085
|
-
const PATH_TOKEN_RE = /(?:[\w.-]+\/)+[\w.-]+|\b[\w-]+\.[A-Za-z0-9]{1,8}\b/g;
|
|
1086
|
-
|
|
1087
|
-
/**
|
|
1088
|
-
* Best-effort working-context paths for a dispatched worker: `writeRoots`
|
|
1089
|
-
* when the caller set them (the precise signal, for internal/programmatic
|
|
1090
|
-
* callers), plus path-like tokens recalled from the task and briefing text.
|
|
1091
|
-
* Feeds the same `selectActiveRules` a session uses, so a worker whose task
|
|
1092
|
-
* touches a ruled path reads that rule instead of never hearing about it.
|
|
1093
|
-
*/
|
|
1094
|
-
function workerWorkingContextPaths(req: DispatchRequest): string[] {
|
|
1095
|
-
const out = new Set<string>();
|
|
1096
|
-
for (const root of req.writeRoots ?? []) out.add(root);
|
|
1097
|
-
const text = `${req.task}\n${req.briefing ?? ""}`;
|
|
1098
|
-
for (const match of text.matchAll(PATH_TOKEN_RE)) {
|
|
1099
|
-
const token = match[0];
|
|
1100
|
-
if (token.startsWith("http://") || token.startsWith("https://")) continue;
|
|
1101
|
-
out.add(token);
|
|
1102
|
-
}
|
|
1103
|
-
return [...out];
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
1116
|
export function buildDynamicPromptMessages(
|
|
1107
1117
|
req: DispatchRequest,
|
|
1108
1118
|
dynamicContext: WorkerDynamicContext = {},
|
|
@@ -1126,6 +1136,10 @@ export function buildDynamicPromptMessages(
|
|
|
1126
1136
|
const body = `Safety posture: autonomy ${autonomy}. ${workerSafetyOneLiner(autonomy, permission)} Worker permission routing: ${permission}.`;
|
|
1127
1137
|
messages.push({ id: "dispatch-safety-posture", body, contentHash: sha256(body) });
|
|
1128
1138
|
}
|
|
1139
|
+
const requirements = renderDispatchIntentRequirements(req.intent);
|
|
1140
|
+
if (requirements !== null) {
|
|
1141
|
+
messages.push({ id: "dispatch-intent-requirements", body: requirements, contentHash: sha256(requirements) });
|
|
1142
|
+
}
|
|
1129
1143
|
if (req.competeStance !== undefined) {
|
|
1130
1144
|
const body = competeStanceLiner(req.competeStance);
|
|
1131
1145
|
messages.push({ id: "dispatch-compete-stance", body, contentHash: sha256(body) });
|
|
@@ -1250,6 +1264,7 @@ interface DispatchAdmissionStage {
|
|
|
1250
1264
|
|
|
1251
1265
|
interface DispatchWorkerSpecInput {
|
|
1252
1266
|
req: DispatchRequest;
|
|
1267
|
+
pathScope: DispatchPathScope;
|
|
1253
1268
|
target: ResolvedTarget;
|
|
1254
1269
|
admission: DispatchAdmissionStage;
|
|
1255
1270
|
recipe?: AgentRecipe | null;
|
|
@@ -1269,6 +1284,7 @@ interface DispatchWorkerSpecInput {
|
|
|
1269
1284
|
|
|
1270
1285
|
interface DispatchLifecycleStage {
|
|
1271
1286
|
recipe: AgentRecipe | null;
|
|
1287
|
+
pathScope: DispatchPathScope;
|
|
1272
1288
|
admission: DispatchAdmissionStage;
|
|
1273
1289
|
target: ResolvedTarget;
|
|
1274
1290
|
cwd: string;
|
|
@@ -1304,6 +1320,7 @@ interface DispatchLifecycleStage {
|
|
|
1304
1320
|
|
|
1305
1321
|
interface AcpDelegationLifecycleStage {
|
|
1306
1322
|
admission: DispatchAdmissionStage;
|
|
1323
|
+
pathScope: DispatchPathScope;
|
|
1307
1324
|
agentConfig: ReturnType<ConfigContract["get"]>["delegation"]["agents"][number];
|
|
1308
1325
|
cwd: string;
|
|
1309
1326
|
systemPrompt: string;
|
|
@@ -1488,10 +1505,6 @@ function explicitToolCapability(
|
|
|
1488
1505
|
return typeof fromKnowledgeBase === "boolean" ? fromKnowledgeBase : null;
|
|
1489
1506
|
}
|
|
1490
1507
|
|
|
1491
|
-
function writeConfinedRequest(req: DispatchRequest): boolean {
|
|
1492
|
-
return req.writeRoots !== undefined && req.writeRoots.length > 0;
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
1508
|
function effectiveToolNames(
|
|
1496
1509
|
allowedTools: ReadonlyArray<ToolName>,
|
|
1497
1510
|
target: ResolvedTarget,
|
|
@@ -1717,6 +1730,7 @@ function resolveDispatchAdmissionStage(
|
|
|
1717
1730
|
req: DispatchRequest,
|
|
1718
1731
|
recipe: AgentRecipe,
|
|
1719
1732
|
safety: SafetyContract,
|
|
1733
|
+
pathScope: DispatchPathScope,
|
|
1720
1734
|
): DispatchAdmissionStage {
|
|
1721
1735
|
const recipeTools = recipe.tools;
|
|
1722
1736
|
const candidateTools = recipeTools && recipeTools.length > 0 ? (Array.from(recipeTools) as ToolName[]) : [];
|
|
@@ -1730,7 +1744,7 @@ function resolveDispatchAdmissionStage(
|
|
|
1730
1744
|
}
|
|
1731
1745
|
const requestedActions = deriveRequestedActions(allowedTools, safety);
|
|
1732
1746
|
const orchScope = pickOrchestratorScope(safety);
|
|
1733
|
-
const workerScope = pickWorkerScope(safety, requestedActions);
|
|
1747
|
+
const workerScope = pickWorkerScope(safety, requestedActions, pathScope);
|
|
1734
1748
|
const verdict = admit(
|
|
1735
1749
|
{
|
|
1736
1750
|
requestedScope: workerScope,
|
|
@@ -1755,7 +1769,7 @@ function resolveDelegationAdmissionStage(req: DispatchRequest, safety: SafetyCon
|
|
|
1755
1769
|
const allowedTools = applyToolProfile([], req.toolProfile);
|
|
1756
1770
|
const requestedActions = deriveRequestedActions(allowedTools, safety);
|
|
1757
1771
|
const orchScope = pickOrchestratorScope(safety);
|
|
1758
|
-
const workerScope = pickWorkerScope(safety, requestedActions);
|
|
1772
|
+
const workerScope = pickWorkerScope(safety, requestedActions, resolveDispatchPathScope(req));
|
|
1759
1773
|
const verdict = admit(
|
|
1760
1774
|
{
|
|
1761
1775
|
requestedScope: workerScope,
|
|
@@ -1833,9 +1847,17 @@ function buildDispatchWorkerSpec(input: DispatchWorkerSpecInput, config?: Config
|
|
|
1833
1847
|
// The worker repairs against exactly the contract the orchestrator will seal.
|
|
1834
1848
|
// A gate role that overrides the recipe contract gets no worker-side repair,
|
|
1835
1849
|
// for the same reason it gets no recipe validation.
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1850
|
+
//
|
|
1851
|
+
// The request's own override is the contract whether or not the seated
|
|
1852
|
+
// recipe declares one, because that is the resolution the seal already uses.
|
|
1853
|
+
// Requiring a recipe contract here meant a caller-supplied override against a
|
|
1854
|
+
// recipe with none was sealed but never sent, so the worker spent no repair
|
|
1855
|
+
// round on a contract it was never told about and the run failed on a shape
|
|
1856
|
+
// nothing had asked it for.
|
|
1857
|
+
const workerResultContract = appliesRecipeResultContract(input.req.gate?.role)
|
|
1858
|
+
? (input.req.resultContractOverride ?? input.recipe?.resultContract)
|
|
1859
|
+
: undefined;
|
|
1860
|
+
if (workerResultContract) spec.resultContract = workerResultContract;
|
|
1839
1861
|
const product = input.req.product ?? input.recipe?.product;
|
|
1840
1862
|
if (product) spec.product = product;
|
|
1841
1863
|
// The orchestrator's tool decision is the one the run was admitted under and
|
|
@@ -1886,14 +1908,9 @@ function buildDispatchWorkerSpec(input: DispatchWorkerSpecInput, config?: Config
|
|
|
1886
1908
|
if (escalation) spec.escalation = { timeoutMs: escalation.timeoutMs, fallback: escalation.fallback };
|
|
1887
1909
|
}
|
|
1888
1910
|
assertRuntimeCanHonorWorkerPermissionMode(input.target.runtime, spec.onPermission);
|
|
1889
|
-
// Carry write
|
|
1890
|
-
//
|
|
1891
|
-
|
|
1892
|
-
// reaches the worker absolute, matching the validation contract.
|
|
1893
|
-
if (input.req.writeRoots !== undefined && input.req.writeRoots.length > 0) {
|
|
1894
|
-
const jobCwd = input.req.cwd !== undefined && input.req.cwd.length > 0 ? input.req.cwd : process.cwd();
|
|
1895
|
-
spec.writeRoots = input.req.writeRoots.map((root) => resolvePath(jobCwd, root));
|
|
1896
|
-
}
|
|
1911
|
+
// Carry canonical write boundaries to the worker safety seam. Exact files
|
|
1912
|
+
// remain exact and trailing-slash entries remain subtrees after resolution.
|
|
1913
|
+
if (input.pathScope.writeBoundaries.length > 0) spec.writeRoots = [...input.pathScope.writeBoundaries];
|
|
1897
1914
|
assertWriteRootsEnforceable(input.target.runtime, spec.writeRoots);
|
|
1898
1915
|
// Carry the tool profile so external CLI runtimes that cannot mediate
|
|
1899
1916
|
// per-tool calls can refuse a narrowing profile they would otherwise ignore.
|
|
@@ -2388,6 +2405,26 @@ export function createDispatchBundle(
|
|
|
2388
2405
|
const active = new Map<string, ActiveRun>();
|
|
2389
2406
|
const targetCooldowns = new Map<string, { until: number; reason: string }>();
|
|
2390
2407
|
const ownedReservations = new Set<string>();
|
|
2408
|
+
/**
|
|
2409
|
+
* Paths each finalized run's own tool calls aimed a successful mutation at,
|
|
2410
|
+
* newest last. `null` records a run whose write record is unusable, which is
|
|
2411
|
+
* a different answer from an empty list and has to survive as one.
|
|
2412
|
+
*
|
|
2413
|
+
* Held in memory rather than sealed into the receipt: the only reader is the
|
|
2414
|
+
* write boundary closing a window seconds later in this process, and a
|
|
2415
|
+
* per-run path list is run-shaped evidence the receipt schema does not carry.
|
|
2416
|
+
* Bounded because a long session finalizes many runs and none of this is
|
|
2417
|
+
* needed once its window has closed.
|
|
2418
|
+
*/
|
|
2419
|
+
const runWriteTargets = new Map<string, ReadonlyArray<string> | null>();
|
|
2420
|
+
const recordRunWriteTargets = (runId: string, paths: ReadonlyArray<string> | null): void => {
|
|
2421
|
+
runWriteTargets.set(runId, paths);
|
|
2422
|
+
while (runWriteTargets.size > RUN_WRITE_TARGET_HISTORY) {
|
|
2423
|
+
const oldest = runWriteTargets.keys().next();
|
|
2424
|
+
if (oldest.done === true) break;
|
|
2425
|
+
runWriteTargets.delete(oldest.value);
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2391
2428
|
const capacityAdmission = createCapacityAdmissionController({
|
|
2392
2429
|
limits: () => {
|
|
2393
2430
|
const settings = getEffectiveSettings();
|
|
@@ -3169,6 +3206,27 @@ export function createDispatchBundle(
|
|
|
3169
3206
|
targetCooldowns.set(key, { until: now() + cooldownMs, reason: failureClass });
|
|
3170
3207
|
}
|
|
3171
3208
|
|
|
3209
|
+
function publishDispatchPathScope(req: DispatchRequest, pathScope: DispatchPathScope): void {
|
|
3210
|
+
const replacementDiagnostic = declaredScopeReplacementDiagnostic(pathScope);
|
|
3211
|
+
if (replacementDiagnostic !== null) {
|
|
3212
|
+
reportDispatchDiagnostic("typed scope replacement", new Error(replacementDiagnostic));
|
|
3213
|
+
}
|
|
3214
|
+
// Only the replacement case reaches the transcript. A dispatch that
|
|
3215
|
+
// declared no intent is the ordinary case rather than an anomaly, and one
|
|
3216
|
+
// receipt in ninety-nine carries an intent key today, so noticing it would
|
|
3217
|
+
// warn on almost every dispatch and teach the operator to skip the channel.
|
|
3218
|
+
// Its provenance is not lost: the receipt seals `pathProvenance`, and the
|
|
3219
|
+
// approval artifact renders every inferred entry in full before a
|
|
3220
|
+
// supervised dispatch runs, which is where an operator can still act on it.
|
|
3221
|
+
const notice = declaredScopeReplacementNotice(pathScope);
|
|
3222
|
+
if (notice !== null) {
|
|
3223
|
+
context.bus.emit(BusChannels.DispatchScopeNotice, {
|
|
3224
|
+
...notice,
|
|
3225
|
+
agentId: req.agentId,
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3172
3230
|
async function resolveLifecycle(req: DispatchRequest, settings: EffectiveSettings): Promise<DispatchLifecycleStage> {
|
|
3173
3231
|
const recipe = agents.get(req.agentId);
|
|
3174
3232
|
if (!recipe) {
|
|
@@ -3180,7 +3238,7 @@ export function createDispatchBundle(
|
|
|
3180
3238
|
`dispatch: agent '${req.agentId}' is a ${spec.audience} agent reserved for Clio internal orchestration`,
|
|
3181
3239
|
);
|
|
3182
3240
|
}
|
|
3183
|
-
if (
|
|
3241
|
+
if (hasCallerPersonaOverride(req) && (spec.audience === "shadow" || spec.audience === "internal")) {
|
|
3184
3242
|
throw new Error(`dispatch: persona overrides are not allowed for ${spec.audience} agent '${req.agentId}'`);
|
|
3185
3243
|
}
|
|
3186
3244
|
// A read-only recipe pointed at a mutating task is knowable here and costs
|
|
@@ -3202,7 +3260,9 @@ export function createDispatchBundle(
|
|
|
3202
3260
|
specs: agents.listSpecs(),
|
|
3203
3261
|
});
|
|
3204
3262
|
if (capabilityMismatch?.verdict === "refuse") throw new Error(capabilityMismatch.detail);
|
|
3205
|
-
const
|
|
3263
|
+
const pathScope = resolveDispatchPathScope(req);
|
|
3264
|
+
publishDispatchPathScope(req, pathScope);
|
|
3265
|
+
const admission = resolveDispatchAdmissionStage(req, recipe, safety, pathScope);
|
|
3206
3266
|
const targets = readWorkerTargets(settings);
|
|
3207
3267
|
const target = resolveDispatchTarget(
|
|
3208
3268
|
req,
|
|
@@ -3221,7 +3281,7 @@ export function createDispatchBundle(
|
|
|
3221
3281
|
const sessionAutonomy = settings?.autonomy ?? "auto-edit";
|
|
3222
3282
|
const effectiveAutonomy = clampWorkerAutonomy(sessionAutonomy, req.autonomy);
|
|
3223
3283
|
const effectiveTools = withLedgerToolNarrowing(
|
|
3224
|
-
effectiveToolNames(admission.allowedTools, target,
|
|
3284
|
+
effectiveToolNames(admission.allowedTools, target, pathScope.writeBoundaries.length > 0, deniedToolNames(req)),
|
|
3225
3285
|
req,
|
|
3226
3286
|
);
|
|
3227
3287
|
assertPostRuntimeToolCompatibility(req.agentId, spec, effectiveTools, target);
|
|
@@ -3250,7 +3310,7 @@ export function createDispatchBundle(
|
|
|
3250
3310
|
dynamic: false,
|
|
3251
3311
|
},
|
|
3252
3312
|
cwd,
|
|
3253
|
-
workingContextPaths:
|
|
3313
|
+
workingContextPaths: pathScope.workingContextPaths,
|
|
3254
3314
|
});
|
|
3255
3315
|
const systemPrompt = compiledWorkerPrompt.systemPrompt;
|
|
3256
3316
|
const budgetEnvelope = resolveEffectiveWorkerBudget({
|
|
@@ -3294,6 +3354,7 @@ export function createDispatchBundle(
|
|
|
3294
3354
|
const limitations = runtimeLimitations(runtimeKind, target.runtime.id);
|
|
3295
3355
|
return {
|
|
3296
3356
|
recipe,
|
|
3357
|
+
pathScope,
|
|
3297
3358
|
admission: effectiveAdmission,
|
|
3298
3359
|
target,
|
|
3299
3360
|
cwd,
|
|
@@ -3331,12 +3392,7 @@ export function createDispatchBundle(
|
|
|
3331
3392
|
): AcpDelegationLifecycleStage {
|
|
3332
3393
|
const agentId = req.delegationAgentId;
|
|
3333
3394
|
if (!agentId) throw new Error("dispatch: missing delegationAgentId");
|
|
3334
|
-
|
|
3335
|
-
role: req.gate?.role,
|
|
3336
|
-
autonomy: req.autonomy,
|
|
3337
|
-
systemPrompt: req.systemPrompt,
|
|
3338
|
-
});
|
|
3339
|
-
if (hasPersonaOverride(req) && !boundedGateRolePrompt) {
|
|
3395
|
+
if (hasCallerPersonaOverride(req)) {
|
|
3340
3396
|
throw new Error(`dispatch: persona overrides are not allowed for ACP delegation agent '${agentId}'`);
|
|
3341
3397
|
}
|
|
3342
3398
|
if (req.agentId && maybeAgents) {
|
|
@@ -3365,6 +3421,7 @@ export function createDispatchBundle(
|
|
|
3365
3421
|
);
|
|
3366
3422
|
}
|
|
3367
3423
|
const cwd = req.cwd ?? process.cwd();
|
|
3424
|
+
const pathScope = resolveDispatchPathScope(req);
|
|
3368
3425
|
const personaBody = workerPersonaBody(req, null, []);
|
|
3369
3426
|
// ACP owns an unknown external tool inventory, so it receives the raw
|
|
3370
3427
|
// bounded persona rather than a native Clio schema-harness claim.
|
|
@@ -3391,6 +3448,7 @@ export function createDispatchBundle(
|
|
|
3391
3448
|
const personaOverride = personaOverrideFor(req, staticCompositionHash);
|
|
3392
3449
|
return {
|
|
3393
3450
|
admission,
|
|
3451
|
+
pathScope,
|
|
3394
3452
|
agentConfig: configured,
|
|
3395
3453
|
cwd,
|
|
3396
3454
|
systemPrompt,
|
|
@@ -3426,6 +3484,7 @@ export function createDispatchBundle(
|
|
|
3426
3484
|
finalPromise: Promise<RunReceipt>;
|
|
3427
3485
|
}> {
|
|
3428
3486
|
const lifecycle = resolveAcpDelegationLifecycle(req, settings);
|
|
3487
|
+
publishDispatchPathScope(req, lifecycle.pathScope);
|
|
3429
3488
|
timing.decisionCompletedAt = new Date(now()).toISOString();
|
|
3430
3489
|
const targetId = `delegation:${lifecycle.agentConfig.id}`;
|
|
3431
3490
|
const runtimeId = "acp";
|
|
@@ -3799,6 +3858,7 @@ export function createDispatchBundle(
|
|
|
3799
3858
|
requestOrigin: lifecycle.requestOrigin,
|
|
3800
3859
|
task: req.task,
|
|
3801
3860
|
...(req.intent !== undefined ? { intent: structuredClone(req.intent) } : {}),
|
|
3861
|
+
pathScope: structuredClone(lifecycle.pathScope.provenance),
|
|
3802
3862
|
targetId,
|
|
3803
3863
|
wireModelId,
|
|
3804
3864
|
runtimeId,
|
|
@@ -4276,6 +4336,7 @@ export function createDispatchBundle(
|
|
|
4276
4336
|
const spec = buildDispatchWorkerSpec(
|
|
4277
4337
|
{
|
|
4278
4338
|
req,
|
|
4339
|
+
pathScope: lifecycle.pathScope,
|
|
4279
4340
|
target: lifecycle.target,
|
|
4280
4341
|
admission: lifecycle.admission,
|
|
4281
4342
|
recipe: lifecycle.recipe,
|
|
@@ -4893,6 +4954,7 @@ export function createDispatchBundle(
|
|
|
4893
4954
|
outcomeCode,
|
|
4894
4955
|
lineage,
|
|
4895
4956
|
...(req.intent !== undefined ? { intent: structuredClone(req.intent) } : {}),
|
|
4957
|
+
pathScope: structuredClone(lifecycle.pathScope.provenance),
|
|
4896
4958
|
identity,
|
|
4897
4959
|
node: placement?.node ?? LOCAL_RUN_NODE,
|
|
4898
4960
|
...receiptAttestationFields(worker.attestation?.() ?? null),
|
|
@@ -5276,6 +5338,25 @@ export function createDispatchBundle(
|
|
|
5276
5338
|
}
|
|
5277
5339
|
ledgerRef.update(envelope.id, ledgerPatch);
|
|
5278
5340
|
const receipt = ledgerRef.recordReceipt(envelope.id, sealRouteDecision(receiptDraft, routeObservation.decision));
|
|
5341
|
+
// Two independent ways a run's write record can fail to be a closed
|
|
5342
|
+
// list, and either one files it as `null` rather than as an empty
|
|
5343
|
+
// one. The difference decides whether the write boundary may clear a
|
|
5344
|
+
// path it did not see this run touch.
|
|
5345
|
+
//
|
|
5346
|
+
// The telemetry axis is whether the events were seen at all: a
|
|
5347
|
+
// subprocess runtime publishes no tool events, and a partial trail
|
|
5348
|
+
// has its holes exactly where an unobserved write would sit.
|
|
5349
|
+
//
|
|
5350
|
+
// The opacity axis is whether the events that were seen say enough.
|
|
5351
|
+
// A run that called bash, verify, dispatch, or steer to a clean exit
|
|
5352
|
+
// could have written through a channel no argument names, so nothing
|
|
5353
|
+
// in the checkout can be cleared on the strength of its absence from
|
|
5354
|
+
// the set. That run's window keeps today's blame-the-whole-diff
|
|
5355
|
+
// behaviour; a run that never touched such a tool keeps the
|
|
5356
|
+
// protection.
|
|
5357
|
+
const writeRecordUsable =
|
|
5358
|
+
receipt.safety?.toolTelemetry?.coverage === "complete" && observedRunEffects.writeRecordComplete;
|
|
5359
|
+
recordRunWriteTargets(envelope.id, writeRecordUsable ? [...observedRunEffects.mutatedPaths] : null);
|
|
5279
5360
|
await ledgerRef.persist();
|
|
5280
5361
|
if (worktreeReceipt?.applied === true && req.taskWorktree !== undefined) {
|
|
5281
5362
|
try {
|
|
@@ -5510,10 +5591,11 @@ export function createDispatchBundle(
|
|
|
5510
5591
|
`dispatch: agent '${req.agentId}' is a ${agentSpec.audience} agent reserved for Clio internal orchestration`,
|
|
5511
5592
|
);
|
|
5512
5593
|
}
|
|
5513
|
-
if (
|
|
5594
|
+
if (hasCallerPersonaOverride(req) && (agentSpec.audience === "shadow" || agentSpec.audience === "internal")) {
|
|
5514
5595
|
throw new Error(`dispatch: persona overrides are not allowed for ${agentSpec.audience} agent '${req.agentId}'`);
|
|
5515
5596
|
}
|
|
5516
|
-
const
|
|
5597
|
+
const pathScope = resolveDispatchPathScope(req);
|
|
5598
|
+
const admission = resolveDispatchAdmissionStage(req, recipe, safety, pathScope);
|
|
5517
5599
|
const targets = readWorkerTargets(settings);
|
|
5518
5600
|
const target = resolveDispatchTarget(
|
|
5519
5601
|
req,
|
|
@@ -5526,14 +5608,14 @@ export function createDispatchBundle(
|
|
|
5526
5608
|
);
|
|
5527
5609
|
enforceCapabilityGate(target.target.id, target.modelCapabilities, req.requiredCapabilities);
|
|
5528
5610
|
const effectiveTools = withLedgerToolNarrowing(
|
|
5529
|
-
effectiveToolNames(admission.allowedTools, target,
|
|
5611
|
+
effectiveToolNames(admission.allowedTools, target, pathScope.writeBoundaries.length > 0, deniedToolNames(req)),
|
|
5530
5612
|
req,
|
|
5531
5613
|
);
|
|
5532
5614
|
assertPostRuntimeToolCompatibility(req.agentId, agentSpec, effectiveTools, target);
|
|
5533
5615
|
assertRuntimeCanHonorWorkerPermissionMode(target.runtime, settings?.workers.onPermission ?? "deny");
|
|
5534
5616
|
assertWorkerBudgetEnforceable(target.runtime, agentSpec.budget !== null || req.budget !== undefined);
|
|
5535
5617
|
assertResponseSchemaEnforceable(target.runtime, target.modelCapabilities, req.responseSchema, effectiveTools.length);
|
|
5536
|
-
assertWriteRootsEnforceable(target.runtime,
|
|
5618
|
+
assertWriteRootsEnforceable(target.runtime, pathScope.writeBoundaries);
|
|
5537
5619
|
assertProtectedArtifactsEnforceable(
|
|
5538
5620
|
target.runtime.id,
|
|
5539
5621
|
target.runtime.kind !== "subprocess",
|
|
@@ -5833,7 +5915,7 @@ export function createDispatchBundle(
|
|
|
5833
5915
|
async function sealCouncilSynthesis(input: {
|
|
5834
5916
|
group: string;
|
|
5835
5917
|
round: number;
|
|
5836
|
-
kind: "none" | "vote";
|
|
5918
|
+
kind: "none" | "vote" | "judge";
|
|
5837
5919
|
text: string;
|
|
5838
5920
|
subjects: ReadonlyArray<{ runId: string; digest: string | null }>;
|
|
5839
5921
|
template: RunReceipt;
|
|
@@ -5991,6 +6073,7 @@ export function createDispatchBundle(
|
|
|
5991
6073
|
const reconciledLedger = ledger;
|
|
5992
6074
|
const reconciled = await reconcileOrphanAssignments({
|
|
5993
6075
|
listRunning: () => listStoredAssignments().filter((record) => record.status === "running"),
|
|
6076
|
+
ownerAlive: assignmentProcessOwnerAlive,
|
|
5994
6077
|
lookupAttempt: (runId) => {
|
|
5995
6078
|
const envelope = reconciledLedger?.get(runId) ?? null;
|
|
5996
6079
|
if (!envelope) return null;
|
|
@@ -6000,7 +6083,8 @@ export function createDispatchBundle(
|
|
|
6000
6083
|
succeeded: envelope.outcome === "succeeded",
|
|
6001
6084
|
};
|
|
6002
6085
|
},
|
|
6003
|
-
settle: (assignmentId, terminalRunId, status) =>
|
|
6086
|
+
settle: (assignmentId, terminalRunId, status, owner) =>
|
|
6087
|
+
settleStoredAssignment(assignmentId, terminalRunId, status, owner),
|
|
6004
6088
|
});
|
|
6005
6089
|
if ((reconciled.recovered > 0 || reconciled.abandoned > 0) && process.env.CLIO_CODER_INTERACTIVE !== "1") {
|
|
6006
6090
|
process.stderr.write(
|
|
@@ -6204,6 +6288,11 @@ export function createDispatchBundle(
|
|
|
6204
6288
|
if (!ledger) return null;
|
|
6205
6289
|
return ledger.get(runId);
|
|
6206
6290
|
},
|
|
6291
|
+
observedRunWrites(runId) {
|
|
6292
|
+
// A run this process never finalized, or finalized long enough ago to
|
|
6293
|
+
// have been evicted, is unknown rather than silent, so both answer null.
|
|
6294
|
+
return runWriteTargets.get(runId) ?? null;
|
|
6295
|
+
},
|
|
6207
6296
|
assignments: {
|
|
6208
6297
|
get: (id) => assignments.get(id),
|
|
6209
6298
|
getStored: (id) => getStoredAssignment(id),
|
|
@@ -21,6 +21,7 @@ import type { FleetCommandRegistry } from "../agents/fleet-commands.js";
|
|
|
21
21
|
import type { FleetContract } from "../agents/fleet-contract.js";
|
|
22
22
|
import { resultContractAuthorship } from "../agents/result-contract.js";
|
|
23
23
|
import type { AgentSpec } from "../agents/spec.js";
|
|
24
|
+
import { claimAssignmentVerdict, recordAssignmentAttempt, settleStoredAssignment } from "./assignment-store.js";
|
|
24
25
|
import { runCodeStep } from "./code-step.js";
|
|
25
26
|
import { codeStepDir, writeCodeStepRecord } from "./code-step-store.js";
|
|
26
27
|
import type { DispatchContract, DispatchRequest } from "./contract.js";
|
|
@@ -247,13 +248,47 @@ export async function executeFleetRun(input: ExecuteFleetRunInput): Promise<Flee
|
|
|
247
248
|
dynamicPlans: [],
|
|
248
249
|
};
|
|
249
250
|
await writeFleetRun(fleetRunRecord);
|
|
251
|
+
// Every step of this run dispatches under `fleetRootId` as its lineage root,
|
|
252
|
+
// so every agent step also settles the assignment row of that id. One step is
|
|
253
|
+
// not the run, and letting the last one to finish write the row reported a
|
|
254
|
+
// green step of a run that aborted at step 2 of 7 as `succeeded`. The run
|
|
255
|
+
// claims the verdict here and answers for itself at the end.
|
|
256
|
+
await claimAssignmentVerdict(fleetRootId, "fleet");
|
|
257
|
+
/** File one step's terminal run id, whichever kind produced it. */
|
|
258
|
+
const fileStepAttempt = (terminalRunId: string): Promise<unknown> =>
|
|
259
|
+
recordAssignmentAttempt(fleetRootId, terminalRunId);
|
|
260
|
+
/**
|
|
261
|
+
* The run's own word on its ledger row. A settle that cannot be written
|
|
262
|
+
* leaves the row `running`, which startup reconciliation resolves to failed,
|
|
263
|
+
* so a lost write never becomes a false success.
|
|
264
|
+
*/
|
|
265
|
+
const settleFleetAssignment = (cleanRun: boolean): Promise<unknown> =>
|
|
266
|
+
settleStoredAssignment(
|
|
267
|
+
fleetRootId,
|
|
268
|
+
fleetRunRecord.steps.at(-1)?.result.terminalRunId ?? fleetRootId,
|
|
269
|
+
cleanRun ? "succeeded" : "failed",
|
|
270
|
+
"fleet",
|
|
271
|
+
).catch(() => undefined);
|
|
250
272
|
const boundaryEnforcer = createWriteBoundaryEnforcer({
|
|
251
273
|
root: workspaceRoot,
|
|
252
274
|
rootId: fleetRootId,
|
|
253
275
|
boundaryFor: (stepId) => livePlan.steps.find((step) => step.id === stepId)?.writes,
|
|
276
|
+
recordedWritesFor: (stepId) => {
|
|
277
|
+
const step = livePlan.steps.find((entry) => entry.id === stepId);
|
|
278
|
+
// A code step runs a registered command in a subprocess. Nothing
|
|
279
|
+
// enumerates what that command wrote, so the step contributes no record
|
|
280
|
+
// and its window falls back to blaming the whole diff.
|
|
281
|
+
if (step === undefined || step.kind !== "agent") return null;
|
|
282
|
+
const receipt = receiptsByStep.get(stepId);
|
|
283
|
+
if (receipt === undefined) return null;
|
|
284
|
+
return dispatch.observedRunWrites?.(receipt.runId) ?? null;
|
|
285
|
+
},
|
|
254
286
|
onVerdict(verdict, path) {
|
|
255
|
-
|
|
256
|
-
|
|
287
|
+
// Concurrent changes are reported too. The window did not fail for them
|
|
288
|
+
// and nothing was touched, and an operator whose file changed under a
|
|
289
|
+
// running fleet should hear it from the fleet.
|
|
290
|
+
if (verdict.detail === null) return;
|
|
291
|
+
notice(`write boundary ${verdict.window}: ${verdict.detail} record=${path}`, "write-boundary");
|
|
257
292
|
},
|
|
258
293
|
});
|
|
259
294
|
const receipts: RunReceipt[] = [];
|
|
@@ -285,6 +320,7 @@ export async function executeFleetRun(input: ExecuteFleetRunInput): Promise<Flee
|
|
|
285
320
|
costUsd: receiptsByStep.get(stepId)?.costUsd ?? 0,
|
|
286
321
|
result: replayedResult,
|
|
287
322
|
});
|
|
323
|
+
await fileStepAttempt(replayedResult.terminalRunId);
|
|
288
324
|
}
|
|
289
325
|
const planStep = (stepId: string) => livePlan.steps.find((entry) => entry.id === stepId);
|
|
290
326
|
|
|
@@ -322,6 +358,9 @@ export async function executeFleetRun(input: ExecuteFleetRunInput): Promise<Flee
|
|
|
322
358
|
try {
|
|
323
359
|
result = await executePlan(livePlan, {
|
|
324
360
|
preflight(step) {
|
|
361
|
+
// The fleet write boundary is deliberately enforced after the step. The enforcer in
|
|
362
|
+
// write-boundary-enforcer.ts snapshots and verifies the step window. Pre-emptive
|
|
363
|
+
// confinement for declared commands would require a command sandbox, which does not exist.
|
|
325
364
|
const request: DispatchRequest = {
|
|
326
365
|
agentId: step.agentId,
|
|
327
366
|
executionRole: step.executionRole,
|
|
@@ -372,6 +411,9 @@ export async function executeFleetRun(input: ExecuteFleetRunInput): Promise<Flee
|
|
|
372
411
|
if (step.planParentId !== undefined && parentReceipt === undefined) {
|
|
373
412
|
throw new Error(`fleet dynamic step '${step.id}' has no terminal plan-step receipt`);
|
|
374
413
|
}
|
|
414
|
+
// The fleet write boundary is deliberately enforced after the step. The enforcer in
|
|
415
|
+
// write-boundary-enforcer.ts snapshots and verifies the step window. Pre-emptive
|
|
416
|
+
// confinement for declared commands would require a command sandbox, which does not exist.
|
|
375
417
|
const request: DispatchRequest = {
|
|
376
418
|
agentId: step.agentId,
|
|
377
419
|
executionRole: step.executionRole,
|
|
@@ -671,6 +713,11 @@ export async function executeFleetRun(input: ExecuteFleetRunInput): Promise<Flee
|
|
|
671
713
|
onStepSettled: async (step, stepResult) => {
|
|
672
714
|
fleetRunRecord.steps.push({ stepId: step.id, result: stepResult });
|
|
673
715
|
await writeFleetRun(fleetRunRecord);
|
|
716
|
+
// The scheduler settles both kinds of step here, so this is where a
|
|
717
|
+
// deterministic step enters the assignment: a code step's `code-*` run
|
|
718
|
+
// id reads exactly like an agent receipt id, and its evidence sits under
|
|
719
|
+
// `code-steps/<fleetRootId>/`.
|
|
720
|
+
await fileStepAttempt(stepResult.terminalRunId);
|
|
674
721
|
},
|
|
675
722
|
spliceAfter(currentPlan, step, stepResult) {
|
|
676
723
|
if (stepResult.delegationPlan === undefined) return null;
|
|
@@ -712,6 +759,9 @@ export async function executeFleetRun(input: ExecuteFleetRunInput): Promise<Flee
|
|
|
712
759
|
} catch (error) {
|
|
713
760
|
fleetRunRecord.endedAt = new Date().toISOString();
|
|
714
761
|
await writeFleetRun(fleetRunRecord);
|
|
762
|
+
// A run the scheduler could not carry to the end never reached a verdict,
|
|
763
|
+
// and a fleet that threw is not a fleet that worked.
|
|
764
|
+
await settleFleetAssignment(false);
|
|
715
765
|
throw error;
|
|
716
766
|
}
|
|
717
767
|
fleetRunRecord.endedAt = new Date().toISOString();
|
|
@@ -723,6 +773,10 @@ export async function executeFleetRun(input: ExecuteFleetRunInput): Promise<Flee
|
|
|
723
773
|
const required = livePlan.steps.filter((step) => step.loop === undefined && !result.unneeded.includes(step.id));
|
|
724
774
|
const failedLoops = result.loops.filter((loop) => !loop.resolved);
|
|
725
775
|
const succeededStepCount = required.filter((step) => result.results.get(step.id)?.succeeded === true).length;
|
|
776
|
+
// A step a stop policy left unrun is counted but never succeeded, so a run
|
|
777
|
+
// that halted early can never reach the required count.
|
|
778
|
+
const cleanRun = failedLoops.length === 0 && result.skipped.length === 0 && succeededStepCount === required.length;
|
|
779
|
+
await settleFleetAssignment(cleanRun);
|
|
726
780
|
return {
|
|
727
781
|
rootId: fleetRootId,
|
|
728
782
|
planHash: input.plan.hash,
|
|
@@ -732,6 +786,6 @@ export async function executeFleetRun(input: ExecuteFleetRunInput): Promise<Flee
|
|
|
732
786
|
requiredStepCount: required.length,
|
|
733
787
|
succeededStepCount,
|
|
734
788
|
resolvedLoopCount: result.loops.length - failedLoops.length,
|
|
735
|
-
cleanRun
|
|
789
|
+
cleanRun,
|
|
736
790
|
};
|
|
737
791
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Bounded role prompts shared by dispatch topology construction and ACP admission. */
|
|
2
2
|
|
|
3
|
+
import { COUNCIL_BALLOT_SHAPE, COUNCIL_BALLOT_VERDICT_MAX_BYTES } from "../agents/result-contract.js";
|
|
4
|
+
|
|
3
5
|
export const REVIEWER_GATE_PROMPT = [
|
|
4
6
|
"You are a strict, read-only reviewer of one implementation run.",
|
|
5
7
|
"Inspect the supplied result and repository state with read-only tools.",
|
|
@@ -26,6 +28,42 @@ export const COUNCIL_JUDGE_PROMPT = [
|
|
|
26
28
|
'End with a JSON object only: {"verdict":"...","text":"the supported synthesis"}.',
|
|
27
29
|
].join("\n");
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* What a council member is told when the synthesis is `vote`.
|
|
33
|
+
*
|
|
34
|
+
* The tally is a strict majority over the members' `verdict` fields, and until
|
|
35
|
+
* this directive existed nothing ever asked a member for one (#230). It rides
|
|
36
|
+
* on the member's task rather than on a system prompt, so the seated recipe
|
|
37
|
+
* keeps its own persona: a council's point is running *that* agent across
|
|
38
|
+
* several targets, and replacing its body with a ballot prompt would run a
|
|
39
|
+
* different agent under the same name. The matching `council-ballot` contract
|
|
40
|
+
* travels with it as a `resultContractOverride`, so the postcondition the run
|
|
41
|
+
* seals is the one this text asks for, and a member that answers in its
|
|
42
|
+
* recipe's shape instead gets the ordinary bounded repair rounds rather than a
|
|
43
|
+
* silent absence from the tally.
|
|
44
|
+
*
|
|
45
|
+
* The precedence line is not decoration. A recipe body states its own result
|
|
46
|
+
* shape (`researcher` asks for findings), so without it the member holds two
|
|
47
|
+
* contradictory instructions and small local models resolve the conflict by
|
|
48
|
+
* burying a verdict inside the recipe's shape, which is exactly what the 0.3.7
|
|
49
|
+
* release test observed and read as correct behavior.
|
|
50
|
+
*/
|
|
51
|
+
export const COUNCIL_VOTE_MEMBER_DIRECTIVE = [
|
|
52
|
+
"This council round is a vote. Do the work the task asks for, then cast one ballot on it.",
|
|
53
|
+
`The verdict is a short comparable token the other members can match exactly, such as "yes", "no", "keep", or the name of an option you were given. It must be a single line of at most ${COUNCIL_BALLOT_VERDICT_MAX_BYTES} bytes and is lower-cased before the tally. Anything longer is reasoning: put that in text, along with the evidence behind it.`,
|
|
54
|
+
"Your recipe's own result shape does not apply to this run. The council reads the ballot and nothing else.",
|
|
55
|
+
`End with a JSON object only: ${COUNCIL_BALLOT_SHAPE}`,
|
|
56
|
+
].join("\n");
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The task a council member is given under `synthesis: "vote"`. Plan admission
|
|
60
|
+
* composes it here once, then the runner reuses the task pinned by that plan so
|
|
61
|
+
* the approval artifact shows the exact ask the member receives.
|
|
62
|
+
*/
|
|
63
|
+
export function renderCouncilVoteMemberTask(originalTask: string): string {
|
|
64
|
+
return [originalTask, COUNCIL_VOTE_MEMBER_DIRECTIVE].join("\n\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
/**
|
|
30
68
|
* Posture a compete candidate runs under. Candidates otherwise differ only by
|
|
31
69
|
* worktree, and identical agents produce correlated output, so each candidate
|