@iowarp/clio-coder 0.3.6 → 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 +59 -0
- package/README.md +22 -6
- package/dist/{acp-2BEHC4DL.js → acp-U67UHUK2.js} +14 -14
- package/dist/{agents-LNNFTM53.js → agents-YU6SGALZ.js} +42 -35
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-KXXFI2VS.js → auth-5ZPJOIVG.js} +32 -24
- package/dist/builtins-C6JMZVV6.js +17 -0
- package/dist/{chunk-E25LMLRW.js → chunk-26LEYJZH.js} +2 -2
- package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
- package/dist/{chunk-PBTHKCPN.js → chunk-2HFZQUHL.js} +7 -7
- package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
- package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
- package/dist/{chunk-CKXWIANG.js → chunk-4SPRNWDE.js} +18 -16
- package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
- package/dist/{chunk-XF5N4U5A.js → chunk-5DHKRSMQ.js} +9 -8
- package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
- package/dist/{chunk-EKY57CSP.js → chunk-5H3GB5BO.js} +68 -772
- package/dist/{chunk-6XXKFVSN.js → chunk-5Q2VVUKB.js} +4 -4
- package/dist/{chunk-DJVECN66.js → chunk-7RFXX52T.js} +295 -3705
- package/dist/{chunk-ZXF4XRKW.js → chunk-7RGZWPB6.js} +158 -8
- package/dist/{chunk-FYYLNIL5.js → chunk-A2NJGIB3.js} +2 -2
- package/dist/{chunk-XXQNGV4M.js → chunk-A3WNZD3P.js} +548 -72
- package/dist/{chunk-VEZEGCGW.js → chunk-B5CSFE7B.js} +23 -21
- package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
- package/dist/chunk-DR52UMZW.js +21 -0
- package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
- package/dist/{chunk-QKMUKYO7.js → chunk-E77JEWSD.js} +270 -125
- package/dist/{chunk-LYF7OHWH.js → chunk-EMYUUSFG.js} +12 -467
- package/dist/{chunk-4VP4KH3K.js → chunk-EQ63NRB7.js} +8 -8
- package/dist/chunk-FBVTI2TJ.js +518 -0
- package/dist/{chunk-R46L2BIR.js → chunk-FHJEP5SW.js} +19 -25
- package/dist/{chunk-WR67VIZY.js → chunk-GN57SG4G.js} +66 -8
- package/dist/{chunk-RD5U66HV.js → chunk-GPIEI3LY.js} +9 -9
- package/dist/{chunk-PCZJO5TI.js → chunk-GU2UIAFZ.js} +13 -178
- package/dist/chunk-GWS3VEIW.js +195 -0
- package/dist/chunk-H7IXIC72.js +103 -0
- package/dist/{chunk-3BPUFZDL.js → chunk-HLE42MG7.js} +3 -3
- package/dist/{chunk-24I7BN55.js → chunk-IFBNV6H6.js} +3 -3
- package/dist/{chunk-QNQHSOLF.js → chunk-IGWKHNIQ.js} +114 -55
- package/dist/chunk-IIZWH4XA.js +172 -0
- package/dist/{chunk-AD2SYQYC.js → chunk-IJ7RPIYJ.js} +124 -6
- package/dist/chunk-J3YUBZWY.js +382 -0
- package/dist/{chunk-5JGRAMKL.js → chunk-JOZYP4GM.js} +8 -6
- package/dist/{chunk-OH3TOQTB.js → chunk-K4XHGFR5.js} +751 -15
- package/dist/{chunk-EYPA3EGJ.js → chunk-KTYTFRMB.js} +178 -14
- package/dist/chunk-LU7P4LHA.js +33 -0
- package/dist/chunk-ME6CCNFO.js +108 -0
- package/dist/chunk-MXKJU4JB.js +1100 -0
- package/dist/{chunk-RY3LY4J5.js → chunk-N22QMJKY.js} +21 -18
- package/dist/chunk-NMPKI6XL.js +3006 -0
- package/dist/chunk-NUGM5KR6.js +165 -0
- package/dist/{chunk-22NAGB7X.js → chunk-P43ETTHK.js} +5 -94
- package/dist/{chunk-NILBFAPG.js → chunk-PMDBGQSJ.js} +2 -2
- package/dist/chunk-PT7HYKEM.js +165 -0
- package/dist/chunk-RVG5JXAL.js +41 -0
- package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
- package/dist/{chunk-4BPJXDWC.js → chunk-TANS5ZJS.js} +35 -19
- package/dist/{verifiers-NCBTHHN2.js → chunk-TB5666IT.js} +67 -324
- package/dist/{chunk-QM3F2GKX.js → chunk-TLQJPP24.js} +7927 -8088
- package/dist/{chunk-G7MUEIGA.js → chunk-TT36MB5S.js} +2 -1
- package/dist/chunk-TTHACPOM.js +961 -0
- package/dist/{chunk-6US73PDB.js → chunk-TYPGUK6W.js} +7 -7
- package/dist/{chunk-MFFY33HR.js → chunk-U6MBIEMB.js} +554 -209
- package/dist/chunk-VAWNZU7Z.js +242 -0
- package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
- package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
- package/dist/{chunk-XYDYPRZI.js → chunk-VWZOAB7K.js} +10 -10
- package/dist/{chunk-ZRGEBJ4T.js → chunk-WLFILSD5.js} +48 -48
- package/dist/{chunk-K7T3E2SR.js → chunk-WNIJTQQK.js} +12 -11
- package/dist/{chunk-CYQKWTG3.js → chunk-WSB3FPX7.js} +68 -20
- package/dist/{chunk-KHSFENX2.js → chunk-WWCZ5F23.js} +116 -14
- package/dist/{chunk-XE2VEJHX.js → chunk-WXY7KU3G.js} +2 -2
- package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
- package/dist/{chunk-KOHPCX4K.js → chunk-XWSF374K.js} +5 -5
- package/dist/{chunk-CJUB2JJ2.js → chunk-YS5VLNH5.js} +10 -10
- 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 +33 -31
- package/dist/{clio-M2KGYUFZ.js → clio-QVTYJ57A.js} +10 -10
- package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +5 -5
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-5TTYYX6G.js → components-ZFA3SAER.js} +9 -9
- package/dist/{config-XUUYQIWO.js → config-LW5IJFQN.js} +65 -55
- package/dist/{configure-IHJ7YOMV.js → configure-7XIZCOU4.js} +29 -24
- package/dist/{context-75MIWW3U.js → context-L3WL3X7K.js} +54 -43
- package/dist/{context-ZQ7SIFJV.js → context-N52ZA626.js} +30 -14
- package/dist/{context-74JLXAWD.js → context-Y6Y7QPR6.js} +12 -12
- package/dist/{context-clear-GYKWNUML.js → context-clear-MBQRLSDQ.js} +54 -43
- package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
- package/dist/{context-working-set-UX5KEP4J.js → context-working-set-GS6DSO7F.js} +18 -18
- package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-22ZCNOM3.js} +367 -79
- package/dist/{docs-6FZSCG5B.js → docs-7LQ23DLM.js} +9 -9
- package/dist/{doctor-SVJ5BZCW.js → doctor-M7YEDGAE.js} +27 -23
- package/dist/{eval-CG6LLBLD.js → eval-BEC2WHDA.js} +73 -20
- package/dist/{evidence-ZYFIEN42.js → evidence-REJUMSKM.js} +70 -59
- package/dist/{evolve-QGEXEMDW.js → evolve-PY5ZBA5K.js} +50 -39
- package/dist/{extensions-ADGNCJJD.js → extensions-HVKU65YU.js} +7 -7
- package/dist/{fleet-S5R4ZOQY.js → fleet-7WZEWRFA.js} +236 -377
- package/dist/fleet-commands-UVHWM76J.js +70 -0
- package/dist/fleet-graph-6ULH7PES.js +125 -0
- package/dist/fleet-new-RDVJLHHH.js +48 -0
- package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
- package/dist/fleet-validate-72PC4SLA.js +79 -0
- package/dist/{init-5DRU55YR.js → init-OG3TPGQG.js} +69 -57
- package/dist/library-CNTMPLRF.js +217 -0
- package/dist/{memory-7YKKR6UC.js → memory-6IS7F275.js} +52 -41
- package/dist/{models-ZPOLRU2C.js → models-ENRJDA5W.js} +37 -31
- package/dist/{monitor-US5F5YGZ.js → monitor-XLDVO7TN.js} +79 -42
- package/dist/{orchestrator-E2AL4T5N.js → orchestrator-6KSPYRHA.js} +4126 -762
- package/dist/{paths-E7KYAQWE.js → paths-DBXMZMDU.js} +6 -6
- package/dist/registry-LG64LTF4.js +11 -0
- package/dist/{reset-KZ652EK6.js → reset-RZ4ER727.js} +12 -12
- package/dist/{run-SRNBKDWD.js → run-Y2CNK5RU.js} +89 -76
- package/dist/{share-CGZE33UP.js → share-A55GYP6Z.js} +37 -13
- package/dist/{skills-S2X4DLY5.js → skills-ALC5J6AT.js} +29 -13
- package/dist/{skills-eval-W2GGIC4R.js → skills-eval-JPBEBYQU.js} +60 -48
- package/dist/support-MIETYA5E.js +38 -0
- package/dist/{targets-54SWINWB.js → targets-VGNXIR3S.js} +46 -36
- package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WOBR64YA.js} +4 -4
- package/dist/{uninstall-BVLWXKBT.js → uninstall-ZJF5H5ZN.js} +9 -9
- package/dist/{upgrade-JKAR27XC.js → upgrade-FUSUAGHR.js} +31 -28
- package/dist/{usage-MSAWCLX4.js → usage-N4MKVHKD.js} +140 -68
- package/dist/verifiers-YAWOJ3H2.js +336 -0
- package/dist/{verify-X5HDROLA.js → verify-LTDHYBGY.js} +10 -9
- package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-6M7GHTBJ.js} +75 -62
- package/dist/worker/entry.js +67 -57
- package/docs/README.md +3 -2
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/artifact-versions.md +11 -6
- package/docs/built-in-agents.md +26 -2
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +83 -3
- package/docs/configuration-and-targets.md +86 -4
- package/docs/context-engine.md +1 -1
- package/docs/development-pipeline.md +1 -1
- package/docs/dispatch-architecture-rationale.md +1 -1
- package/docs/documentation-coverage.md +3 -3
- package/docs/documentation-guide.md +3 -3
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +1 -1
- package/docs/evidence-and-memory.md +74 -10
- package/docs/evolution.md +1 -1
- package/docs/exit-codes-and-output.md +4 -1
- package/docs/extensions-and-sharing.md +6 -2
- package/docs/fleet-demo-runbook.md +2 -2
- package/docs/fleet-dispatch.md +228 -16
- package/docs/git-commit-provenance.md +2 -2
- package/docs/glossary.md +22 -2
- package/docs/installation-and-lifecycle.md +2 -2
- package/docs/middleware-and-components.md +2 -1
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +56 -9
- 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 +83 -65
- package/docs/resource-library.md +59 -0
- package/docs/safety-model.md +2 -2
- package/docs/scientific-validation.md +3 -3
- package/docs/session-lifecycle.md +37 -1
- package/docs/skills-marketplace.md +16 -3
- package/docs/tool-usage.md +14 -7
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/tui-design.md +1 -1
- package/docs/worker-dispatch-mechanics.md +3 -3
- 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-commands.ts +37 -0
- package/src/cli/fleet-graph.ts +102 -0
- package/src/cli/fleet-new.ts +36 -0
- package/src/cli/fleet-preflight.ts +111 -0
- package/src/cli/fleet-validate.ts +30 -0
- package/src/cli/fleet.ts +173 -335
- package/src/cli/index.ts +3 -1
- package/src/cli/library.ts +190 -0
- package/src/cli/share.ts +13 -1
- package/src/cli/shared.ts +1 -0
- package/src/cli/targets.ts +4 -1
- package/src/cli/usage.ts +111 -19
- package/src/cli/validate-model.ts +60 -5
- package/src/core/bus-events.ts +29 -0
- package/src/core/commit-attribution.ts +4 -4
- package/src/core/config.ts +130 -0
- package/src/core/defaults.ts +81 -0
- package/src/core/path-boundary.ts +100 -0
- package/src/domains/agents/builtins/architect.md +1 -0
- package/src/domains/agents/builtins/oracle.md +33 -0
- package/src/domains/agents/catalog.ts +13 -1
- package/src/domains/agents/extension.ts +2 -11
- package/src/domains/agents/fleet-contract.ts +304 -24
- package/src/domains/agents/index.ts +14 -0
- package/src/domains/agents/recipe.ts +7 -1
- package/src/domains/agents/registry.ts +73 -5
- package/src/domains/agents/result-contract.ts +360 -15
- package/src/domains/agents/write-boundary.ts +15 -50
- package/src/domains/config/classify.ts +4 -0
- package/src/domains/context/project-rules.ts +51 -1
- package/src/domains/dispatch/active-route-planner.ts +14 -0
- package/src/domains/dispatch/assignment-reconcile.ts +22 -5
- package/src/domains/dispatch/assignment-store.ts +151 -14
- package/src/domains/dispatch/backoff.ts +2 -1
- package/src/domains/dispatch/capability-match.ts +1 -0
- package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
- package/src/domains/dispatch/contract.ts +48 -0
- package/src/domains/dispatch/delegation-plan.ts +164 -0
- package/src/domains/dispatch/execution-plan.ts +76 -5
- package/src/domains/dispatch/execution-role.ts +12 -2
- package/src/domains/dispatch/execution-scheduler.ts +183 -67
- package/src/domains/dispatch/extension.ts +412 -74
- package/src/domains/dispatch/fleet-gate.ts +14 -0
- package/src/domains/dispatch/fleet-plan.ts +63 -3
- package/src/domains/dispatch/fleet-run.ts +791 -0
- package/src/domains/dispatch/gate-role-prompts.ts +47 -0
- package/src/domains/dispatch/host-verification.ts +178 -0
- package/src/domains/dispatch/index.ts +41 -1
- package/src/domains/dispatch/intent-requirements.ts +40 -0
- package/src/domains/dispatch/intent.ts +235 -0
- package/src/domains/dispatch/path-scope.ts +370 -0
- package/src/domains/dispatch/receipt-integrity.ts +9 -4
- package/src/domains/dispatch/state.ts +36 -3
- package/src/domains/dispatch/types.ts +64 -12
- package/src/domains/dispatch/validation.ts +69 -6
- 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 +155 -18
- 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/middleware/index.ts +15 -0
- package/src/domains/middleware/watchdog.ts +281 -0
- package/src/domains/observability/contract.ts +3 -1
- package/src/domains/observability/cost.ts +12 -1
- package/src/domains/observability/extension.ts +2 -2
- package/src/domains/observability/index.ts +10 -0
- package/src/domains/observability/out-of-turn-usage.ts +223 -0
- 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/index.ts +20 -0
- package/src/domains/resources/library.ts +326 -0
- package/src/domains/resources/prompts/loader.ts +119 -14
- package/src/domains/resources/skills/marketplace.ts +37 -12
- 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/domains/session/handoff.ts +629 -0
- package/src/domains/share/archive.ts +67 -2
- 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/entry/orchestrator.ts +37 -0
- package/src/interactive/bus-notices.ts +26 -0
- package/src/interactive/chat-loop.ts +235 -1
- package/src/interactive/chat-renderer.ts +22 -0
- package/src/interactive/cost-overlay.ts +31 -3
- package/src/interactive/council-dispatch.ts +30 -0
- package/src/interactive/council-grid.ts +213 -0
- package/src/interactive/council.ts +99 -0
- package/src/interactive/dispatch-board.ts +311 -17
- package/src/interactive/fleet-run-preview.ts +307 -0
- package/src/interactive/footer/notifications.ts +219 -0
- package/src/interactive/handoff-round.ts +56 -0
- package/src/interactive/interactive-application.ts +43 -1
- package/src/interactive/interactive-event-projection.ts +23 -1
- package/src/interactive/interactive-slash-runtime.ts +52 -2
- package/src/interactive/interactive-subscriptions.ts +14 -2
- package/src/interactive/oracle.ts +179 -0
- package/src/interactive/overlay-ask-user-lifecycle.ts +6 -0
- package/src/interactive/overlay-general-openers.ts +190 -1
- package/src/interactive/overlay-key-routing.ts +17 -1
- package/src/interactive/overlay-lifecycle.ts +41 -1
- package/src/interactive/overlay-permission-lifecycle.ts +10 -0
- package/src/interactive/overlay-resource-openers.ts +11 -3
- package/src/interactive/overlay-session-lifecycle.ts +234 -2
- package/src/interactive/overlays/fleet-run-approval.ts +208 -0
- package/src/interactive/overlays/handoff-review.ts +185 -0
- package/src/interactive/overlays/library-install-confirm.ts +151 -0
- package/src/interactive/overlays/list-overlay.ts +168 -2
- package/src/interactive/overlays/settings.ts +221 -30
- package/src/interactive/overlays/side-question.ts +139 -0
- package/src/interactive/overlays/skills-hub.ts +401 -15
- package/src/interactive/side-question.ts +171 -0
- package/src/interactive/slash-commands.ts +439 -7
- package/src/interactive/slash-spec.ts +19 -6
- package/src/interactive/theme/tokens.ts +30 -0
- package/src/interactive/turn-middleware.ts +15 -1
- package/src/interactive/view/artifacts.ts +42 -9
- package/src/interactive/view/view-overlay.ts +15 -3
- package/src/interactive/watchdog-run.ts +75 -0
- package/src/interactive/worker-receipts.ts +14 -2
- package/src/interactive/worker-share.ts +56 -1
- package/src/interactive/worker-stream.ts +15 -0
- package/src/tools/bootstrap.ts +3 -0
- package/src/tools/compete-worktrees.ts +13 -79
- package/src/tools/dispatch-admission.ts +251 -18
- package/src/tools/dispatch-arguments.ts +84 -1
- package/src/tools/dispatch-plan.ts +165 -6
- package/src/tools/dispatch-runner.ts +364 -23
- package/src/tools/dispatch-types.ts +20 -1
- package/src/tools/dispatch.ts +72 -2
- package/src/tools/monitor.ts +29 -0
- package/src/tools/profiles.ts +18 -4
- package/src/tools/task-worktree.ts +238 -0
- package/src/tools/verify/authoring.ts +61 -1
- package/src/tools/verify/scripts.ts +62 -0
- package/src/tools/worker-evidence.ts +21 -14
- package/src/worker/spec-contract.ts +3 -1
- package/dist/chunk-HC4CLZ2Y.js +0 -68
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { THINKING_LEVELS, type ThinkingLevel } from "../../core/defaults.js";
|
|
10
|
+
import { asDirectoryPathBoundary, resolvePathBoundary } from "../../core/path-boundary.js";
|
|
10
11
|
import { cloneValidatedResponseSchema } from "../../core/response-schema.js";
|
|
11
12
|
import { isToolProfileName, type ToolProfileName } from "../../tools/profiles.js";
|
|
12
13
|
import type { AgentProduct } from "../agents/spec.js";
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from "./execution-handoff.js";
|
|
20
21
|
import { isExecutionRole } from "./execution-role.js";
|
|
21
22
|
import { COMPETE_STANCES, type CompeteStance } from "./gate-role-prompts.js";
|
|
23
|
+
import { type DispatchIntent, isDispatchIntent } from "./intent.js";
|
|
22
24
|
import { isRoutingIntent, type RoutingIntent } from "./routing-intent.js";
|
|
23
25
|
import type {
|
|
24
26
|
DispatchRequestOrigin,
|
|
@@ -121,6 +123,10 @@ export interface JobSpec {
|
|
|
121
123
|
*/
|
|
122
124
|
denyTools?: ReadonlyArray<string>;
|
|
123
125
|
cwd?: string;
|
|
126
|
+
/** Run this writer in an isolated git worktree. */
|
|
127
|
+
worktree?: true;
|
|
128
|
+
/** How a successful isolated writer is applied to its parent checkout. */
|
|
129
|
+
apply?: "merge" | "preserve";
|
|
124
130
|
memorySection?: string;
|
|
125
131
|
noSkills?: boolean;
|
|
126
132
|
skillPaths?: ReadonlyArray<string>;
|
|
@@ -134,6 +140,8 @@ export interface JobSpec {
|
|
|
134
140
|
* per-tool calls (subprocess).
|
|
135
141
|
*/
|
|
136
142
|
writeRoots?: ReadonlyArray<string>;
|
|
143
|
+
/** Normalized model-declared path, output, and host verification intent. */
|
|
144
|
+
intent?: DispatchIntent;
|
|
137
145
|
requestOrigin?: DispatchRequestOrigin;
|
|
138
146
|
/**
|
|
139
147
|
* Threaded output from the previous pipeline step. Set only by the dispatch
|
|
@@ -159,6 +167,8 @@ export interface JobSpec {
|
|
|
159
167
|
autonomy?: AutonomyLevel;
|
|
160
168
|
/** Review/compete gate provenance sealed into the run's receipt. */
|
|
161
169
|
gate?: RunGateProvenance;
|
|
170
|
+
/** Council grouping projected to receipts and fleet surfaces. */
|
|
171
|
+
council?: { group: string; label: string; color?: string; round: number };
|
|
162
172
|
/** Plan-approval provenance sealed into the run's receipt. */
|
|
163
173
|
plan?: RunPlanProvenance;
|
|
164
174
|
/**
|
|
@@ -195,18 +205,22 @@ const KNOWN_KEYS = new Set([
|
|
|
195
205
|
"toolProfile",
|
|
196
206
|
"denyTools",
|
|
197
207
|
"cwd",
|
|
208
|
+
"worktree",
|
|
209
|
+
"apply",
|
|
198
210
|
"memorySection",
|
|
199
211
|
"noSkills",
|
|
200
212
|
"skillPaths",
|
|
201
213
|
"trustProjectCompatRoots",
|
|
202
214
|
"responseSchema",
|
|
203
215
|
"writeRoots",
|
|
216
|
+
"intent",
|
|
204
217
|
"requestOrigin",
|
|
205
218
|
"pipelineInput",
|
|
206
219
|
"predecessorHandoffs",
|
|
207
220
|
"lineage",
|
|
208
221
|
"autonomy",
|
|
209
222
|
"gate",
|
|
223
|
+
"council",
|
|
210
224
|
"plan",
|
|
211
225
|
"competeStance",
|
|
212
226
|
]);
|
|
@@ -399,7 +413,7 @@ export function validateJobSpec(spec: unknown): Validated {
|
|
|
399
413
|
|
|
400
414
|
if ("toolProfile" in spec && spec.toolProfile !== undefined) {
|
|
401
415
|
if (typeof spec.toolProfile !== "string" || !isToolProfileName(spec.toolProfile)) {
|
|
402
|
-
errors.push("toolProfile must be one of: minimal-local|science-local|full-agent");
|
|
416
|
+
errors.push("toolProfile must be one of: minimal-local|science-local|full-agent|council-read-only");
|
|
403
417
|
}
|
|
404
418
|
}
|
|
405
419
|
|
|
@@ -414,6 +428,13 @@ export function validateJobSpec(spec: unknown): Validated {
|
|
|
414
428
|
errors.push("cwd must be a non-empty string");
|
|
415
429
|
}
|
|
416
430
|
}
|
|
431
|
+
if ("worktree" in spec && spec.worktree !== undefined && spec.worktree !== true) {
|
|
432
|
+
errors.push("worktree must be true when present");
|
|
433
|
+
}
|
|
434
|
+
if ("apply" in spec && spec.apply !== undefined && spec.apply !== "merge" && spec.apply !== "preserve") {
|
|
435
|
+
errors.push("apply must be merge or preserve");
|
|
436
|
+
}
|
|
437
|
+
if (spec.apply !== undefined && spec.worktree !== true) errors.push("apply requires worktree: true");
|
|
417
438
|
|
|
418
439
|
if ("memorySection" in spec && spec.memorySection !== undefined) {
|
|
419
440
|
if (typeof spec.memorySection !== "string") {
|
|
@@ -457,6 +478,15 @@ export function validateJobSpec(spec: unknown): Validated {
|
|
|
457
478
|
errors.push("writeRoots must be a non-empty array of non-empty strings");
|
|
458
479
|
}
|
|
459
480
|
}
|
|
481
|
+
if ("intent" in spec && spec.intent !== undefined && !isDispatchIntent(spec.intent)) {
|
|
482
|
+
errors.push("intent must be a normalized dispatch intent");
|
|
483
|
+
}
|
|
484
|
+
if (isDispatchIntent(spec.intent) && spec.intent.writeRoots.length > 0 && Array.isArray(spec.writeRoots)) {
|
|
485
|
+
const jobCwd = typeof spec.cwd === "string" && spec.cwd.length > 0 ? spec.cwd : process.cwd();
|
|
486
|
+
const legacy = [...new Set(spec.writeRoots.map((root) => resolvePathBoundary(jobCwd, String(root))))].sort();
|
|
487
|
+
const declared = [...new Set(spec.intent.writeRoots.map((root) => resolvePathBoundary(jobCwd, root)))].sort();
|
|
488
|
+
if (JSON.stringify(legacy) !== JSON.stringify(declared)) errors.push("intent_write_roots_contradiction");
|
|
489
|
+
}
|
|
460
490
|
|
|
461
491
|
if ("requestOrigin" in spec && spec.requestOrigin !== undefined) {
|
|
462
492
|
if (typeof spec.requestOrigin !== "string" || !VALID_REQUEST_ORIGINS.has(spec.requestOrigin)) {
|
|
@@ -550,18 +580,27 @@ export function validateJobSpec(spec: unknown): Validated {
|
|
|
550
580
|
if (typeof spec.toolProfile === "string" && isToolProfileName(spec.toolProfile)) out.toolProfile = spec.toolProfile;
|
|
551
581
|
if (Array.isArray(spec.denyTools)) out.denyTools = spec.denyTools.map((tool) => String(tool));
|
|
552
582
|
if (typeof spec.cwd === "string") out.cwd = spec.cwd;
|
|
583
|
+
if (spec.worktree === true) out.worktree = true;
|
|
584
|
+
if (spec.apply === "merge" || spec.apply === "preserve") out.apply = spec.apply;
|
|
553
585
|
if (typeof spec.memorySection === "string") out.memorySection = spec.memorySection;
|
|
554
586
|
if (typeof spec.noSkills === "boolean") out.noSkills = spec.noSkills;
|
|
555
587
|
if (Array.isArray(spec.skillPaths)) out.skillPaths = spec.skillPaths.map((p) => String(p));
|
|
556
588
|
if (typeof spec.trustProjectCompatRoots === "boolean") out.trustProjectCompatRoots = spec.trustProjectCompatRoots;
|
|
557
589
|
if (responseSchema) out.responseSchema = responseSchema;
|
|
558
|
-
if (
|
|
590
|
+
if (isDispatchIntent(spec.intent)) out.intent = structuredClone(spec.intent);
|
|
591
|
+
const effectiveWriteRoots =
|
|
559
592
|
Array.isArray(spec.writeRoots) &&
|
|
560
593
|
spec.writeRoots.length > 0 &&
|
|
561
594
|
spec.writeRoots.every((root) => typeof root === "string" && root.length > 0)
|
|
562
|
-
|
|
595
|
+
? spec.writeRoots
|
|
596
|
+
: isDispatchIntent(spec.intent) && spec.intent.writeRoots.length > 0
|
|
597
|
+
? spec.intent.writeRoots
|
|
598
|
+
: undefined;
|
|
599
|
+
if (effectiveWriteRoots !== undefined) {
|
|
563
600
|
const jobCwd = typeof spec.cwd === "string" && spec.cwd.length > 0 ? spec.cwd : process.cwd();
|
|
564
|
-
out.writeRoots = spec.
|
|
601
|
+
out.writeRoots = isDispatchIntent(spec.intent)
|
|
602
|
+
? effectiveWriteRoots.map((root) => resolvePathBoundary(jobCwd, String(root)))
|
|
603
|
+
: effectiveWriteRoots.map((root) => asDirectoryPathBoundary(path.resolve(jobCwd, String(root))));
|
|
565
604
|
}
|
|
566
605
|
if (typeof spec.requestOrigin === "string" && VALID_REQUEST_ORIGINS.has(spec.requestOrigin)) {
|
|
567
606
|
out.requestOrigin = spec.requestOrigin as DispatchRequestOrigin;
|
|
@@ -572,6 +611,7 @@ export function validateJobSpec(spec: unknown): Validated {
|
|
|
572
611
|
if (isValidLineage(spec.lineage)) out.lineage = spec.lineage;
|
|
573
612
|
if (isAutonomyLevel(spec.autonomy)) out.autonomy = spec.autonomy;
|
|
574
613
|
if (isValidGate(spec.gate)) out.gate = cloneGate(spec.gate);
|
|
614
|
+
if (isValidCouncil(spec.council)) out.council = { ...spec.council };
|
|
575
615
|
if (typeof spec.competeStance === "string" && VALID_COMPETE_STANCES.has(spec.competeStance)) {
|
|
576
616
|
out.competeStance = spec.competeStance as CompeteStance;
|
|
577
617
|
}
|
|
@@ -654,9 +694,18 @@ function isValidProtectedArtifactRemap(value: unknown): value is ProtectedArtifa
|
|
|
654
694
|
);
|
|
655
695
|
}
|
|
656
696
|
|
|
657
|
-
const VALID_GATE_ROLES = new Set(["builder", "reviewer", "candidate", "judge"]);
|
|
697
|
+
const VALID_GATE_ROLES = new Set(["builder", "reviewer", "candidate", "judge", "member", "synthesis"]);
|
|
658
698
|
const VALID_GATE_VERDICTS = new Set(["pass", "fail", "revise"]);
|
|
659
|
-
const VALID_PLAN_TOPOLOGIES = new Set([
|
|
699
|
+
const VALID_PLAN_TOPOLOGIES = new Set([
|
|
700
|
+
"parallel",
|
|
701
|
+
"sequential",
|
|
702
|
+
"pipeline",
|
|
703
|
+
"review",
|
|
704
|
+
"compete",
|
|
705
|
+
"council",
|
|
706
|
+
"detached",
|
|
707
|
+
"fleet",
|
|
708
|
+
]);
|
|
660
709
|
const VALID_PLAN_APPROVALS = new Set(["operator", "full-auto"]);
|
|
661
710
|
|
|
662
711
|
function isValidGate(value: unknown): value is RunGateProvenance {
|
|
@@ -688,6 +737,20 @@ function cloneGate(gate: RunGateProvenance): RunGateProvenance {
|
|
|
688
737
|
};
|
|
689
738
|
}
|
|
690
739
|
|
|
740
|
+
function isValidCouncil(value: unknown): value is NonNullable<JobSpec["council"]> {
|
|
741
|
+
return (
|
|
742
|
+
isPlainObject(value) &&
|
|
743
|
+
typeof value.group === "string" &&
|
|
744
|
+
value.group.length > 0 &&
|
|
745
|
+
typeof value.label === "string" &&
|
|
746
|
+
value.label.length > 0 &&
|
|
747
|
+
(value.color === undefined || typeof value.color === "string") &&
|
|
748
|
+
typeof value.round === "number" &&
|
|
749
|
+
Number.isInteger(value.round) &&
|
|
750
|
+
value.round >= 1
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
691
754
|
function isValidPlan(value: unknown): value is RunPlanProvenance {
|
|
692
755
|
if (!isPlainObject(value)) return false;
|
|
693
756
|
if (typeof value.hash !== "string" || value.hash.length === 0) return false;
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* a path that changed cannot be pinned on one of them, and the plan compiler
|
|
9
9
|
* has already refused the case where more than one of them was allowed to
|
|
10
10
|
* write. Every step in a violating window fails, and the verdict says why.
|
|
11
|
+
*
|
|
12
|
+
* The window also carries what its runs recorded writing, which is what keeps
|
|
13
|
+
* the checkout diff from being read as authorship. A change nobody in the
|
|
14
|
+
* window recorded belongs to whoever else has the checkout open, and the
|
|
15
|
+
* verdict reports it without touching it.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { existsSync } from "node:fs";
|
|
@@ -16,9 +21,11 @@ import type { ExecutionPlan } from "./execution-plan.js";
|
|
|
16
21
|
import type { ExecutionWriteBoundaryOutcome } from "./execution-scheduler.js";
|
|
17
22
|
import {
|
|
18
23
|
assertWriteBoundaryInsideRoot,
|
|
24
|
+
assertWriteBoundaryVisibleToGit,
|
|
19
25
|
captureWorkspaceSnapshot,
|
|
20
26
|
enforceWriteBoundary,
|
|
21
27
|
type WorkspaceSnapshot,
|
|
28
|
+
type WriteBoundaryAttribution,
|
|
22
29
|
type WriteBoundaryVerdict,
|
|
23
30
|
writeWriteBoundaryVerdict,
|
|
24
31
|
} from "./write-boundary.js";
|
|
@@ -30,6 +37,15 @@ export interface WriteBoundaryEnforcerInput {
|
|
|
30
37
|
rootId: string;
|
|
31
38
|
/** Declared allowlist for a step, or undefined when it declares no boundary. */
|
|
32
39
|
boundaryFor(stepId: string): ReadonlyArray<string> | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* What the step's own run recorded writing, or null when the step has no
|
|
42
|
+
* closed write record: a code step running a registered command, a runtime
|
|
43
|
+
* that publishes no tool telemetry, a run whose telemetry had holes, or a run
|
|
44
|
+
* that shelled out or spawned a worker and could have written a path no
|
|
45
|
+
* argument names. Absent entirely means the caller offers no record at all,
|
|
46
|
+
* and every change outside the declaration is blamed on the window.
|
|
47
|
+
*/
|
|
48
|
+
recordedWritesFor?(stepId: string): ReadonlyArray<string> | null;
|
|
33
49
|
/** Called once per closed window with the sealed verdict and where it landed. */
|
|
34
50
|
onVerdict?(verdict: WriteBoundaryVerdict, path: string): void;
|
|
35
51
|
}
|
|
@@ -54,6 +70,9 @@ export function preflightWriteBoundaries(plan: ExecutionPlan, root: string): voi
|
|
|
54
70
|
);
|
|
55
71
|
}
|
|
56
72
|
for (const step of declared) assertWriteBoundaryInsideRoot(root, step.writes ?? []);
|
|
73
|
+
// One question over the whole plan: an entry is refused for being invisible
|
|
74
|
+
// to git whichever step declared it, and the diagnostic names the path.
|
|
75
|
+
assertWriteBoundaryVisibleToGit(root, [...new Set(declared.flatMap((step) => [...(step.writes ?? [])]))]);
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
export function createWriteBoundaryEnforcer(input: WriteBoundaryEnforcerInput): WriteBoundaryEnforcer {
|
|
@@ -63,10 +82,34 @@ export function createWriteBoundaryEnforcer(input: WriteBoundaryEnforcerInput):
|
|
|
63
82
|
for (const stepId of stepIds) for (const entry of input.boundaryFor(stepId) ?? []) allow.add(entry);
|
|
64
83
|
return [...allow].sort();
|
|
65
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* The window's write witness. One step without a record makes the whole
|
|
87
|
+
* window's record incomplete: the steps share a checkout, so a change no
|
|
88
|
+
* other step wrote could still be the unobserved one's.
|
|
89
|
+
*/
|
|
90
|
+
const attributionFor = (stepIds: ReadonlyArray<string>): WriteBoundaryAttribution | undefined => {
|
|
91
|
+
const recordedWritesFor = input.recordedWritesFor;
|
|
92
|
+
if (recordedWritesFor === undefined) return undefined;
|
|
93
|
+
const recorded = new Set<string>();
|
|
94
|
+
let complete = true;
|
|
95
|
+
for (const stepId of stepIds) {
|
|
96
|
+
const observed = recordedWritesFor(stepId);
|
|
97
|
+
if (observed === null) {
|
|
98
|
+
complete = false;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const target of observed) recorded.add(target);
|
|
102
|
+
}
|
|
103
|
+
return { recorded: [...recorded].sort(), complete };
|
|
104
|
+
};
|
|
66
105
|
return {
|
|
67
106
|
begin(window, stepIds) {
|
|
68
107
|
const allow = allowFor(stepIds);
|
|
69
108
|
assertWriteBoundaryInsideRoot(input.root, allow);
|
|
109
|
+
// Also checked whole-plan at preflight. Repeated here because a
|
|
110
|
+
// delegation plan splices steps into a running fleet, and their
|
|
111
|
+
// declarations reach this function without ever passing preflight.
|
|
112
|
+
assertWriteBoundaryVisibleToGit(input.root, allow);
|
|
70
113
|
open.set(window, { snapshot: captureWorkspaceSnapshot(input.root), allow });
|
|
71
114
|
},
|
|
72
115
|
async verify(window, stepIds) {
|
|
@@ -75,11 +118,13 @@ export function createWriteBoundaryEnforcer(input: WriteBoundaryEnforcerInput):
|
|
|
75
118
|
throw new Error(`write boundary: window '${window}' was verified without a snapshot`);
|
|
76
119
|
}
|
|
77
120
|
open.delete(window);
|
|
121
|
+
const attribution = attributionFor(stepIds);
|
|
78
122
|
const verdict = enforceWriteBoundary({
|
|
79
123
|
snapshot: pending.snapshot,
|
|
80
124
|
window,
|
|
81
125
|
stepIds,
|
|
82
126
|
allow: pending.allow,
|
|
127
|
+
...(attribution === undefined ? {} : { attribution }),
|
|
83
128
|
});
|
|
84
129
|
const path = writeWriteBoundaryVerdict(input.rootId, verdict);
|
|
85
130
|
input.onVerdict?.(verdict, path);
|
|
@@ -21,18 +21,27 @@
|
|
|
21
21
|
* Everything is pinned to the baseline commit recorded at snapshot time, so a
|
|
22
22
|
* commit step that moves HEAD without touching the working tree reads as "no
|
|
23
23
|
* change" instead of as a wholesale rewrite.
|
|
24
|
+
*
|
|
25
|
+
* A window's diff answers "what changed", never "who changed it". An operator
|
|
26
|
+
* editing the checkout while a fleet runs produces changes inside the window
|
|
27
|
+
* that no step made, and rolling those back to the baseline destroys work
|
|
28
|
+
* nobody asked Clio to touch. So a change is blamed on the window only when it
|
|
29
|
+
* intersects what the window's own runs recorded writing; everything else is
|
|
30
|
+
* reported as an unattributed concurrent change and left exactly as it is.
|
|
24
31
|
*/
|
|
25
32
|
|
|
26
33
|
import { execFileSync } from "node:child_process";
|
|
27
34
|
import { createHash } from "node:crypto";
|
|
28
35
|
import { existsSync, lstatSync, mkdirSync, readlinkSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
36
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
37
|
+
import { asDirectoryPathBoundary, pathBoundaryEntryCovers } from "../../core/path-boundary.js";
|
|
30
38
|
import { clioStateDir } from "../../core/xdg.js";
|
|
31
39
|
import { atomicWrite } from "../../engine/session.js";
|
|
32
40
|
import { isGitRepository } from "../../tools/compete-worktrees.js";
|
|
33
41
|
import {
|
|
34
42
|
describeWriteBoundary,
|
|
35
43
|
normalizeWriteBoundary,
|
|
44
|
+
normalizeWriteBoundaryEntry,
|
|
36
45
|
type WriteBoundary,
|
|
37
46
|
writeBoundaryCovers,
|
|
38
47
|
} from "../agents/write-boundary.js";
|
|
@@ -71,10 +80,15 @@ function gitBytes(root: string, args: ReadonlyArray<string>): Buffer {
|
|
|
71
80
|
*
|
|
72
81
|
* Enforcement sees exactly what git sees, so an ignored path is outside it: a
|
|
73
82
|
* repository decides what counts as its own content, and this is not the place
|
|
74
|
-
* to overrule that. The
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
83
|
+
* to overrule that. The consequence is that a boundary declared over an ignored
|
|
84
|
+
* path could never be verified, which is why `assertWriteBoundaryVisibleToGit`
|
|
85
|
+
* refuses that declaration before anything runs rather than letting this
|
|
86
|
+
* function certify an unobserved window as clean.
|
|
87
|
+
*
|
|
88
|
+
* The one thing subtracted is Clio's own state directory when an operator has
|
|
89
|
+
* placed it inside the workspace. Receipts, code-step logs, and boundary
|
|
90
|
+
* verdicts are the orchestrator writing its journal while the step runs, and
|
|
91
|
+
* blaming the step for them would make every window a violation.
|
|
78
92
|
*/
|
|
79
93
|
function dirtyPaths(root: string): string[] {
|
|
80
94
|
const raw = git(root, ["status", "--porcelain=v1", "-z", "-uall", "--no-renames"]);
|
|
@@ -222,14 +236,55 @@ export interface WriteBoundaryUnrecoverable {
|
|
|
222
236
|
}
|
|
223
237
|
|
|
224
238
|
/**
|
|
225
|
-
* `clean` means
|
|
226
|
-
*
|
|
227
|
-
* `rollback-incomplete`: the working tree is
|
|
228
|
-
* and handed to the operator with the list,
|
|
229
|
-
* content it never recorded destroys work.
|
|
239
|
+
* `clean` means no change outside the allowlist is attributable to this
|
|
240
|
+
* window's steps. `rolled-back` means one was and the repository now looks as
|
|
241
|
+
* it did before. Anything else is `rollback-incomplete`: the working tree is
|
|
242
|
+
* left exactly as the step made it and handed to the operator with the list,
|
|
243
|
+
* because a rollback that guesses at content it never recorded destroys work.
|
|
244
|
+
*
|
|
245
|
+
* A `clean` verdict can still carry `unattributed` paths. Those changed inside
|
|
246
|
+
* the window and no run in it recorded writing them, so they are the
|
|
247
|
+
* operator's, another process's, or a build artifact's, and nothing here may
|
|
248
|
+
* touch them.
|
|
230
249
|
*/
|
|
231
250
|
export type WriteBoundaryStatus = "clean" | "rolled-back" | "rollback-incomplete";
|
|
232
251
|
|
|
252
|
+
/**
|
|
253
|
+
* What the window's own runs recorded aiming a successful mutation at.
|
|
254
|
+
*
|
|
255
|
+
* This is the write-side witness the diff cannot be: the checkout says a path
|
|
256
|
+
* changed, and only the run's own tool-call record says whether the run is what
|
|
257
|
+
* changed it. The record comes from the same fold that grounds a sealed
|
|
258
|
+
* mutation report, so the boundary and the result contract judge one set of
|
|
259
|
+
* facts.
|
|
260
|
+
*
|
|
261
|
+
* A run can write through a channel no tool event enumerates, and then its path
|
|
262
|
+
* set is a lower bound rather than the whole list. That is not silently
|
|
263
|
+
* tolerated: the recorder knows which tools can do it (bash, verify, dispatch,
|
|
264
|
+
* steer, and any tool this process has no schema for) and reports the record
|
|
265
|
+
* open, which arrives here as `complete: false`. So the set is only ever read
|
|
266
|
+
* as a closed list for a run that could not have written outside it, and
|
|
267
|
+
* enforcement over a run that could stays exactly as strong as it was before
|
|
268
|
+
* attribution existed.
|
|
269
|
+
*/
|
|
270
|
+
export interface WriteBoundaryAttribution {
|
|
271
|
+
/**
|
|
272
|
+
* Paths the window's runs aimed a successful mutation at. Absolute, or
|
|
273
|
+
* relative to the workspace root; anything resolving outside the workspace is
|
|
274
|
+
* dropped, because git could not have reported it as a change either.
|
|
275
|
+
*/
|
|
276
|
+
recorded: ReadonlyArray<string>;
|
|
277
|
+
/**
|
|
278
|
+
* False when at least one step in the window has no closed write record: a
|
|
279
|
+
* code step running a registered command, a runtime that publishes no tool
|
|
280
|
+
* telemetry, a run whose telemetry came back with holes, or a run that
|
|
281
|
+
* called a tool able to write a path its arguments do not name. An open
|
|
282
|
+
* record cannot clear anybody, so the window falls back to blaming every
|
|
283
|
+
* change outside the allowlist, and the verdict says it did.
|
|
284
|
+
*/
|
|
285
|
+
complete: boolean;
|
|
286
|
+
}
|
|
287
|
+
|
|
233
288
|
export interface WriteBoundaryVerdict {
|
|
234
289
|
version: 1;
|
|
235
290
|
/** Which scheduling window this covers, e.g. `wave-2` or `revalidate-suite.check.1`. */
|
|
@@ -243,12 +298,25 @@ export interface WriteBoundaryVerdict {
|
|
|
243
298
|
checkedAt: string;
|
|
244
299
|
changedPaths: ReadonlyArray<string>;
|
|
245
300
|
violations: ReadonlyArray<string>;
|
|
301
|
+
/**
|
|
302
|
+
* Paths outside the allowlist that changed inside the window and that no run
|
|
303
|
+
* in it recorded writing. Recorded so the operator can see them, never rolled
|
|
304
|
+
* back: the window did not make them, so the baseline commit is not their
|
|
305
|
+
* prior content.
|
|
306
|
+
*/
|
|
307
|
+
unattributed: ReadonlyArray<string>;
|
|
308
|
+
/** Whether every step in the window offered an enumerable write record. */
|
|
309
|
+
attributionComplete: boolean;
|
|
246
310
|
rolledBack: ReadonlyArray<WriteBoundaryRollback>;
|
|
247
311
|
unrecoverable: ReadonlyArray<WriteBoundaryUnrecoverable>;
|
|
248
312
|
status: WriteBoundaryStatus;
|
|
249
313
|
/** Typed failure reason, or null when the window stayed inside its boundary. */
|
|
250
314
|
reason: typeof WRITE_BOUNDARY_VIOLATION_REASON | null;
|
|
251
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Operator-facing message naming the offending paths and the declaration, or
|
|
317
|
+
* naming the concurrent changes the window saw but did not cause. Null only
|
|
318
|
+
* when neither happened.
|
|
319
|
+
*/
|
|
252
320
|
detail: string | null;
|
|
253
321
|
digest: string;
|
|
254
322
|
}
|
|
@@ -258,6 +326,12 @@ export interface EnforceWriteBoundaryInput {
|
|
|
258
326
|
window: string;
|
|
259
327
|
stepIds: ReadonlyArray<string>;
|
|
260
328
|
allow: WriteBoundary;
|
|
329
|
+
/**
|
|
330
|
+
* What the window's runs recorded writing. Absent means no caller offered a
|
|
331
|
+
* record, which reads the same as an incomplete one: every change outside the
|
|
332
|
+
* allowlist is blamed on the window.
|
|
333
|
+
*/
|
|
334
|
+
attribution?: WriteBoundaryAttribution;
|
|
261
335
|
}
|
|
262
336
|
|
|
263
337
|
function canonicalVerdict(verdict: Omit<WriteBoundaryVerdict, "digest">): string {
|
|
@@ -317,6 +391,22 @@ function rollbackPath(
|
|
|
317
391
|
return { rolledBack: { path: change.path, action: "restored", restoredFrom: `${snapshot.head}:${change.path}` } };
|
|
318
392
|
}
|
|
319
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Whether one changed path falls inside what a run recorded writing.
|
|
396
|
+
*
|
|
397
|
+
* A recorded target is usually the exact file a write or edit named, but a
|
|
398
|
+
* command's delete target can be a directory, so a change under a recorded
|
|
399
|
+
* prefix counts as the same write. Matching is on segment boundaries: `work`
|
|
400
|
+
* does not cover `workspace/a.txt`.
|
|
401
|
+
*/
|
|
402
|
+
function withinRecordedWrite(recorded: ReadonlySet<string>, path: string): boolean {
|
|
403
|
+
if (recorded.has(path)) return true;
|
|
404
|
+
for (const target of recorded) {
|
|
405
|
+
if (path.startsWith(`${target}/`)) return true;
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
320
410
|
/**
|
|
321
411
|
* Compare, roll back, and return the verdict. Never throws for a violation: a
|
|
322
412
|
* violation is evidence the caller records and acts on, not an exception that
|
|
@@ -325,7 +415,21 @@ function rollbackPath(
|
|
|
325
415
|
export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBoundaryVerdict {
|
|
326
416
|
const allow = normalizeWriteBoundary(input.allow);
|
|
327
417
|
const changes = diffWorkspace(input.snapshot);
|
|
328
|
-
const
|
|
418
|
+
const outside = changes.filter((change) => !writeBoundaryCovers(allow, change.path));
|
|
419
|
+
const attributionComplete = input.attribution?.complete === true;
|
|
420
|
+
const recorded = new Set<string>();
|
|
421
|
+
for (const target of input.attribution?.recorded ?? []) {
|
|
422
|
+
const relativeTarget = repoRelative(input.snapshot.root, target);
|
|
423
|
+
if (relativeTarget !== null) recorded.add(relativeTarget);
|
|
424
|
+
}
|
|
425
|
+
// Without a complete record nothing can be cleared, so the window keeps the
|
|
426
|
+
// whole diff. With one, only what the runs actually wrote is theirs.
|
|
427
|
+
const violations = attributionComplete
|
|
428
|
+
? outside.filter((change) => withinRecordedWrite(recorded, change.path))
|
|
429
|
+
: outside;
|
|
430
|
+
const unattributed = attributionComplete
|
|
431
|
+
? outside.filter((change) => !withinRecordedWrite(recorded, change.path))
|
|
432
|
+
: [];
|
|
329
433
|
const rolledBack: WriteBoundaryRollback[] = [];
|
|
330
434
|
const unrecoverable: WriteBoundaryUnrecoverable[] = [];
|
|
331
435
|
for (const change of violations) {
|
|
@@ -335,15 +439,15 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
|
|
|
335
439
|
}
|
|
336
440
|
const status: WriteBoundaryStatus =
|
|
337
441
|
violations.length === 0 ? "clean" : unrecoverable.length === 0 ? "rolled-back" : "rollback-incomplete";
|
|
338
|
-
const
|
|
442
|
+
const who =
|
|
339
443
|
input.stepIds.length === 1
|
|
340
444
|
? `step '${input.stepIds[0]}'`
|
|
341
445
|
: `steps ${input.stepIds.map((id) => `'${id}'`).join(", ")} ran concurrently in one checkout, so the write cannot be attributed to one of them`;
|
|
342
|
-
const
|
|
446
|
+
const violationDetail =
|
|
343
447
|
violations.length === 0
|
|
344
|
-
?
|
|
448
|
+
? []
|
|
345
449
|
: [
|
|
346
|
-
`${
|
|
450
|
+
`${who} wrote outside its declared boundary.`,
|
|
347
451
|
`Unauthorized paths: ${violations.map((change) => change.path).join(", ")}.`,
|
|
348
452
|
`Declared writes: ${describeWriteBoundary(allow)}.`,
|
|
349
453
|
status === "rolled-back"
|
|
@@ -351,8 +455,21 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
|
|
|
351
455
|
: `Rollback is incomplete; the working tree is left as the step made it. Unrestorable: ${unrecoverable
|
|
352
456
|
.map((entry) => `${entry.path} (${entry.reason})`)
|
|
353
457
|
.join("; ")}.`,
|
|
458
|
+
attributionComplete
|
|
459
|
+
? "Each was blamed on this window because a run in it recorded writing that path."
|
|
460
|
+
: "This window had no complete write record, so every change outside the declaration was blamed on it.",
|
|
354
461
|
"If the change was legitimate, widen the step's `writes:` declaration to cover it.",
|
|
355
|
-
]
|
|
462
|
+
];
|
|
463
|
+
const concurrentDetail =
|
|
464
|
+
unattributed.length === 0
|
|
465
|
+
? []
|
|
466
|
+
: [
|
|
467
|
+
`Concurrent changes were seen in this window that no run in it recorded writing: ${unattributed
|
|
468
|
+
.map((change) => change.path)
|
|
469
|
+
.join(", ")}.`,
|
|
470
|
+
"They were left untouched, because a path this window did not write is not this window's to restore.",
|
|
471
|
+
];
|
|
472
|
+
const detail = [...violationDetail, ...concurrentDetail];
|
|
356
473
|
const body: Omit<WriteBoundaryVerdict, "digest"> = {
|
|
357
474
|
version: 1,
|
|
358
475
|
window: input.window,
|
|
@@ -363,11 +480,13 @@ export function enforceWriteBoundary(input: EnforceWriteBoundaryInput): WriteBou
|
|
|
363
480
|
checkedAt: new Date().toISOString(),
|
|
364
481
|
changedPaths: changes.map((change) => change.path),
|
|
365
482
|
violations: violations.map((change) => change.path),
|
|
483
|
+
unattributed: unattributed.map((change) => change.path),
|
|
484
|
+
attributionComplete,
|
|
366
485
|
rolledBack,
|
|
367
486
|
unrecoverable,
|
|
368
487
|
status,
|
|
369
488
|
reason: violations.length === 0 ? null : WRITE_BOUNDARY_VIOLATION_REASON,
|
|
370
|
-
detail,
|
|
489
|
+
detail: detail.length === 0 ? null : detail.join(" "),
|
|
371
490
|
};
|
|
372
491
|
return { ...body, digest: createHash("sha256").update(canonicalVerdict(body), "utf8").digest("hex") };
|
|
373
492
|
}
|
|
@@ -387,16 +506,76 @@ export function assertWriteBoundaryInsideRoot(root: string, allow: WriteBoundary
|
|
|
387
506
|
// path a step is about to create.
|
|
388
507
|
while (probe !== realRoot && !existsSync(probe)) probe = dirname(probe);
|
|
389
508
|
const resolved = realpathSync(probe);
|
|
390
|
-
|
|
509
|
+
// The root is inside itself. The prefix walk above stops at the root when
|
|
510
|
+
// a declared directory does not exist yet, and `pathBoundaryEntryCovers`
|
|
511
|
+
// answers a different question: it asks whether a changed path falls under
|
|
512
|
+
// a declaration, where the directory entry itself is not a change git ever
|
|
513
|
+
// reports. Asking it about the root would refuse every boundary naming a
|
|
514
|
+
// path the step is about to create.
|
|
515
|
+
if (resolved !== realRoot && !pathBoundaryEntryCovers(asDirectoryPathBoundary(realRoot), resolved)) {
|
|
391
516
|
throw new Error(`write boundary: entry '${entry}' resolves to ${resolved}, outside the workspace at ${realRoot}`);
|
|
392
517
|
}
|
|
393
518
|
}
|
|
394
519
|
}
|
|
395
520
|
|
|
396
|
-
/**
|
|
521
|
+
/**
|
|
522
|
+
* Refuse a declaration git will never report on.
|
|
523
|
+
*
|
|
524
|
+
* `git status` does not list an ignored path, so a boundary entry the
|
|
525
|
+
* repository ignores names a region enforcement is blind to: every window over
|
|
526
|
+
* it is certified clean without a single observation, which is a stronger claim
|
|
527
|
+
* than "nothing was checked" and reads as a passing one. That is the same
|
|
528
|
+
* defect `assertWriteBoundaryInsideRoot` refuses for an entry that leaves the
|
|
529
|
+
* workspace through a symlink, so it is refused in the same place and at the
|
|
530
|
+
* same time, before anything runs.
|
|
531
|
+
*
|
|
532
|
+
* A tracked path is reportable whatever the ignore rules say, and git's own
|
|
533
|
+
* index-aware check is what decides that, so this never refuses a path the
|
|
534
|
+
* repository is actually following.
|
|
535
|
+
*/
|
|
536
|
+
export function assertWriteBoundaryVisibleToGit(root: string, allow: WriteBoundary): void {
|
|
537
|
+
// Entry by entry rather than through `normalizeWriteBoundary`, whose 32-entry
|
|
538
|
+
// cap is a per-step declaration limit. A caller may legitimately ask this
|
|
539
|
+
// question about every entry a whole plan declared.
|
|
540
|
+
const entries = [...new Set(allow.map(normalizeWriteBoundaryEntry))].sort();
|
|
541
|
+
if (entries.length === 0) return;
|
|
542
|
+
const absolute = resolve(root);
|
|
543
|
+
let raw: string;
|
|
544
|
+
try {
|
|
545
|
+
raw = git(absolute, ["check-ignore", "-v", "-z", "--stdin"], `${entries.join("\0")}\0`);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
// `check-ignore` exits 1 when nothing matched, which is the common answer
|
|
548
|
+
// and not a failure. Any other exit is a real inability to decide, and
|
|
549
|
+
// enforcement fails closed rather than assuming visibility.
|
|
550
|
+
const status = (error as { status?: number }).status;
|
|
551
|
+
if (status === 1) return;
|
|
552
|
+
throw new Error(
|
|
553
|
+
`write boundary: could not determine whether ${describeWriteBoundary(entries)} is ignored by git in ${absolute}: ${(error as Error).message}`,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
const fields = raw.split("\0");
|
|
557
|
+
const refusals: string[] = [];
|
|
558
|
+
// `-v -z` emits source, line number, pattern, and pathname per match.
|
|
559
|
+
for (let index = 0; index + 3 < fields.length; index += 4) {
|
|
560
|
+
const [source, line, pattern, path] = fields.slice(index, index + 4);
|
|
561
|
+
if (path === undefined || path.length === 0) continue;
|
|
562
|
+
refusals.push(`'${path}' is ignored by ${source}:${line}:${pattern}`);
|
|
563
|
+
}
|
|
564
|
+
if (refusals.length === 0) return;
|
|
565
|
+
throw new Error(
|
|
566
|
+
`write boundary: ${refusals.join(", ")}. git status never reports an ignored path, so a boundary declared over one is verified against nothing and every window would be certified clean without an observation. Declare a path the repository tracks, or stop ignoring this one.`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Workspace-relative form of a path, or null when it resolves outside. A
|
|
572
|
+
* relative input is taken as already workspace-relative, which is how a
|
|
573
|
+
* recorded write target and an absolute state directory both normalize here.
|
|
574
|
+
*/
|
|
397
575
|
function repoRelative(root: string, path: string): string | null {
|
|
398
|
-
const
|
|
399
|
-
|
|
576
|
+
const base = resolve(root);
|
|
577
|
+
const rel = relative(base, resolve(base, path));
|
|
578
|
+
if (rel.length === 0 || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;
|
|
400
579
|
return rel.split(sep).join("/");
|
|
401
580
|
}
|
|
402
581
|
|