@iowarp/clio-coder 0.3.4 → 0.3.7
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 +65 -2
- package/CONTRIBUTING.md +6 -6
- package/README.md +16 -5
- package/dist/{acp-S5R4RR5B.js → acp-SK4MD6MM.js} +11 -11
- package/dist/{agents-P6DMMVZY.js → agents-2FN2K6ME.js} +33 -26
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-2XCZLPKS.js → auth-QIYZWM5I.js} +15 -15
- package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
- package/dist/{chunk-VAWWTKDP.js → chunk-3HAPLH5M.js} +11 -11
- package/dist/{chunk-YCWGATWI.js → chunk-465YSENW.js} +3 -3
- package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
- package/dist/{chunk-UZHIZC5S.js → chunk-4DWFMQDR.js} +61 -76
- package/dist/{chunk-ZWMF7253.js → chunk-5C3AQNDW.js} +328 -9
- package/dist/{chunk-22NAGB7X.js → chunk-5C77SEEY.js} +5 -94
- package/dist/{chunk-WPQLXFOZ.js → chunk-5FR74PWO.js} +3 -2
- package/dist/{chunk-35MKKU5R.js → chunk-5UJ6ECTS.js} +18 -10
- package/dist/{chunk-BRXQQJFP.js → chunk-6M7VS3J3.js} +571 -50
- package/dist/{chunk-QQK64KLB.js → chunk-6TUKSZVF.js} +141 -23
- package/dist/{chunk-N4CZJQRK.js → chunk-AB4XIIVB.js} +8 -6
- package/dist/{chunk-KRPY7NTG.js → chunk-BMWK7ZIZ.js} +14 -20
- package/dist/{chunk-4BPJXDWC.js → chunk-C4JBQ5SR.js} +30 -14
- package/dist/{chunk-ZYKPLLNQ.js → chunk-CEYBNUGC.js} +821 -83
- package/dist/{chunk-VEZEGCGW.js → chunk-D4MDIG46.js} +20 -18
- package/dist/chunk-DJNLUABN.js +843 -0
- package/dist/{chunk-BP4OYD6A.js → chunk-DMD2AGVS.js} +21 -2
- package/dist/{chunk-KOHPCX4K.js → chunk-DOOEX22V.js} +2 -2
- package/dist/chunk-DQA7QLMD.js +123 -0
- package/dist/chunk-DR52UMZW.js +21 -0
- package/dist/{chunk-3HZ5RWN2.js → chunk-EBEFWSGL.js} +9 -7
- package/dist/{chunk-EDRHSCIE.js → chunk-EELBMBT6.js} +128 -13
- package/dist/{chunk-HV5X7OR2.js → chunk-EOOQZZDE.js} +16 -14
- package/dist/{chunk-WR67VIZY.js → chunk-FOT2FX5J.js} +63 -5
- package/dist/{chunk-BPGS2WCQ.js → chunk-GEYXPTRF.js} +2 -1
- package/dist/{chunk-FYYLNIL5.js → chunk-GH5622CP.js} +2 -2
- package/dist/{chunk-BEY543CS.js → chunk-GOXNB3AO.js} +5 -2
- package/dist/chunk-GWS3VEIW.js +195 -0
- package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
- package/dist/chunk-HWUFFB6L.js +83 -0
- package/dist/{chunk-X6COSD2O.js → chunk-J7PIKKWC.js} +8 -436
- package/dist/{chunk-NILBFAPG.js → chunk-JNXPYBB4.js} +2 -2
- package/dist/{chunk-4VP4KH3K.js → chunk-JRIO5UD2.js} +4 -4
- package/dist/{chunk-K6WL7QZT.js → chunk-JTSEDYVQ.js} +7 -7
- package/dist/{chunk-QKMUKYO7.js → chunk-KCMKRQX4.js} +236 -84
- package/dist/chunk-KZ2H5X4G.js +1026 -0
- package/dist/{chunk-A2GZF7DC.js → chunk-LADCF22A.js} +13 -13
- package/dist/chunk-LCGCVYZ4.js +57 -0
- package/dist/chunk-M4AKACEO.js +382 -0
- package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
- package/dist/{chunk-4JUF2NNX.js → chunk-MXI6J5JF.js} +7 -7
- package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
- package/dist/{chunk-TTNYS3EA.js → chunk-OB5HIGJY.js} +1 -1
- package/dist/{chunk-7RXG6QRZ.js → chunk-OBMAI2DP.js} +61 -840
- package/dist/{chunk-5M54SPOL.js → chunk-ODFEOB4F.js} +161 -5
- package/dist/chunk-PD3MESLB.js +242 -0
- package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
- package/dist/{chunk-VMNQ6OZA.js → chunk-QCTRSGHQ.js} +963 -786
- package/dist/chunk-RVG5JXAL.js +41 -0
- package/dist/{chunk-RD5U66HV.js → chunk-SROCI7ZU.js} +7 -7
- package/dist/{chunk-MFFY33HR.js → chunk-THKY7CD7.js} +466 -205
- package/dist/{chunk-34475P3I.js → chunk-TSHXZTOQ.js} +5 -4
- package/dist/{chunk-PCZJO5TI.js → chunk-UFQ3F4FW.js} +13 -178
- package/dist/{chunk-AD2SYQYC.js → chunk-UHXRNZ2J.js} +121 -3
- package/dist/chunk-UND3GU2L.js +103 -0
- package/dist/{chunk-QQL5RT5M.js → chunk-UUANF5CR.js} +2323 -2114
- package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
- package/dist/chunk-UVDSQ6LW.js +472 -0
- package/dist/{chunk-QWU7ZBO7.js → chunk-VQNODYQ4.js} +215 -56
- package/dist/chunk-VREKEFLL.js +37 -0
- package/dist/{chunk-2TZWSW76.js → chunk-WHGPSPT5.js} +2 -2
- package/dist/{chunk-TW3WDMVS.js → chunk-WHJYKASB.js} +2 -2
- package/dist/{chunk-MEQ45TQ4.js → chunk-WJHBC77E.js} +21 -7
- package/dist/{chunk-HXG4IURW.js → chunk-X2KV5FXT.js} +2 -2
- package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
- package/dist/{chunk-2LZI5CAG.js → chunk-XEGB6BCN.js} +228 -36
- package/dist/{chunk-E25LMLRW.js → chunk-YD734TPH.js} +2 -2
- package/dist/{verifiers-4UUM6TEE.js → chunk-YTYFXUI3.js} +121 -372
- package/dist/{chunk-3JLKSKD7.js → chunk-ZGH7FGS5.js} +17 -7
- package/dist/{chunk-VSNATDE6.js → chunk-ZZMN5OM4.js} +2 -2
- package/dist/cli/index.js +34 -32
- package/dist/{clio-J5JIOIDS.js → clio-WBVQEBKO.js} +7 -7
- package/dist/{code-nav-AXCXSBHX.js → code-nav-FGGFIE7L.js} +7 -7
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-KELWS457.js → components-F7OEATSO.js} +5 -5
- package/dist/{config-OEBMIN2U.js → config-TRBL3RCF.js} +48 -41
- package/dist/{configure-PUQOSIXQ.js → configure-OLCVPHNM.js} +17 -17
- package/dist/{context-URSXPBCK.js → context-MJIJ6GOX.js} +12 -12
- package/dist/{context-EKDCKUUZ.js → context-WFPKQSM6.js} +26 -9
- package/dist/{context-MGSE4Z2T.js → context-XEWE3MOJ.js} +44 -37
- package/dist/{context-clear-KDAJRNUK.js → context-clear-KNOS2JPB.js} +44 -37
- package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
- package/dist/{context-working-set-SBKMPPI2.js → context-working-set-EUXAZI6N.js} +14 -13
- package/dist/{dispatch-runner-MSWN72NK.js → dispatch-runner-B7MTOVKL.js} +321 -60
- package/dist/{docs-2C2LTVT2.js → docs-FLJTIDSE.js} +5 -5
- package/dist/{doctor-7BSE27PJ.js → doctor-RN4YKO2X.js} +15 -15
- package/dist/{eval-IZGDOO4H.js → eval-RUBJVSNQ.js} +52 -236
- package/dist/{evidence-SR7WXB5B.js → evidence-JZNBUOQZ.js} +39 -33
- package/dist/{evolve-K7VE2CBX.js → evolve-FJVC4KKI.js} +39 -33
- package/dist/{extensions-QVDOHDGJ.js → extensions-IQL36S7K.js} +5 -5
- package/dist/{fleet-7XMJNQNF.js → fleet-BDKYJFCP.js} +243 -370
- package/dist/fleet-commands-ZFIWZSB3.js +70 -0
- package/dist/fleet-graph-Y6HPXIVF.js +125 -0
- package/dist/fleet-new-RDVJLHHH.js +48 -0
- package/dist/{fleet-preflight-AQNAH644.js → fleet-preflight-BHSNPBMH.js} +2 -2
- package/dist/fleet-validate-BIYREGIK.js +79 -0
- package/dist/{init-JGNPAYXT.js → init-LQUB5COQ.js} +57 -48
- package/dist/library-NJAHIGG4.js +217 -0
- package/dist/memory-OG6HOYKM.js +472 -0
- package/dist/{models-ZMMLFJNN.js → models-5ZG5XY7J.js} +23 -22
- package/dist/{monitor-2F3T5KHP.js → monitor-TJ7AMTGB.js} +69 -35
- package/dist/{orchestrator-ORHT43JB.js → orchestrator-WZYB54DM.js} +4868 -1189
- package/dist/{paths-UXLN5YYZ.js → paths-XUC7GS6E.js} +5 -5
- package/dist/{reset-NXGTYNUO.js → reset-PXQT45IY.js} +8 -8
- package/dist/{run-RF4WJGMT.js → run-FQ74YF62.js} +82 -62
- package/dist/{share-UT3W6E4M.js → share-FW7SVCL3.js} +34 -10
- package/dist/{skills-PSACKC5Q.js → skills-7E7IRB3R.js} +25 -9
- package/dist/{skills-eval-WJSI55RZ.js → skills-eval-LI75W6OK.js} +43 -35
- package/dist/{targets-PIIRAOYS.js → targets-4CIFKCTW.js} +27 -24
- package/dist/{terminal-lease-ULWXWNVY.js → terminal-lease-WUZY7ZV5.js} +5 -4
- package/dist/{uninstall-FZCQCDKC.js → uninstall-7FV7IP4E.js} +5 -5
- package/dist/{upgrade-346TZ6AV.js → upgrade-K2HVIVMQ.js} +21 -20
- package/dist/{usage-6KKXR32N.js → usage-GTZELZQX.js} +159 -59
- package/dist/verifiers-RLAHT27O.js +336 -0
- package/dist/{verify-X5HDROLA.js → verify-BX3BRKH5.js} +7 -6
- package/dist/{wiki-generate-7STOCIFZ.js → wiki-generate-ASIFASCN.js} +58 -48
- package/dist/worker/entry.js +98 -84
- package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
- package/docs/README.md +4 -3
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/artifact-placement.md +1 -2
- package/docs/artifact-versions.md +10 -6
- package/docs/built-in-agents.md +26 -2
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +90 -8
- package/docs/configuration-and-targets.md +90 -2
- package/docs/context-engine.md +4 -2
- package/docs/context-working-set.md +4 -4
- package/docs/development-pipeline.md +1 -1
- package/docs/dispatch-architecture-rationale.md +1 -1
- package/docs/documentation-coverage.md +4 -4
- package/docs/documentation-guide.md +4 -4
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +4 -45
- package/docs/evidence-and-memory.md +70 -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 +224 -11
- package/docs/git-commit-provenance.md +2 -2
- package/docs/glossary.md +1 -1
- package/docs/installation-and-lifecycle.md +2 -2
- package/docs/middleware-and-components.md +20 -2
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +55 -8
- package/docs/proactive-memory.md +26 -16
- 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 +29 -7
- 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 +38 -4
- package/docs/worker-dispatch-mechanics.md +3 -3
- package/package.json +7 -4
- package/src/cli/agents.ts +2 -3
- package/src/cli/argv.ts +14 -1
- 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 +121 -0
- package/src/cli/fleet-validate.ts +30 -0
- package/src/cli/fleet.ts +188 -335
- package/src/cli/index.ts +4 -2
- package/src/cli/library.ts +190 -0
- package/src/cli/memory.ts +272 -10
- package/src/cli/modes/json-stream.ts +2 -2
- package/src/cli/modes/print.ts +12 -1
- package/src/cli/run.ts +22 -2
- package/src/cli/share.ts +13 -1
- package/src/cli/targets.ts +12 -3
- package/src/cli/usage.ts +160 -20
- package/src/core/bus-events.ts +7 -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/response-model-id.ts +134 -0
- package/src/core/toml.ts +62 -0
- package/src/core/workspace-files.ts +0 -1
- package/src/domains/agents/builtins/architect.md +2 -1
- package/src/domains/agents/builtins/oracle.md +33 -0
- package/src/domains/agents/catalog.ts +18 -5
- package/src/domains/agents/fleet-contract.ts +278 -16
- package/src/domains/agents/index.ts +14 -0
- package/src/domains/agents/recipe.ts +54 -14
- package/src/domains/agents/result-contract.ts +242 -5
- package/src/domains/config/classify.ts +4 -0
- package/src/domains/context/bootstrap.ts +36 -27
- package/src/domains/context/project-metadata.ts +19 -63
- package/src/domains/context/prompt-context.ts +8 -0
- package/src/domains/context/working-set/policies/index.ts +3 -4
- package/src/domains/dispatch/active-route-planner.ts +14 -0
- package/src/domains/dispatch/backoff.ts +2 -1
- package/src/domains/dispatch/budget-envelope.ts +396 -0
- 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 +36 -0
- package/src/domains/dispatch/delegation-plan.ts +167 -0
- package/src/domains/dispatch/execution-plan.ts +76 -5
- package/src/domains/dispatch/execution-role.ts +3 -1
- package/src/domains/dispatch/execution-scheduler.ts +183 -67
- package/src/domains/dispatch/extension.ts +339 -36
- 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 +737 -0
- package/src/domains/dispatch/gate-role-prompts.ts +9 -0
- package/src/domains/dispatch/host-verification.ts +178 -0
- package/src/domains/dispatch/index.ts +38 -0
- package/src/domains/dispatch/intent.ts +159 -0
- package/src/domains/dispatch/orphan-recovery.ts +1 -0
- package/src/domains/dispatch/receipt-integrity.ts +12 -4
- package/src/domains/dispatch/state.ts +37 -3
- package/src/domains/dispatch/types.ts +61 -9
- package/src/domains/dispatch/validation.ts +80 -6
- package/src/domains/dispatch/worker-spawn.ts +14 -3
- package/src/domains/eval/metrics/evidence.ts +0 -116
- package/src/domains/eval/metrics/invariants.ts +1 -1
- package/src/domains/eval/runners/clio-run.ts +1 -10
- package/src/domains/eval/runners/external-command.ts +2 -29
- package/src/domains/eval/schema/suite.ts +0 -7
- package/src/domains/eval/suites/run.ts +1 -7
- package/src/domains/evidence/trust-status.ts +10 -1
- package/src/domains/memory/index.ts +22 -0
- package/src/domains/memory/operations.ts +58 -1
- package/src/domains/memory/promotion.ts +281 -0
- package/src/domains/memory/prompt-section.ts +25 -5
- package/src/domains/memory/proposal.ts +51 -7
- package/src/domains/memory/task-bank.ts +3 -2
- package/src/domains/memory/task-memory-handoff.ts +181 -24
- package/src/domains/memory/task-memory-policy.ts +3 -1
- package/src/domains/memory/types.ts +37 -0
- package/src/domains/memory/validate.ts +178 -0
- package/src/domains/middleware/index.ts +15 -0
- package/src/domains/middleware/memory-intervention.ts +35 -25
- package/src/domains/middleware/runtime.ts +6 -0
- package/src/domains/middleware/skills-reminder.ts +19 -4
- package/src/domains/middleware/stalled-turn.ts +43 -1
- package/src/domains/middleware/types.ts +10 -0
- package/src/domains/middleware/watchdog.ts +281 -0
- package/src/domains/observability/contract.ts +9 -2
- package/src/domains/observability/cost.ts +31 -4
- 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/providers/index.ts +3 -0
- package/src/domains/providers/model-discovery.ts +9 -0
- package/src/domains/providers/runtime-resolution.ts +38 -1
- package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
- package/src/domains/providers/types/context-window-slots.ts +18 -0
- package/src/domains/providers/types/runtime-descriptor.ts +3 -1
- package/src/domains/resources/index.ts +20 -0
- package/src/domains/resources/library.ts +326 -0
- package/src/domains/resources/skills/marketplace.ts +37 -12
- package/src/domains/safety/call-target.ts +211 -14
- package/src/domains/safety/decision-presentation.ts +268 -0
- package/src/domains/safety/redaction.ts +73 -0
- package/src/domains/session/context-ledger.ts +10 -1
- package/src/domains/session/decision-board.ts +4 -0
- package/src/domains/session/entries.ts +3 -0
- package/src/domains/session/handoff.ts +629 -0
- package/src/domains/session/history.ts +68 -19
- package/src/domains/session/usage.ts +24 -7
- package/src/domains/share/archive.ts +67 -2
- package/src/engine/acp/event-mapper.ts +7 -0
- package/src/engine/acp/server.ts +29 -2
- package/src/engine/apis/lmstudio.ts +25 -4
- package/src/engine/apis/openai-completions.ts +147 -22
- package/src/engine/claude/sdk-runtime.ts +8 -2
- package/src/engine/claude/tool-safety.ts +13 -0
- package/src/engine/loop-guard.ts +27 -3
- package/src/engine/worker-events.ts +4 -3
- package/src/engine/worker-runtime.ts +59 -54
- package/src/entry/orchestrator.ts +55 -1
- package/src/interactive/bus-notices.ts +26 -0
- package/src/interactive/chat-loop-messages.ts +22 -0
- package/src/interactive/chat-loop.ts +248 -1
- package/src/interactive/chat-renderer.ts +41 -3
- package/src/interactive/clio-editor.ts +44 -7
- package/src/interactive/context-overlay.ts +43 -5
- package/src/interactive/cost-overlay.ts +70 -11
- 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 +471 -50
- package/src/interactive/fleet-run-preview.ts +307 -0
- package/src/interactive/footer/notifications.ts +219 -0
- package/src/interactive/footer/widgets.ts +13 -0
- package/src/interactive/handoff-round.ts +56 -0
- package/src/interactive/interactive-application.ts +49 -2
- package/src/interactive/interactive-event-projection.ts +9 -1
- package/src/interactive/interactive-input-runtime.ts +11 -1
- package/src/interactive/interactive-presentation.ts +11 -1
- package/src/interactive/interactive-slash-runtime.ts +52 -2
- package/src/interactive/interactive-subscriptions.ts +14 -2
- package/src/interactive/memory-overlay.ts +89 -4
- package/src/interactive/oracle.ts +179 -0
- package/src/interactive/overlay-ask-user-lifecycle.ts +7 -1
- package/src/interactive/overlay-frame.ts +5 -2
- package/src/interactive/overlay-general-openers.ts +230 -2
- package/src/interactive/overlay-key-routing.ts +58 -2
- package/src/interactive/overlay-lifecycle.ts +52 -5
- package/src/interactive/overlay-permission-lifecycle.ts +33 -8
- package/src/interactive/overlay-resource-openers.ts +11 -3
- package/src/interactive/overlay-session-lifecycle.ts +234 -2
- package/src/interactive/overlay-transitions.ts +11 -0
- package/src/interactive/overlays/ask-user.ts +74 -30
- package/src/interactive/overlays/decisions.ts +3 -1
- 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 +101 -4
- package/src/interactive/overlays/side-question.ts +139 -0
- package/src/interactive/overlays/skills-hub.ts +401 -15
- package/src/interactive/permission-hint.ts +35 -0
- package/src/interactive/permission-overlay.ts +95 -45
- package/src/interactive/renderers/tool-execution.ts +19 -49
- package/src/interactive/session-last-turn.ts +8 -1
- package/src/interactive/session-usage-reseed.ts +36 -10
- package/src/interactive/side-question.ts +171 -0
- package/src/interactive/slash-commands.ts +434 -7
- package/src/interactive/slash-spec.ts +19 -6
- package/src/interactive/status/summary.ts +5 -0
- package/src/interactive/status/types.ts +5 -0
- package/src/interactive/terminal-lease.ts +1 -0
- package/src/interactive/theme/tokens.ts +30 -0
- package/src/interactive/turn-context.ts +96 -23
- package/src/interactive/turn-middleware.ts +16 -1
- package/src/interactive/turn-runtime.ts +37 -8
- package/src/interactive/turn-state.ts +3 -0
- package/src/interactive/watchdog-run.ts +75 -0
- package/src/interactive/worker-progress.ts +440 -0
- package/src/interactive/worker-share.ts +56 -1
- package/src/interactive/worker-stream.ts +58 -110
- package/src/tools/agent-tools.ts +28 -3
- package/src/tools/ask-user.ts +21 -1
- package/src/tools/bootstrap.ts +3 -0
- package/src/tools/compete-worktrees.ts +13 -79
- package/src/tools/context/index.ts +2 -2
- package/src/tools/dispatch-admission.ts +242 -8
- package/src/tools/dispatch-arguments.ts +65 -1
- package/src/tools/dispatch-event-text.ts +19 -0
- package/src/tools/dispatch-plan.ts +136 -6
- package/src/tools/dispatch-runner.ts +319 -13
- package/src/tools/dispatch-types.ts +20 -1
- package/src/tools/dispatch.ts +96 -3
- package/src/tools/monitor.ts +31 -0
- package/src/tools/profiles.ts +18 -4
- package/src/tools/registry.ts +15 -5
- package/src/tools/result-disposition.ts +156 -0
- package/src/tools/result-shaping.ts +59 -1
- package/src/tools/task-worktree.ts +238 -0
- package/src/tools/verify/authoring.ts +116 -55
- package/src/tools/verify/scripts.ts +62 -0
- package/src/tools/worker-evidence.ts +21 -1
- package/src/worker/spec-contract.ts +44 -3
- package/dist/chunk-EFADSJET.js +0 -18
- package/dist/chunk-HC4CLZ2Y.js +0 -68
- package/dist/memory-4ALKDJ4Q.js +0 -246
- package/src/domains/eval/metrics/chaos-stream.ts +0 -93
|
@@ -20,6 +20,10 @@ import { wikiCompleteness, wikiStaleness } from "./wiki/staleness.js";
|
|
|
20
20
|
* context-init preload reporting, and `clio-coder config inspect`, so every surface
|
|
21
21
|
* measures the same text the session prompt would preload.
|
|
22
22
|
*/
|
|
23
|
+
/** The one line that stands in for a handbook this workspace does not have. */
|
|
24
|
+
export const HANDBOOK_ABSENT_FRAGMENT =
|
|
25
|
+
"<handbook>none: this workspace has no CLIO-CODER.md, so do not read one; learn the repository from its files, and the operator can run /context init to write a handbook</handbook>";
|
|
26
|
+
|
|
23
27
|
export function renderPromptContext(cwd: string): ProjectPromptContext {
|
|
24
28
|
const projectType = detectProjectType(cwd);
|
|
25
29
|
const pieces = [renderProjectTypeFragment(projectType)];
|
|
@@ -30,6 +34,10 @@ export function renderPromptContext(cwd: string): ProjectPromptContext {
|
|
|
30
34
|
for (const issue of loadedClioMd.errors) {
|
|
31
35
|
warnings.push(`clio: malformed ${issue.path} ignored: ${issue.error}`);
|
|
32
36
|
}
|
|
37
|
+
// Said where the handbook would have been: a model that sees no project
|
|
38
|
+
// context spends its first tool call reading CLIO-CODER.md and gets
|
|
39
|
+
// ENOENT, while the operator's header already says it is missing (#191).
|
|
40
|
+
if (loadedClioMd.files.length === 0 && loadedClioMd.errors.length === 0) pieces.push(HANDBOOK_ABSENT_FRAGMENT);
|
|
33
41
|
const codewiki = readCodewiki(cwd);
|
|
34
42
|
if (codewiki) {
|
|
35
43
|
const state = readClioState(cwd);
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* replay-lite runner resolve the same object from the same settings value and
|
|
4
4
|
* cannot drift into running different selections.
|
|
5
5
|
*
|
|
6
|
-
* `structural-v1` is the default;
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* resolvable as the exact pre-layer selection.
|
|
6
|
+
* `structural-v1` is the default; internal replay studies put it ahead of
|
|
7
|
+
* `age-horizon` on retention and ahead of the random control on precision.
|
|
8
|
+
* `age-horizon` stays resolvable as the exact pre-layer selection.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
import type { WorkingSetPolicy, WorkingSetPolicyId } from "../contract.js";
|
|
@@ -37,6 +37,12 @@ export function routeValidationProjection(
|
|
|
37
37
|
// Presentation-only parentage stamped by the calling tool, on the same
|
|
38
38
|
// terms: a model must not be able to author it.
|
|
39
39
|
parentToolCallId,
|
|
40
|
+
resolvedVerification,
|
|
41
|
+
resultContractOverride,
|
|
42
|
+
fleetGateReceipt,
|
|
43
|
+
taskWorktree,
|
|
44
|
+
runIdHint,
|
|
45
|
+
taskWorktreeDestination,
|
|
40
46
|
...raw
|
|
41
47
|
} = request;
|
|
42
48
|
const awaitingEnvelope =
|
|
@@ -54,6 +60,14 @@ export function routeValidationProjection(
|
|
|
54
60
|
...(assignmentDeadlineAt !== undefined ? { assignmentDeadlineAt } : {}),
|
|
55
61
|
...(ledger !== undefined ? { ledger } : {}),
|
|
56
62
|
...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
|
|
63
|
+
...(resolvedVerification !== undefined
|
|
64
|
+
? { resolvedVerification: resolvedVerification.map((check) => ({ ...check, argv: [...check.argv] })) }
|
|
65
|
+
: {}),
|
|
66
|
+
...(resultContractOverride !== undefined ? { resultContractOverride } : {}),
|
|
67
|
+
...(fleetGateReceipt !== undefined ? { fleetGateReceipt: { ...fleetGateReceipt } } : {}),
|
|
68
|
+
...(taskWorktree !== undefined ? { taskWorktree } : {}),
|
|
69
|
+
...(runIdHint !== undefined ? { runIdHint } : {}),
|
|
70
|
+
...(taskWorktreeDestination !== undefined ? { taskWorktreeDestination } : {}),
|
|
57
71
|
...(awaitingEnvelope ? { failover: "approved" as const } : {}),
|
|
58
72
|
}),
|
|
59
73
|
};
|
|
@@ -54,6 +54,7 @@ export function isDeterministicOutcomeCode(code: RunOutcomeCode | null | undefin
|
|
|
54
54
|
code === "worker_tool_call_cap_exhausted" ||
|
|
55
55
|
code === "loop_guard_tools_disabled_exhausted" ||
|
|
56
56
|
code === "result_contract_exhausted" ||
|
|
57
|
-
code === "worker_final_output_missing"
|
|
57
|
+
code === "worker_final_output_missing" ||
|
|
58
|
+
code === "host_verification_rejected"
|
|
58
59
|
);
|
|
59
60
|
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import type { WorkerBudget } from "../../worker/spec-contract.js";
|
|
2
|
+
import type { AgentBudget, AgentBudgetPhase } from "../agents/recipe.js";
|
|
3
|
+
|
|
4
|
+
/** Invocation-level phase requested by dispatch. */
|
|
5
|
+
export interface DispatchBudgetPhase {
|
|
6
|
+
toolCalls: number;
|
|
7
|
+
readReserve: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Typed invocation request. The ceiling preauthorizes growth on a later phase. */
|
|
11
|
+
export interface DispatchBudgetRequest extends DispatchBudgetPhase {
|
|
12
|
+
retryRevision?: DispatchBudgetPhase;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type BudgetEnvelopeReasonCode =
|
|
16
|
+
| "global-cap-clamp"
|
|
17
|
+
| "read-reserve-unavailable-clamp"
|
|
18
|
+
| "read-reserve-cap-clamp"
|
|
19
|
+
| "retry-growth-authorized"
|
|
20
|
+
| "retry-growth-denied"
|
|
21
|
+
| "revision-growth-authorized"
|
|
22
|
+
| "revision-growth-denied";
|
|
23
|
+
|
|
24
|
+
export interface BudgetEnvelopeReason {
|
|
25
|
+
code: BudgetEnvelopeReasonCode;
|
|
26
|
+
detail: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RunToolBudgetEnvelope {
|
|
30
|
+
version: 1;
|
|
31
|
+
policy: {
|
|
32
|
+
recipeId: string;
|
|
33
|
+
default: AgentBudgetPhase & { synthesis: boolean };
|
|
34
|
+
maximum: AgentBudgetPhase;
|
|
35
|
+
exact: boolean;
|
|
36
|
+
};
|
|
37
|
+
request: DispatchBudgetRequest | null;
|
|
38
|
+
effective: WorkerBudget;
|
|
39
|
+
reasons: ReadonlyArray<BudgetEnvelopeReason>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type BudgetAdmissionDenialCode =
|
|
43
|
+
| "exact-recipe-policy"
|
|
44
|
+
| "recipe-maximum"
|
|
45
|
+
| "global-cap"
|
|
46
|
+
| "ceiling-below-request";
|
|
47
|
+
|
|
48
|
+
export class BudgetAdmissionError extends Error {
|
|
49
|
+
readonly code: BudgetAdmissionDenialCode;
|
|
50
|
+
|
|
51
|
+
constructor(code: BudgetAdmissionDenialCode, message: string) {
|
|
52
|
+
super(`dispatch: budget admission denied (${code}): ${message}`);
|
|
53
|
+
this.name = "BudgetAdmissionError";
|
|
54
|
+
this.code = code;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
59
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function clonePhase(value: unknown, prefix: string): DispatchBudgetPhase {
|
|
63
|
+
if (!isRecord(value)) throw new Error(`${prefix} must be an object`);
|
|
64
|
+
for (const key of Object.keys(value)) {
|
|
65
|
+
if (key !== "toolCalls" && key !== "readReserve") throw new Error(`${prefix}.${key} is unknown`);
|
|
66
|
+
}
|
|
67
|
+
for (const key of ["toolCalls", "readReserve"] as const) {
|
|
68
|
+
if (!Object.hasOwn(value, key)) throw new Error(`${prefix}.${key} is required`);
|
|
69
|
+
}
|
|
70
|
+
const toolCalls = value.toolCalls;
|
|
71
|
+
if (typeof toolCalls !== "number" || !Number.isSafeInteger(toolCalls) || toolCalls <= 0) {
|
|
72
|
+
throw new Error(`${prefix}.toolCalls must be a positive safe integer`);
|
|
73
|
+
}
|
|
74
|
+
const readReserve = value.readReserve;
|
|
75
|
+
if (typeof readReserve !== "number" || !Number.isSafeInteger(readReserve) || readReserve < 0) {
|
|
76
|
+
throw new Error(`${prefix}.readReserve must be a non-negative safe integer`);
|
|
77
|
+
}
|
|
78
|
+
if (readReserve >= toolCalls) throw new Error(`${prefix}.readReserve must be less than ${prefix}.toolCalls`);
|
|
79
|
+
return { toolCalls, readReserve };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Strict validation and cloning for model, CLI, and persisted request inputs. */
|
|
83
|
+
export function cloneDispatchBudgetRequest(value: unknown, prefix = "budget"): DispatchBudgetRequest {
|
|
84
|
+
if (!isRecord(value)) throw new Error(`${prefix} must be an object`);
|
|
85
|
+
for (const key of Object.keys(value)) {
|
|
86
|
+
if (key !== "toolCalls" && key !== "readReserve" && key !== "retryRevision") {
|
|
87
|
+
throw new Error(`${prefix}.${key} is unknown`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const phase = clonePhase({ toolCalls: value.toolCalls, readReserve: value.readReserve }, prefix);
|
|
91
|
+
const retryRevision =
|
|
92
|
+
value.retryRevision === undefined ? undefined : clonePhase(value.retryRevision, `${prefix}.retryRevision`);
|
|
93
|
+
return { ...phase, ...(retryRevision === undefined ? {} : { retryRevision }) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function freezeEnvelope(envelope: RunToolBudgetEnvelope): RunToolBudgetEnvelope {
|
|
97
|
+
Object.freeze(envelope.policy.default);
|
|
98
|
+
Object.freeze(envelope.policy.maximum);
|
|
99
|
+
Object.freeze(envelope.policy);
|
|
100
|
+
if (envelope.request !== null) {
|
|
101
|
+
if (envelope.request.retryRevision !== undefined) Object.freeze(envelope.request.retryRevision);
|
|
102
|
+
Object.freeze(envelope.request);
|
|
103
|
+
}
|
|
104
|
+
Object.freeze(envelope.effective);
|
|
105
|
+
for (const reason of envelope.reasons) Object.freeze(reason);
|
|
106
|
+
Object.freeze(envelope.reasons);
|
|
107
|
+
return Object.freeze(envelope);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function phaseWithin(candidate: DispatchBudgetPhase, maximum: AgentBudgetPhase): boolean {
|
|
111
|
+
return candidate.toolCalls <= maximum.toolCalls && candidate.readReserve <= maximum.readReserve;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function samePhase(left: DispatchBudgetPhase, right: AgentBudgetPhase): boolean {
|
|
115
|
+
return left.toolCalls === right.toolCalls && left.readReserve === right.readReserve;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function assertRequestAdmitted(input: { request: DispatchBudgetRequest; policy: AgentBudget; hardCap: number }): void {
|
|
119
|
+
const { request, policy, hardCap } = input;
|
|
120
|
+
const authoredMaximum = policy.maximum ?? policy;
|
|
121
|
+
if (policy.maximum === undefined && !samePhase(request, policy)) {
|
|
122
|
+
throw new BudgetAdmissionError(
|
|
123
|
+
"exact-recipe-policy",
|
|
124
|
+
`the recipe pins ${policy.toolCalls} tool calls with a read reserve of ${policy.readReserve}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (!phaseWithin(request, authoredMaximum)) {
|
|
128
|
+
throw new BudgetAdmissionError(
|
|
129
|
+
"recipe-maximum",
|
|
130
|
+
`the requested phase ${request.toolCalls}/${request.readReserve} exceeds recipe maximum ${authoredMaximum.toolCalls}/${authoredMaximum.readReserve}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (request.toolCalls > hardCap) {
|
|
134
|
+
throw new BudgetAdmissionError(
|
|
135
|
+
"global-cap",
|
|
136
|
+
`the requested ${request.toolCalls} tool calls exceed the operator cap of ${hardCap}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const ceiling = request.retryRevision;
|
|
140
|
+
if (ceiling === undefined) return;
|
|
141
|
+
if (ceiling.toolCalls < request.toolCalls || ceiling.readReserve < request.readReserve) {
|
|
142
|
+
throw new BudgetAdmissionError(
|
|
143
|
+
"ceiling-below-request",
|
|
144
|
+
`retryRevision ${ceiling.toolCalls}/${ceiling.readReserve} must not be smaller than the requested phase ${request.toolCalls}/${request.readReserve}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (policy.maximum === undefined && !samePhase(ceiling, policy)) {
|
|
148
|
+
throw new BudgetAdmissionError(
|
|
149
|
+
"exact-recipe-policy",
|
|
150
|
+
`the recipe pins ${policy.toolCalls} tool calls with a read reserve of ${policy.readReserve}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (!phaseWithin(ceiling, authoredMaximum)) {
|
|
154
|
+
throw new BudgetAdmissionError(
|
|
155
|
+
"recipe-maximum",
|
|
156
|
+
`retryRevision ${ceiling.toolCalls}/${ceiling.readReserve} exceeds recipe maximum ${authoredMaximum.toolCalls}/${authoredMaximum.readReserve}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (ceiling.toolCalls > hardCap) {
|
|
160
|
+
throw new BudgetAdmissionError(
|
|
161
|
+
"global-cap",
|
|
162
|
+
`retryRevision ${ceiling.toolCalls} exceeds the operator cap of ${hardCap}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface ResolveToolBudgetEnvelopeInput {
|
|
168
|
+
recipeId: string;
|
|
169
|
+
policy: AgentBudget;
|
|
170
|
+
request?: DispatchBudgetRequest;
|
|
171
|
+
hardCap: number;
|
|
172
|
+
hasReadTool: boolean;
|
|
173
|
+
retry: boolean;
|
|
174
|
+
revision: boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Resolve the one immutable envelope used by admission, enforcement, and evidence. */
|
|
178
|
+
export function resolveToolBudgetEnvelope(input: ResolveToolBudgetEnvelopeInput): RunToolBudgetEnvelope {
|
|
179
|
+
if (!Number.isSafeInteger(input.hardCap) || input.hardCap <= 0) {
|
|
180
|
+
throw new Error("dispatch: worker tool-call cap must be a positive safe integer");
|
|
181
|
+
}
|
|
182
|
+
if (input.request !== undefined)
|
|
183
|
+
assertRequestAdmitted({ request: input.request, policy: input.policy, hardCap: input.hardCap });
|
|
184
|
+
|
|
185
|
+
const request = input.request === undefined ? null : cloneDispatchBudgetRequest(input.request);
|
|
186
|
+
const base = request ?? input.policy;
|
|
187
|
+
const reasons: BudgetEnvelopeReason[] = [];
|
|
188
|
+
let selected: DispatchBudgetPhase = base;
|
|
189
|
+
const phaseKind = input.retry ? "retry" : input.revision ? "revision" : null;
|
|
190
|
+
if (phaseKind !== null) {
|
|
191
|
+
const ceiling = request?.retryRevision;
|
|
192
|
+
if (ceiling !== undefined && !samePhase(ceiling, base)) {
|
|
193
|
+
selected = ceiling;
|
|
194
|
+
reasons.push({
|
|
195
|
+
code: phaseKind === "retry" ? "retry-growth-authorized" : "revision-growth-authorized",
|
|
196
|
+
detail: `${phaseKind} phase grew from ${base.toolCalls}/${base.readReserve} to the preauthorized ceiling ${ceiling.toolCalls}/${ceiling.readReserve}`,
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
reasons.push({
|
|
200
|
+
code: phaseKind === "retry" ? "retry-growth-denied" : "revision-growth-denied",
|
|
201
|
+
detail:
|
|
202
|
+
ceiling === undefined
|
|
203
|
+
? `${phaseKind} phase retained ${base.toolCalls}/${base.readReserve} because the original request declared no ceiling`
|
|
204
|
+
: `${phaseKind} phase retained ${base.toolCalls}/${base.readReserve} because the declared ceiling did not authorize growth`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const revisionCeiling = phaseKind === null ? request?.retryRevision : undefined;
|
|
209
|
+
if (revisionCeiling !== undefined && !samePhase(revisionCeiling, selected)) {
|
|
210
|
+
reasons.push({
|
|
211
|
+
code: "revision-growth-authorized",
|
|
212
|
+
detail: `a result-contract revision may grow from ${selected.toolCalls}/${selected.readReserve} to the preauthorized ceiling ${revisionCeiling.toolCalls}/${revisionCeiling.readReserve}`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let toolCalls = selected.toolCalls;
|
|
217
|
+
if (toolCalls > input.hardCap) {
|
|
218
|
+
reasons.push({
|
|
219
|
+
code: "global-cap-clamp",
|
|
220
|
+
detail: `tool calls were clamped from ${toolCalls} to the operator cap of ${input.hardCap}`,
|
|
221
|
+
});
|
|
222
|
+
toolCalls = input.hardCap;
|
|
223
|
+
}
|
|
224
|
+
let readReserve = selected.readReserve;
|
|
225
|
+
if (!input.hasReadTool && readReserve > 0) {
|
|
226
|
+
reasons.push({
|
|
227
|
+
code: "read-reserve-unavailable-clamp",
|
|
228
|
+
detail: `read reserve was clamped from ${readReserve} to 0 because the read tool is unavailable`,
|
|
229
|
+
});
|
|
230
|
+
readReserve = 0;
|
|
231
|
+
}
|
|
232
|
+
const maximumEffectiveReserve = Math.max(0, toolCalls - 1);
|
|
233
|
+
if (readReserve > maximumEffectiveReserve) {
|
|
234
|
+
reasons.push({
|
|
235
|
+
code: "read-reserve-cap-clamp",
|
|
236
|
+
detail: `read reserve was clamped from ${readReserve} to ${maximumEffectiveReserve} after the tool-call cap`,
|
|
237
|
+
});
|
|
238
|
+
readReserve = maximumEffectiveReserve;
|
|
239
|
+
}
|
|
240
|
+
let effectiveRevision: DispatchBudgetPhase | undefined;
|
|
241
|
+
if (revisionCeiling !== undefined && !samePhase(revisionCeiling, selected)) {
|
|
242
|
+
let revisionReadReserve = revisionCeiling.readReserve;
|
|
243
|
+
if (!input.hasReadTool && revisionReadReserve > 0) {
|
|
244
|
+
reasons.push({
|
|
245
|
+
code: "read-reserve-unavailable-clamp",
|
|
246
|
+
detail: `result-contract revision read reserve was clamped from ${revisionReadReserve} to 0 because the read tool is unavailable`,
|
|
247
|
+
});
|
|
248
|
+
revisionReadReserve = 0;
|
|
249
|
+
}
|
|
250
|
+
effectiveRevision = { toolCalls: revisionCeiling.toolCalls, readReserve: revisionReadReserve };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const authoredMaximum = input.policy.maximum ?? input.policy;
|
|
254
|
+
return freezeEnvelope({
|
|
255
|
+
version: 1,
|
|
256
|
+
policy: {
|
|
257
|
+
recipeId: input.recipeId,
|
|
258
|
+
default: {
|
|
259
|
+
toolCalls: input.policy.toolCalls,
|
|
260
|
+
readReserve: input.policy.readReserve,
|
|
261
|
+
synthesis: input.policy.synthesis,
|
|
262
|
+
},
|
|
263
|
+
maximum: { toolCalls: authoredMaximum.toolCalls, readReserve: authoredMaximum.readReserve },
|
|
264
|
+
exact: input.policy.maximum === undefined,
|
|
265
|
+
},
|
|
266
|
+
request,
|
|
267
|
+
effective: {
|
|
268
|
+
toolCalls,
|
|
269
|
+
readReserve,
|
|
270
|
+
synthesis: input.policy.synthesis,
|
|
271
|
+
hardCap: input.hardCap,
|
|
272
|
+
...(effectiveRevision === undefined ? {} : { revision: effectiveRevision }),
|
|
273
|
+
},
|
|
274
|
+
reasons,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function formatBudgetPolicy(envelope: RunToolBudgetEnvelope): string {
|
|
279
|
+
const policy = envelope.policy;
|
|
280
|
+
const range = policy.exact
|
|
281
|
+
? `exact ${policy.default.toolCalls}/${policy.default.readReserve}`
|
|
282
|
+
: `default ${policy.default.toolCalls}/${policy.default.readReserve}, max ${policy.maximum.toolCalls}/${policy.maximum.readReserve}`;
|
|
283
|
+
return `${policy.recipeId} ${range}, synthesis=${policy.default.synthesis ? "on" : "off"}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function formatBudgetRequest(envelope: RunToolBudgetEnvelope): string {
|
|
287
|
+
const request = envelope.request;
|
|
288
|
+
if (request === null) return "recipe default";
|
|
289
|
+
const ceiling = request.retryRevision;
|
|
290
|
+
return ceiling === undefined
|
|
291
|
+
? `${request.toolCalls}/${request.readReserve}, no retry/revision growth`
|
|
292
|
+
: `${request.toolCalls}/${request.readReserve}, retry/revision ceiling ${ceiling.toolCalls}/${ceiling.readReserve}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function formatEffectiveBudget(envelope: RunToolBudgetEnvelope): string {
|
|
296
|
+
const budget = envelope.effective;
|
|
297
|
+
const revision = budget.revision
|
|
298
|
+
? `, result revision ${budget.revision.toolCalls}/${budget.revision.readReserve}`
|
|
299
|
+
: "";
|
|
300
|
+
return `${budget.toolCalls}/${budget.readReserve}${revision}, lifetime cap ${budget.hardCap}, synthesis=${budget.synthesis ? "on" : "off"}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function formatBudgetReasons(envelope: RunToolBudgetEnvelope): string {
|
|
304
|
+
return envelope.reasons.length === 0 ? "none" : envelope.reasons.map((reason) => reason.code).join(",");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const BUDGET_REASON_CODES: ReadonlySet<string> = new Set<BudgetEnvelopeReasonCode>([
|
|
308
|
+
"global-cap-clamp",
|
|
309
|
+
"read-reserve-unavailable-clamp",
|
|
310
|
+
"read-reserve-cap-clamp",
|
|
311
|
+
"retry-growth-authorized",
|
|
312
|
+
"retry-growth-denied",
|
|
313
|
+
"revision-growth-authorized",
|
|
314
|
+
"revision-growth-denied",
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
/** Safely project an envelope that crossed a persistence or event boundary. */
|
|
318
|
+
export function cloneRunToolBudgetEnvelope(value: unknown): RunToolBudgetEnvelope | undefined {
|
|
319
|
+
try {
|
|
320
|
+
if (!isRecord(value) || value.version !== 1 || !isRecord(value.policy) || !isRecord(value.effective)) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
const recipeId = value.policy.recipeId;
|
|
324
|
+
if (typeof recipeId !== "string" || recipeId.length === 0 || typeof value.policy.exact !== "boolean") {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
if (!isRecord(value.policy.default) || typeof value.policy.default.synthesis !== "boolean") return undefined;
|
|
328
|
+
const defaultPhase = clonePhase(
|
|
329
|
+
{ toolCalls: value.policy.default.toolCalls, readReserve: value.policy.default.readReserve },
|
|
330
|
+
"budget.policy.default",
|
|
331
|
+
);
|
|
332
|
+
const maximum = clonePhase(value.policy.maximum, "budget.policy.maximum");
|
|
333
|
+
const request = value.request === null ? null : cloneDispatchBudgetRequest(value.request);
|
|
334
|
+
const effectivePhase = clonePhase(
|
|
335
|
+
{ toolCalls: value.effective.toolCalls, readReserve: value.effective.readReserve },
|
|
336
|
+
"budget.effective",
|
|
337
|
+
);
|
|
338
|
+
const revision =
|
|
339
|
+
value.effective.revision === undefined
|
|
340
|
+
? undefined
|
|
341
|
+
: clonePhase(value.effective.revision, "budget.effective.revision");
|
|
342
|
+
if (
|
|
343
|
+
typeof value.effective.synthesis !== "boolean" ||
|
|
344
|
+
typeof value.effective.hardCap !== "number" ||
|
|
345
|
+
!Number.isSafeInteger(value.effective.hardCap) ||
|
|
346
|
+
value.effective.hardCap <= 0 ||
|
|
347
|
+
!Array.isArray(value.reasons)
|
|
348
|
+
) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
if (
|
|
352
|
+
maximum.toolCalls < defaultPhase.toolCalls ||
|
|
353
|
+
maximum.readReserve < defaultPhase.readReserve ||
|
|
354
|
+
(value.policy.exact && !samePhase(maximum, defaultPhase)) ||
|
|
355
|
+
effectivePhase.toolCalls > value.effective.hardCap ||
|
|
356
|
+
value.effective.synthesis !== value.policy.default.synthesis ||
|
|
357
|
+
(revision !== undefined &&
|
|
358
|
+
(revision.toolCalls < effectivePhase.toolCalls ||
|
|
359
|
+
revision.toolCalls > value.effective.hardCap ||
|
|
360
|
+
(revision.toolCalls === effectivePhase.toolCalls && revision.readReserve <= effectivePhase.readReserve)))
|
|
361
|
+
) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
const reasons: BudgetEnvelopeReason[] = [];
|
|
365
|
+
for (const reason of value.reasons) {
|
|
366
|
+
if (
|
|
367
|
+
!isRecord(reason) ||
|
|
368
|
+
typeof reason.code !== "string" ||
|
|
369
|
+
!BUDGET_REASON_CODES.has(reason.code) ||
|
|
370
|
+
typeof reason.detail !== "string"
|
|
371
|
+
) {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
reasons.push({ code: reason.code as BudgetEnvelopeReasonCode, detail: reason.detail });
|
|
375
|
+
}
|
|
376
|
+
return freezeEnvelope({
|
|
377
|
+
version: 1,
|
|
378
|
+
policy: {
|
|
379
|
+
recipeId,
|
|
380
|
+
default: { ...defaultPhase, synthesis: value.policy.default.synthesis },
|
|
381
|
+
maximum,
|
|
382
|
+
exact: value.policy.exact,
|
|
383
|
+
},
|
|
384
|
+
request,
|
|
385
|
+
effective: {
|
|
386
|
+
...effectivePhase,
|
|
387
|
+
synthesis: value.effective.synthesis,
|
|
388
|
+
hardCap: value.effective.hardCap,
|
|
389
|
+
...(revision === undefined ? {} : { revision }),
|
|
390
|
+
},
|
|
391
|
+
reasons,
|
|
392
|
+
});
|
|
393
|
+
} catch {
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { BIRTH_TOKEN_SOURCE_AVAILABLE, processAlive, processBirthToken } from "../../core/process-identity.js";
|
|
5
|
+
import { withStateFileLockSync } from "../../core/state-file-lock.js";
|
|
6
|
+
import { clioStateDir } from "../../core/xdg.js";
|
|
7
|
+
import { atomicWrite } from "../../engine/session.js";
|
|
8
|
+
|
|
9
|
+
export interface CheckoutWriterLeaseRecord {
|
|
10
|
+
version: 1;
|
|
11
|
+
checkout: string;
|
|
12
|
+
pid: number;
|
|
13
|
+
processBirthToken: string;
|
|
14
|
+
acquiredAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CheckoutWriterLease {
|
|
18
|
+
checkout: string;
|
|
19
|
+
path: string;
|
|
20
|
+
release(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CheckoutWriterLeaseProbe {
|
|
24
|
+
birthToken(pid: number): string | null;
|
|
25
|
+
tokenProvesDeath?: boolean;
|
|
26
|
+
alive?(pid: number): boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const held = new Map<string, { count: number; path: string }>();
|
|
30
|
+
let exitHooksInstalled = false;
|
|
31
|
+
|
|
32
|
+
function canonicalCheckout(checkout: string): string {
|
|
33
|
+
return realpathSync(checkout);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function checkoutWriterLeasePath(checkout: string): string {
|
|
37
|
+
const canonical = canonicalCheckout(checkout);
|
|
38
|
+
const key = createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
39
|
+
return join(clioStateDir(), "checkout-writer-leases", `${key}.json`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validRecord(value: unknown): value is CheckoutWriterLeaseRecord {
|
|
43
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
44
|
+
const record = value as Partial<CheckoutWriterLeaseRecord>;
|
|
45
|
+
return (
|
|
46
|
+
record.version === 1 &&
|
|
47
|
+
typeof record.checkout === "string" &&
|
|
48
|
+
Number.isSafeInteger(record.pid) &&
|
|
49
|
+
(record.pid ?? 0) > 0 &&
|
|
50
|
+
typeof record.processBirthToken === "string" &&
|
|
51
|
+
typeof record.acquiredAt === "string" &&
|
|
52
|
+
Number.isFinite(Date.parse(record.acquiredAt))
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readRecord(path: string): CheckoutWriterLeaseRecord | null {
|
|
57
|
+
if (!existsSync(path)) return null;
|
|
58
|
+
try {
|
|
59
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
60
|
+
if (!validRecord(value)) throw new Error("invalid schema");
|
|
61
|
+
return value;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`checkout writer lease is unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ownerAlive(record: CheckoutWriterLeaseRecord, probe: CheckoutWriterLeaseProbe): boolean {
|
|
70
|
+
const current = probe.birthToken(record.pid);
|
|
71
|
+
if (current === null || current !== record.processBirthToken) return false;
|
|
72
|
+
if (probe.tokenProvesDeath === false) return probe.alive?.(record.pid) ?? false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const defaultProbe: CheckoutWriterLeaseProbe = {
|
|
77
|
+
birthToken: processBirthToken,
|
|
78
|
+
tokenProvesDeath: BIRTH_TOKEN_SOURCE_AVAILABLE,
|
|
79
|
+
alive: processAlive,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function releaseAllOnExit(): void {
|
|
83
|
+
for (const [checkout, entry] of held) {
|
|
84
|
+
try {
|
|
85
|
+
withStateFileLockSync(entry.path, () => {
|
|
86
|
+
const record = readRecord(entry.path);
|
|
87
|
+
if (record?.checkout === checkout && record.pid === process.pid) unlinkSync(entry.path);
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
// Process exit is best effort. A stale record is reclaimed by the next admission.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
held.clear();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function installExitHooks(): void {
|
|
97
|
+
if (exitHooksInstalled) return;
|
|
98
|
+
exitHooksInstalled = true;
|
|
99
|
+
process.once("exit", releaseAllOnExit);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function acquireCheckoutWriterLease(input: {
|
|
103
|
+
checkout: string;
|
|
104
|
+
nowMs?: number;
|
|
105
|
+
pid?: number;
|
|
106
|
+
processBirthToken?: string;
|
|
107
|
+
probe?: CheckoutWriterLeaseProbe;
|
|
108
|
+
}): CheckoutWriterLease {
|
|
109
|
+
const checkout = canonicalCheckout(input.checkout);
|
|
110
|
+
const path = checkoutWriterLeasePath(checkout);
|
|
111
|
+
const local = held.get(checkout);
|
|
112
|
+
if (local !== undefined && (input.pid === undefined || input.pid === process.pid)) {
|
|
113
|
+
local.count += 1;
|
|
114
|
+
let released = false;
|
|
115
|
+
return {
|
|
116
|
+
checkout,
|
|
117
|
+
path,
|
|
118
|
+
release: () => {
|
|
119
|
+
if (released) return;
|
|
120
|
+
released = true;
|
|
121
|
+
releaseCheckoutWriterLease(checkout);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
126
|
+
const ownerPid = input.pid ?? process.pid;
|
|
127
|
+
const token = input.processBirthToken ?? processBirthToken(ownerPid);
|
|
128
|
+
if (token === null) throw new Error("dispatch: cannot establish checkout writer process birth token");
|
|
129
|
+
withStateFileLockSync(path, () => {
|
|
130
|
+
const existing = readRecord(path);
|
|
131
|
+
if (existing !== null && ownerAlive(existing, input.probe ?? defaultProbe)) {
|
|
132
|
+
throw new Error(`checkout_writer_lease_held: checkout writer lease is held by pid ${existing.pid}`);
|
|
133
|
+
}
|
|
134
|
+
const record: CheckoutWriterLeaseRecord = {
|
|
135
|
+
version: 1,
|
|
136
|
+
checkout,
|
|
137
|
+
pid: ownerPid,
|
|
138
|
+
processBirthToken: token,
|
|
139
|
+
acquiredAt: new Date(input.nowMs ?? Date.now()).toISOString(),
|
|
140
|
+
};
|
|
141
|
+
atomicWrite(path, JSON.stringify(record, null, 2));
|
|
142
|
+
});
|
|
143
|
+
if (ownerPid === process.pid) {
|
|
144
|
+
held.set(checkout, { count: 1, path });
|
|
145
|
+
installExitHooks();
|
|
146
|
+
}
|
|
147
|
+
let released = false;
|
|
148
|
+
return {
|
|
149
|
+
checkout,
|
|
150
|
+
path,
|
|
151
|
+
release: () => {
|
|
152
|
+
if (released) return;
|
|
153
|
+
released = true;
|
|
154
|
+
if (ownerPid === process.pid) releaseCheckoutWriterLease(checkout);
|
|
155
|
+
else releaseLeaseRecord(path, checkout, ownerPid, token);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function releaseLeaseRecord(path: string, checkout: string, pid: number, token: string): void {
|
|
161
|
+
withStateFileLockSync(path, () => {
|
|
162
|
+
const record = readRecord(path);
|
|
163
|
+
if (record?.checkout === checkout && record.pid === pid && record.processBirthToken === token) unlinkSync(path);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function releaseCheckoutWriterLease(checkout: string): void {
|
|
168
|
+
const entry = held.get(checkout);
|
|
169
|
+
if (entry === undefined) return;
|
|
170
|
+
entry.count -= 1;
|
|
171
|
+
if (entry.count > 0) return;
|
|
172
|
+
held.delete(checkout);
|
|
173
|
+
const token = processBirthToken(process.pid);
|
|
174
|
+
if (token !== null) releaseLeaseRecord(entry.path, checkout, process.pid, token);
|
|
175
|
+
}
|