@iowarp/clio-coder 0.3.7 → 0.3.9
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 +80 -0
- package/README.md +13 -4
- package/dist/{acp-SK4MD6MM.js → acp-7LOELQFP.js} +13 -13
- package/dist/{agents-2FN2K6ME.js → agents-FIBG2SHA.js} +41 -37
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-QIYZWM5I.js → auth-OI4LIH2I.js} +31 -24
- package/dist/builtins-AD25UL3C.js +17 -0
- package/dist/{chunk-5FR74PWO.js → chunk-2JDWVJND.js} +2 -2
- package/dist/chunk-3DPEIQKN.js +113 -0
- package/dist/{chunk-EOOQZZDE.js → chunk-3DUR4WUA.js} +19 -19
- package/dist/{chunk-WHJYKASB.js → chunk-3MRC2YSQ.js} +2 -2
- package/dist/{chunk-EBEFWSGL.js → chunk-3UUY7R3Z.js} +14 -10
- package/dist/{chunk-LADCF22A.js → chunk-3V5AYSEQ.js} +113 -54
- package/dist/{chunk-BMWK7ZIZ.js → chunk-465CC7FK.js} +16 -13
- package/dist/{chunk-5WIGXA4T.js → chunk-47CMYGET.js} +111 -4
- package/dist/{chunk-CEYBNUGC.js → chunk-4H6ULJ3H.js} +378 -36
- package/dist/{chunk-YTYFXUI3.js → chunk-4LJX2PUC.js} +9 -9
- package/dist/{chunk-DOOEX22V.js → chunk-56KB5IJP.js} +5 -5
- package/dist/{chunk-SPULKLCF.js → chunk-5DQRIYDZ.js} +2 -2
- package/dist/{chunk-TSHXZTOQ.js → chunk-5HFBWUMU.js} +23 -11
- package/dist/{chunk-5UJ6ECTS.js → chunk-5PVQ4SRS.js} +80 -8
- package/dist/{chunk-ZWLZP4ZT.js → chunk-5QKCQQ3E.js} +359 -17
- package/dist/{chunk-6M7VS3J3.js → chunk-5T7RBWN2.js} +111 -5
- package/dist/chunk-774ILSRL.js +172 -0
- package/dist/chunk-7C6RYZGQ.js +391 -0
- package/dist/{chunk-GH5622CP.js → chunk-A2NJGIB3.js} +2 -2
- package/dist/{chunk-C4JBQ5SR.js → chunk-AD7Y7STJ.js} +6 -6
- package/dist/{chunk-GEYXPTRF.js → chunk-AEYBF3TB.js} +33 -12
- package/dist/{chunk-2SFS6XQE.js → chunk-AMKHQW3C.js} +3 -2
- package/dist/{chunk-D4MDIG46.js → chunk-B5CSFE7B.js} +7 -7
- package/dist/{chunk-MXI6J5JF.js → chunk-B5XRQOLB.js} +10 -10
- package/dist/{chunk-X2KV5FXT.js → chunk-BVDVID7E.js} +2 -2
- package/dist/{chunk-JNXPYBB4.js → chunk-CA42X6KT.js} +3 -3
- package/dist/{chunk-VREKEFLL.js → chunk-D73KXYPF.js} +3 -3
- package/dist/{chunk-JTSEDYVQ.js → chunk-DG4M6ZUE.js} +7 -7
- package/dist/{chunk-DQA7QLMD.js → chunk-EBOC7MT3.js} +10 -25
- package/dist/{chunk-KZ2H5X4G.js → chunk-ECUO3KDP.js} +129 -14
- package/dist/{chunk-JRIO5UD2.js → chunk-EQ63NRB7.js} +5 -5
- package/dist/{chunk-YD734TPH.js → chunk-FALJGAWU.js} +2 -2
- package/dist/{chunk-GWS3VEIW.js → chunk-FWDFM5ZU.js} +24 -3
- package/dist/{chunk-XEGB6BCN.js → chunk-GAYUJ7LE.js} +68 -14
- package/dist/{chunk-UND3GU2L.js → chunk-H7IXIC72.js} +2 -2
- package/dist/{chunk-IR4CFBFN.js → chunk-HAY4ZE2P.js} +12 -12
- package/dist/{chunk-UVDSQ6LW.js → chunk-HCBCAYZU.js} +74 -147
- package/dist/{chunk-4DWFMQDR.js → chunk-HJB5IUKP.js} +89 -145
- package/dist/{chunk-M4AKACEO.js → chunk-HKO36JWF.js} +33 -5
- package/dist/{chunk-KCMKRQX4.js → chunk-HPCTNZM2.js} +45 -82
- package/dist/{chunk-465YSENW.js → chunk-IFBNV6H6.js} +3 -3
- package/dist/{chunk-FJ3H4MN5.js → chunk-IHKBWSXF.js} +2 -2
- package/dist/chunk-JEQQR47K.js +3025 -0
- package/dist/{chunk-FO5ZOVUY.js → chunk-KV2AOLDF.js} +27 -7
- package/dist/chunk-LU7P4LHA.js +33 -0
- package/dist/{chunk-6TUKSZVF.js → chunk-LXPJXFM5.js} +11 -11
- package/dist/{chunk-VQNODYQ4.js → chunk-MIX5N5AC.js} +488 -3668
- package/dist/chunk-MLOK6ZOS.js +2888 -0
- package/dist/{chunk-ZZMN5OM4.js → chunk-MV2VUEJC.js} +2 -2
- package/dist/{chunk-MVVUPGPW.js → chunk-MXHC5QYU.js} +6 -6
- package/dist/{chunk-OBMAI2DP.js → chunk-N3PBVRTZ.js} +12 -388
- package/dist/{chunk-WJHBC77E.js → chunk-N5XKWMDW.js} +17 -7
- package/dist/{chunk-5C3AQNDW.js → chunk-NNNWO6F2.js} +124 -36
- package/dist/{chunk-UFQ3F4FW.js → chunk-NQ6UCCOD.js} +4 -4
- package/dist/chunk-NUGM5KR6.js +165 -0
- package/dist/{chunk-DMD2AGVS.js → chunk-NZU6YDNV.js} +20 -18
- package/dist/{chunk-WHGPSPT5.js → chunk-O6I4CIEU.js} +151 -13
- package/dist/{chunk-XN3L4EYL.js → chunk-OEDBCISO.js} +2 -2
- package/dist/{chunk-PD3MESLB.js → chunk-P3JGPQFL.js} +4 -4
- package/dist/{chunk-UHXRNZ2J.js → chunk-PNY46YEY.js} +23 -6
- package/dist/{chunk-THKY7CD7.js → chunk-PZ4I4JE2.js} +134 -29
- package/dist/{chunk-SROCI7ZU.js → chunk-QQ7EKM72.js} +5 -5
- package/dist/{chunk-QCTRSGHQ.js → chunk-R7LNVMCS.js} +91 -53
- package/dist/{chunk-GOXNB3AO.js → chunk-RAPCMZL4.js} +75 -4
- package/dist/chunk-RKKLTLYB.js +45 -0
- package/dist/{chunk-OB5HIGJY.js → chunk-RKRLDWD3.js} +4 -1
- package/dist/{chunk-DJNLUABN.js → chunk-S4COXYBG.js} +588 -32
- package/dist/{chunk-3HAPLH5M.js → chunk-T3Z6VAAF.js} +172 -11
- package/dist/{chunk-FOT2FX5J.js → chunk-TD7UE2L5.js} +12 -10
- package/dist/{chunk-UUANF5CR.js → chunk-TEO2TLVN.js} +856 -967
- package/dist/{chunk-FCSXB6T2.js → chunk-UOSL25KY.js} +14 -2
- package/dist/{chunk-EELBMBT6.js → chunk-VKBMFOYV.js} +74 -15
- package/dist/chunk-VO2LKSTM.js +165 -0
- package/dist/{chunk-5C77SEEY.js → chunk-VPTUJU4P.js} +3 -3
- package/dist/{chunk-WEH5XRJQ.js → chunk-WIE7ZOSW.js} +2 -2
- package/dist/{chunk-J7PIKKWC.js → chunk-WXCJ7VME.js} +8 -8
- package/dist/{chunk-4DGYLA73.js → chunk-XDOQXGFO.js} +22 -7
- package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
- package/dist/{chunk-AB4XIIVB.js → chunk-YKOFT37S.js} +6 -6
- package/dist/chunk-YSEHGPCT.js +127 -0
- package/dist/{chunk-HFSBBKSQ.js → chunk-YW7UVM5V.js} +138 -3
- package/dist/cli/index.js +32 -32
- package/dist/{clio-WBVQEBKO.js → clio-LT5V7SSZ.js} +9 -9
- package/dist/{code-nav-FGGFIE7L.js → code-nav-LMW275PA.js} +5 -5
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-F7OEATSO.js → components-ZFA3SAER.js} +8 -8
- package/dist/{config-TRBL3RCF.js → config-RXS5T3JT.js} +98 -65
- package/dist/{configure-OLCVPHNM.js → configure-2WYWSCSD.js} +26 -22
- package/dist/{context-MJIJ6GOX.js → context-I3BTOTCS.js} +12 -12
- package/dist/{context-XEWE3MOJ.js → context-MVOORGMF.js} +54 -47
- package/dist/{context-WFPKQSM6.js → context-PALKKQYL.js} +28 -28
- package/dist/{context-clear-KNOS2JPB.js → context-clear-N2WOYZ2K.js} +53 -46
- package/dist/{context-index-SSR5ECNE.js → context-index-HNG3MOME.js} +6 -6
- package/dist/{context-working-set-EUXAZI6N.js → context-working-set-MIEVECVZ.js} +17 -18
- package/dist/{dispatch-runner-B7MTOVKL.js → dispatch-runner-VVA4SRRH.js} +90 -61
- package/dist/{docs-FLJTIDSE.js → docs-7LQ23DLM.js} +8 -8
- package/dist/doctor-TWBWFK5V.js +165 -0
- package/dist/eval-IJ5VEZDJ.js +4483 -0
- package/dist/{evidence-JZNBUOQZ.js → evidence-L5APPXNV.js} +68 -61
- package/dist/{evolve-FJVC4KKI.js → evolve-RGNKFJ52.js} +47 -40
- package/dist/{extensions-IQL36S7K.js → extensions-7WYWUX5A.js} +13 -7
- package/dist/{fleet-BDKYJFCP.js → fleet-6CNVBZZP.js} +113 -76
- package/dist/{fleet-commands-ZFIWZSB3.js → fleet-commands-L2SXSYEI.js} +10 -10
- package/dist/{fleet-graph-Y6HPXIVF.js → fleet-graph-2J3OOIPO.js} +17 -15
- package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-CZRJ4JP5.js} +5 -6
- package/dist/{fleet-validate-BIYREGIK.js → fleet-validate-C5RI6DP7.js} +20 -19
- package/dist/{init-LQUB5COQ.js → init-VBN2ACVA.js} +70 -63
- package/dist/{library-NJAHIGG4.js → library-JHGUMLY2.js} +22 -20
- package/dist/{memory-OG6HOYKM.js → memory-K4OQIYWG.js} +49 -42
- package/dist/{models-5ZG5XY7J.js → models-2NCZUWDD.js} +35 -29
- package/dist/{monitor-TJ7AMTGB.js → monitor-MMVTJABD.js} +64 -45
- package/dist/{orchestrator-WZYB54DM.js → orchestrator-ZKBPCHW6.js} +1971 -520
- package/dist/{paths-XUC7GS6E.js → paths-DBXMZMDU.js} +5 -5
- package/dist/registry-LG64LTF4.js +11 -0
- package/dist/{reset-PXQT45IY.js → reset-DD5JGOY3.js} +11 -11
- package/dist/{run-FQ74YF62.js → run-QEGNX7FL.js} +89 -83
- package/dist/{share-FW7SVCL3.js → share-JKD3BQMW.js} +20 -18
- package/dist/{skills-7E7IRB3R.js → skills-LMQIKDOZ.js} +23 -21
- package/dist/{skills-eval-LI75W6OK.js → skills-eval-I7X2774U.js} +59 -52
- package/dist/{steer-GGWFUJUD.js → steer-CF5TDANS.js} +3 -3
- package/dist/support-I7LOJLIF.js +38 -0
- package/dist/{targets-4CIFKCTW.js → targets-RUSR6B5Z.js} +77 -42
- package/dist/{terminal-lease-WUZY7ZV5.js → terminal-lease-QYVORFR4.js} +6 -4
- package/dist/{trace-PNCASAXC.js → trace-ODOQIVIW.js} +61 -6
- package/dist/{uninstall-7FV7IP4E.js → uninstall-ZJF5H5ZN.js} +8 -8
- package/dist/{upgrade-K2HVIVMQ.js → upgrade-XANW3FXB.js} +29 -26
- package/dist/{usage-GTZELZQX.js → usage-4H7ZRXQT.js} +110 -61
- package/dist/{verifiers-RLAHT27O.js → verifiers-UZXNBZEB.js} +13 -13
- package/dist/{verify-BX3BRKH5.js → verify-BVKWTNDL.js} +9 -9
- package/dist/{wiki-generate-ASIFASCN.js → wiki-generate-MY7WV2QI.js} +76 -69
- package/dist/worker/entry.js +69 -66
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/artifact-versions.md +11 -5
- package/docs/built-in-agents.md +1 -1
- package/docs/capacity-and-scheduling.md +23 -2
- package/docs/commands-and-modes.md +2 -2
- package/docs/configuration-and-targets.md +37 -5
- package/docs/context-engine.md +63 -4
- package/docs/documentation-coverage.md +3 -3
- package/docs/documentation-guide.md +1 -1
- package/docs/environment-variables.md +2 -0
- package/docs/eval-runner.md +262 -11
- package/docs/evals-internal.md +72 -2
- package/docs/evidence-and-memory.md +77 -12
- package/docs/evolution.md +1 -1
- package/docs/extensions-and-sharing.md +3 -1
- package/docs/fleet-dispatch.md +34 -9
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +1 -1
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +54 -3
- package/docs/proactive-memory.md +127 -14
- package/docs/prompt-envelope-and-tools.md +22 -2
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +60 -41
- package/docs/safety-model.md +1 -1
- package/docs/scientific-validation.md +1 -1
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +1 -1
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +87 -0
- package/docs/tui-design.md +1 -1
- package/docs/worker-dispatch-mechanics.md +1 -1
- package/package.json +2 -2
- package/src/cli/agents.ts +1 -1
- package/src/cli/argv.ts +5 -0
- package/src/cli/config-inspect.ts +33 -6
- package/src/cli/config.ts +1 -1
- package/src/cli/configure.ts +107 -23
- package/src/cli/doctor-state-size.ts +82 -0
- package/src/cli/doctor.ts +7 -1
- package/src/cli/eval.ts +80 -16
- package/src/cli/evidence.ts +30 -25
- package/src/cli/extensions.ts +5 -1
- package/src/cli/fleet-preflight.ts +2 -12
- package/src/cli/fleet.ts +32 -3
- package/src/cli/shared.ts +1 -0
- package/src/cli/targets.ts +45 -11
- package/src/cli/trace.ts +63 -4
- package/src/cli/usage.ts +63 -14
- package/src/cli/validate-model.ts +60 -5
- package/src/core/bus-events.ts +54 -1
- package/src/core/cache-telemetry.ts +42 -0
- package/src/core/commit-attribution.ts +4 -4
- package/src/core/config.ts +18 -0
- package/src/core/defaults.ts +36 -6
- package/src/core/endpoint-key.ts +27 -0
- package/src/core/path-boundary.ts +100 -0
- package/src/core/residency-target-key.ts +25 -0
- package/src/core/response-schema.ts +36 -2
- package/src/domains/agents/extension.ts +2 -11
- package/src/domains/agents/fleet-contract.ts +30 -12
- package/src/domains/agents/recipe.ts +7 -1
- package/src/domains/agents/registry.ts +73 -5
- package/src/domains/agents/result-contract.ts +128 -17
- package/src/domains/agents/write-boundary.ts +15 -50
- package/src/domains/config/classify.ts +3 -0
- package/src/domains/context/codewiki/coordinator.ts +12 -4
- package/src/domains/context/project-rules.ts +51 -1
- package/src/domains/dispatch/admission.ts +40 -3
- package/src/domains/dispatch/assignment-reconcile.ts +22 -5
- package/src/domains/dispatch/assignment-store.ts +151 -14
- package/src/domains/dispatch/capacity-lease.ts +98 -9
- package/src/domains/dispatch/contract.ts +26 -1
- package/src/domains/dispatch/delegation-plan.ts +2 -5
- package/src/domains/dispatch/execution-plan.ts +44 -4
- package/src/domains/dispatch/execution-role.ts +9 -1
- package/src/domains/dispatch/extension.ts +309 -96
- package/src/domains/dispatch/fleet-run.ts +78 -4
- package/src/domains/dispatch/gate-role-prompts.ts +38 -0
- package/src/domains/dispatch/heartbeat.ts +32 -8
- package/src/domains/dispatch/index.ts +6 -1
- package/src/domains/dispatch/intent-requirements.ts +40 -0
- package/src/domains/dispatch/intent.ts +84 -8
- package/src/domains/dispatch/orphan-recovery.ts +5 -0
- package/src/domains/dispatch/path-scope.ts +370 -0
- package/src/domains/dispatch/receipt-integrity.ts +2 -1
- package/src/domains/dispatch/reservation-store.ts +116 -8
- package/src/domains/dispatch/state.ts +4 -0
- package/src/domains/dispatch/types.ts +14 -7
- package/src/domains/dispatch/validation.ts +6 -3
- package/src/domains/dispatch/worker-spawn.ts +25 -11
- package/src/domains/dispatch/write-boundary-enforcer.ts +62 -0
- package/src/domains/dispatch/write-boundary.ts +262 -22
- package/src/domains/eval/artifacts/store.ts +62 -0
- package/src/domains/eval/compare/behavioral.ts +224 -0
- package/src/domains/eval/compare/compare.ts +355 -2
- package/src/domains/eval/compare/envelope.ts +128 -0
- package/src/domains/eval/compare/gates.ts +24 -6
- package/src/domains/eval/compare/thresholds.ts +30 -3
- package/src/domains/eval/execution-provenance.ts +240 -0
- package/src/domains/eval/metrics/aggregate.ts +136 -0
- package/src/domains/eval/metrics/call-ledger-stream.ts +112 -0
- package/src/domains/eval/metrics/evidence.ts +79 -2
- package/src/domains/eval/metrics/tracked.ts +413 -0
- package/src/domains/eval/provenance.ts +117 -0
- package/src/domains/eval/reports/comparison.ts +128 -0
- package/src/domains/eval/reports/junit.ts +17 -3
- package/src/domains/eval/reports/markdown.ts +3 -3
- package/src/domains/eval/reports/text.ts +14 -0
- package/src/domains/eval/run-compare.ts +20 -0
- package/src/domains/eval/runners/clio-run.ts +139 -2
- package/src/domains/eval/runners/external-command.ts +28 -3
- package/src/domains/eval/schema/adapter.ts +111 -0
- package/src/domains/eval/schema/artifact.ts +20 -0
- package/src/domains/eval/schema/behavioral-metrics.ts +204 -0
- package/src/domains/eval/schema/behavioral.ts +520 -0
- package/src/domains/eval/schema/execution-envelope.ts +194 -0
- package/src/domains/eval/schema/serving.ts +74 -0
- package/src/domains/eval/schema/suite.ts +38 -8
- package/src/domains/eval/schema/validate.ts +58 -3
- package/src/domains/eval/schema/verdict.ts +237 -0
- package/src/domains/eval/suites/resolve.ts +2 -0
- package/src/domains/eval/suites/run.ts +264 -33
- package/src/domains/eval/verifiers/command.ts +2 -1
- package/src/domains/eval/workspaces/temp-copy.ts +145 -13
- package/src/domains/evidence/build.ts +68 -21
- package/src/domains/evidence/eval.ts +2 -12
- package/src/domains/evidence/findings-markdown.ts +33 -0
- package/src/domains/evidence/index.ts +21 -0
- package/src/domains/evidence/provenance.ts +46 -11
- package/src/domains/evidence/run-trust.ts +7 -113
- package/src/domains/evidence/trust-projection.ts +274 -0
- package/src/domains/evidence/trust-status.ts +145 -17
- package/src/domains/evidence/types.ts +4 -0
- package/src/domains/extensions/compatibility.ts +285 -0
- package/src/domains/extensions/discovery.ts +126 -4
- package/src/domains/extensions/resources.ts +21 -9
- package/src/domains/extensions/state.ts +18 -5
- package/src/domains/extensions/types.ts +6 -1
- package/src/domains/lifecycle/doctor.ts +209 -2
- package/src/domains/memory/index.ts +14 -0
- package/src/domains/memory/task-bank-promotion.ts +64 -0
- package/src/domains/memory/task-memory-policy.ts +77 -8
- package/src/domains/memory/task-memory-spend.ts +131 -0
- package/src/domains/memory/task-memory-status.ts +7 -0
- package/src/domains/memory/task-memory-telemetry.ts +2 -0
- package/src/domains/middleware/index.ts +1 -0
- package/src/domains/middleware/memory-intervention.ts +69 -5
- package/src/domains/middleware/memory-step-endpoint.ts +71 -0
- package/src/domains/observability/background-memory-usage.ts +140 -0
- package/src/domains/observability/cost.ts +1 -1
- package/src/domains/observability/index.ts +7 -0
- package/src/domains/observability/out-of-turn-usage.ts +51 -2
- package/src/domains/observability/trace-store.ts +192 -2
- package/src/domains/prompts/compiler.ts +100 -13
- package/src/domains/prompts/contract.ts +3 -5
- package/src/domains/providers/endpoint-capacity.ts +96 -0
- package/src/domains/providers/extension.ts +30 -2
- package/src/domains/providers/index.ts +10 -0
- package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +243 -1
- package/src/domains/providers/runtime-resolution.ts +8 -1
- package/src/domains/providers/runtimes/common/probe-helpers.ts +31 -9
- package/src/domains/providers/runtimes/local-native/llamacpp-anthropic.ts +1 -1
- package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +1 -1
- package/src/domains/providers/runtimes/local-native/llamacpp-embed.ts +1 -1
- package/src/domains/providers/runtimes/local-native/llamacpp-rerank.ts +1 -1
- package/src/domains/providers/runtimes/local-native/llamacpp.ts +4 -1
- package/src/domains/providers/runtimes/local-native/lmstudio.ts +4 -1
- package/src/domains/providers/runtimes/local-native/ollama-native.ts +6 -1
- package/src/domains/providers/types/capability-flags.ts +2 -0
- package/src/domains/providers/types/target-descriptor.ts +2 -0
- package/src/domains/resources/common-loader.ts +3 -0
- package/src/domains/resources/prompts/loader.ts +184 -17
- package/src/domains/safety/call-target.ts +52 -0
- package/src/domains/safety/policy-engine.ts +5 -5
- package/src/domains/safety/run-effects.ts +96 -2
- package/src/domains/safety/scope.ts +7 -12
- package/src/domains/session/context-accounting.ts +52 -1
- package/src/domains/session/context-ledger.ts +37 -13
- package/src/domains/session/index.ts +6 -0
- package/src/domains/session/prompt-cache.ts +140 -0
- package/src/domains/session/prompt-manifest.ts +42 -0
- package/src/engine/acp/adapter.ts +18 -3
- package/src/engine/acp/server.ts +4 -1
- package/src/engine/ai.ts +35 -0
- package/src/engine/apis/llamacpp-residency.ts +55 -3
- package/src/engine/apis/lmstudio.ts +25 -5
- package/src/engine/apis/ollama-native.ts +2 -1
- package/src/engine/apis/openai-completions.ts +80 -17
- package/src/engine/apis/residency-lock.ts +3 -1
- package/src/engine/apis/residency.ts +34 -1
- package/src/engine/prompt-templates.ts +18 -1
- package/src/engine/provider-payload.ts +29 -1
- package/src/engine/worker-runtime.ts +6 -3
- package/src/entry/orchestrator.ts +176 -30
- package/src/interactive/chat-loop-messages.ts +26 -7
- package/src/interactive/chat-loop.ts +318 -41
- package/src/interactive/chat-panel.ts +62 -8
- package/src/interactive/clio-editor.ts +45 -8
- package/src/interactive/context-activity.ts +5 -1
- package/src/interactive/context-meter.ts +1 -1
- package/src/interactive/context-overlay.ts +40 -10
- package/src/interactive/cost-overlay.ts +64 -6
- package/src/interactive/dispatch-board.ts +127 -5
- package/src/interactive/fleet-run-preview.ts +41 -15
- package/src/interactive/handoff-round.ts +41 -2
- package/src/interactive/interactive-application.ts +24 -1
- package/src/interactive/interactive-event-projection.ts +14 -0
- package/src/interactive/interactive-input-runtime.ts +8 -0
- package/src/interactive/interactive-presentation.ts +4 -0
- package/src/interactive/interactive-shell.ts +20 -17
- package/src/interactive/interactive-slash-runtime.ts +27 -4
- package/src/interactive/memory-overlay.ts +8 -0
- package/src/interactive/mutation-preview.ts +295 -0
- package/src/interactive/overlay-general-openers.ts +16 -0
- package/src/interactive/overlay-key-routing.ts +38 -0
- package/src/interactive/overlay-lifecycle.ts +38 -5
- package/src/interactive/overlay-permission-lifecycle.ts +22 -2
- package/src/interactive/overlay-session-lifecycle.ts +73 -9
- package/src/interactive/overlays/ask-user.ts +91 -19
- package/src/interactive/overlays/help-reference.ts +4 -0
- package/src/interactive/overlays/prompts.ts +11 -1
- package/src/interactive/overlays/settings.ts +176 -48
- package/src/interactive/permission-hint.ts +34 -2
- package/src/interactive/permission-overlay.ts +159 -9
- package/src/interactive/prewarm.ts +197 -0
- package/src/interactive/render-trace.ts +162 -15
- package/src/interactive/renderers/tool-execution.ts +4 -0
- package/src/interactive/side-question.ts +58 -1
- package/src/interactive/slash-commands.ts +7 -2
- package/src/interactive/status/controller.ts +11 -0
- package/src/interactive/status/state-machine.ts +54 -2
- package/src/interactive/status/types.ts +7 -0
- package/src/interactive/terminal-lease.ts +2 -0
- package/src/interactive/turn-context.ts +299 -31
- package/src/interactive/turn-persistence.ts +14 -4
- package/src/interactive/turn-prewarm.ts +364 -0
- package/src/interactive/turn-queues.ts +7 -4
- package/src/interactive/turn-runtime.ts +8 -1
- package/src/interactive/turn-state.ts +23 -0
- package/src/interactive/view/artifacts.ts +42 -9
- package/src/interactive/view/view-overlay.ts +43 -6
- package/src/interactive/worker-receipts.ts +14 -2
- package/src/interactive/worker-stream.ts +8 -0
- package/src/tools/ask-user.ts +43 -2
- package/src/tools/dispatch-admission.ts +12 -13
- package/src/tools/dispatch-arguments.ts +27 -0
- package/src/tools/dispatch-plan.ts +46 -9
- package/src/tools/dispatch-runner.ts +48 -13
- package/src/tools/dispatch-scout.ts +1 -1
- package/src/tools/monitor.ts +13 -0
- package/src/tools/registry.ts +16 -0
- package/src/tools/worker-evidence.ts +19 -13
- package/src/worker/spec-contract.ts +2 -1
- package/dist/chunk-AOCYTWAV.js +0 -449
- package/dist/chunk-HWUFFB6L.js +0 -83
- package/dist/chunk-R346GLFC.js +0 -31
- package/dist/chunk-ZGH7FGS5.js +0 -1079
- package/dist/doctor-RN4YKO2X.js +0 -87
- package/dist/eval-RUBJVSNQ.js +0 -2557
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseCommandArgs, substituteArgs } from "../../../engine/prompt-templates.js";
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
COMPAT_RESOURCE_PRECEDENCE,
|
|
15
15
|
defaultScopedResourceRoots,
|
|
16
|
-
readRootEntries,
|
|
17
16
|
sourceInfoForRoot,
|
|
18
17
|
splitYamlFrontmatter,
|
|
19
18
|
stringField,
|
|
@@ -28,11 +27,21 @@ export interface PromptTemplate {
|
|
|
28
27
|
sourceInfo: ResourceSourceInfo;
|
|
29
28
|
/** False for a project compatibility root until the operator opts in; such a template refuses to expand. */
|
|
30
29
|
trusted: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Why this template cannot be expanded, when a load-time failure left it
|
|
32
|
+
* with no usable body. It is loaded anyway, so the namespaced command is
|
|
33
|
+
* recognized and invoking it reports this reason instead of "not a command"
|
|
34
|
+
* (issue #245). `content` is empty on such a template and nothing may send
|
|
35
|
+
* it to a model.
|
|
36
|
+
*/
|
|
37
|
+
unavailable?: string;
|
|
31
38
|
argumentHint?: string;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
export interface PromptTemplateRoot {
|
|
35
42
|
path: string;
|
|
43
|
+
/** Present only for an installed extension resource root. */
|
|
44
|
+
rootPath?: string;
|
|
36
45
|
scope: ResourceScope;
|
|
37
46
|
source?: string;
|
|
38
47
|
precedence?: number;
|
|
@@ -139,6 +148,95 @@ function fallbackDescription(body: string): string {
|
|
|
139
148
|
return normalized.length > 60 ? `${normalized.slice(0, 57)}...` : normalized;
|
|
140
149
|
}
|
|
141
150
|
|
|
151
|
+
// Keep the suffix deliberately conservative. Prompt prose commonly places a
|
|
152
|
+
// comma, parenthesis, or full stop immediately after an @path; treating that
|
|
153
|
+
// punctuation as part of the filesystem reference makes a valid template look
|
|
154
|
+
// unresolved. Extension bundles use portable path components, so these are the
|
|
155
|
+
// only characters needed in a reference while still allowing `..` to be
|
|
156
|
+
// detected and rejected by the containment check below.
|
|
157
|
+
const EXTENSION_ROOT_REFERENCE = /\$\{extensionRoot\}(\/[A-Za-z0-9._~%+@/-]*)?/g;
|
|
158
|
+
const EXTENSION_ROOT_TOKEN = "$" + "{extensionRoot}";
|
|
159
|
+
|
|
160
|
+
function contained(root: string, candidate: string): boolean {
|
|
161
|
+
const relative = path.relative(root, candidate);
|
|
162
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The outcome of a step that may leave the template with no usable body.
|
|
167
|
+
* `reason` is the one sentence that becomes both the diagnostic the `/prompts`
|
|
168
|
+
* overlay renders and the refusal the operator gets when they invoke the
|
|
169
|
+
* command, so the two can never disagree.
|
|
170
|
+
*/
|
|
171
|
+
type BodyResolution = { body: string } | { reason: string };
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolve only existing paths contained by the declaring extension package.
|
|
175
|
+
*
|
|
176
|
+
* The safety half is unchanged: a missing or escaping reference never expands
|
|
177
|
+
* and never reads outside the package. What changed is what the caller does
|
|
178
|
+
* with the failure, which used to be dropping the template entirely.
|
|
179
|
+
*/
|
|
180
|
+
function resolveExtensionReferences(body: string, root: PromptTemplateRoot): BodyResolution {
|
|
181
|
+
if (root.rootPath === undefined || !body.includes(EXTENSION_ROOT_TOKEN)) return { body };
|
|
182
|
+
let canonicalRoot: string;
|
|
183
|
+
try {
|
|
184
|
+
canonicalRoot = realpathSync(root.rootPath);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return { reason: `extension root could not be resolved: ${error instanceof Error ? error.message : String(error)}` };
|
|
187
|
+
}
|
|
188
|
+
let invalid: string | null = null;
|
|
189
|
+
const resolved = body.replace(EXTENSION_ROOT_REFERENCE, (reference, suffix: string | undefined) => {
|
|
190
|
+
const candidate = path.resolve(canonicalRoot, `.${suffix ?? ""}`);
|
|
191
|
+
try {
|
|
192
|
+
const canonicalCandidate = realpathSync(candidate);
|
|
193
|
+
if (!contained(canonicalRoot, canonicalCandidate)) invalid = reference;
|
|
194
|
+
} catch {
|
|
195
|
+
invalid = reference;
|
|
196
|
+
}
|
|
197
|
+
return candidate;
|
|
198
|
+
});
|
|
199
|
+
if (invalid !== null) {
|
|
200
|
+
return { reason: `prompt template has an unresolved or escaping extension reference: ${invalid}` };
|
|
201
|
+
}
|
|
202
|
+
return { body: resolved };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* A template that exists and cannot run.
|
|
207
|
+
*
|
|
208
|
+
* Dropping it made the operator's namespaced command answer "not a command",
|
|
209
|
+
* so an extension author's single bad `${extensionRoot}` path read to their
|
|
210
|
+
* users as "the prompt was never installed" (issue #245). The template is
|
|
211
|
+
* loaded with an empty body and the reason attached instead, which is the same
|
|
212
|
+
* shape an untrusted project template already had, so the existing refusal
|
|
213
|
+
* path carries it to the operator with no new machinery.
|
|
214
|
+
*/
|
|
215
|
+
function unavailableTemplate(
|
|
216
|
+
name: string,
|
|
217
|
+
filePath: string,
|
|
218
|
+
root: PromptTemplateRoot,
|
|
219
|
+
reason: string,
|
|
220
|
+
diagnostics: ResourceDiagnostic[],
|
|
221
|
+
description?: string,
|
|
222
|
+
): ResourceCandidate<PromptTemplate> {
|
|
223
|
+
diagnostics.push({ type: "warning", message: reason, path: filePath });
|
|
224
|
+
const sourceInfo: ResourceSourceInfo = sourceInfoForRoot(root, filePath);
|
|
225
|
+
return {
|
|
226
|
+
name,
|
|
227
|
+
value: {
|
|
228
|
+
name,
|
|
229
|
+
description: description ?? "Prompt template (unavailable)",
|
|
230
|
+
content: "",
|
|
231
|
+
filePath,
|
|
232
|
+
sourceInfo,
|
|
233
|
+
trusted: root.trusted !== false,
|
|
234
|
+
unavailable: `${reason} (${filePath})`,
|
|
235
|
+
},
|
|
236
|
+
source: sourceInfo,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
142
240
|
function loadPromptFile(
|
|
143
241
|
filePath: string,
|
|
144
242
|
root: PromptTemplateRoot,
|
|
@@ -146,25 +244,45 @@ function loadPromptFile(
|
|
|
146
244
|
): ResourceCandidate<PromptTemplate> | null {
|
|
147
245
|
const ext = path.extname(filePath).toLowerCase();
|
|
148
246
|
if (ext !== ".md") return null;
|
|
247
|
+
|
|
248
|
+
// The name comes before the read, so a file that exists but cannot be read
|
|
249
|
+
// still has a command the operator can invoke and be told why.
|
|
250
|
+
const relativePath = path.relative(root.path, filePath);
|
|
251
|
+
if (
|
|
252
|
+
relativePath === "" ||
|
|
253
|
+
relativePath === ".." ||
|
|
254
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
255
|
+
path.isAbsolute(relativePath)
|
|
256
|
+
) {
|
|
257
|
+
// The one failure that stays a drop: the path is outside the discovery
|
|
258
|
+
// root, so there is no name here to offer a command under.
|
|
259
|
+
diagnostics.push({ type: "warning", message: "prompt template escaped its discovery root", path: filePath });
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const name = relativePath.slice(0, -ext.length).split(path.sep).join(":");
|
|
263
|
+
if (name.trim().length === 0) return null;
|
|
264
|
+
|
|
149
265
|
let raw: string;
|
|
150
266
|
try {
|
|
151
267
|
raw = readFileSync(filePath, "utf8");
|
|
152
268
|
} catch (err) {
|
|
153
269
|
const reason = err instanceof Error ? err.message : String(err);
|
|
154
|
-
|
|
155
|
-
return null;
|
|
270
|
+
return unavailableTemplate(name, filePath, root, `prompt template could not be read: ${reason}`, diagnostics);
|
|
156
271
|
}
|
|
157
272
|
|
|
158
|
-
const name = path.basename(filePath, ext);
|
|
159
|
-
if (name.trim().length === 0) return null;
|
|
160
273
|
const { frontmatter, body } = splitOptionalFrontmatter(raw, filePath, diagnostics);
|
|
161
|
-
const
|
|
274
|
+
const declaredDescription = stringField(frontmatter, "description");
|
|
275
|
+
const resolution = resolveExtensionReferences(body, root);
|
|
276
|
+
if ("reason" in resolution) {
|
|
277
|
+
return unavailableTemplate(name, filePath, root, resolution.reason, diagnostics, declaredDescription ?? undefined);
|
|
278
|
+
}
|
|
279
|
+
const description = declaredDescription ?? fallbackDescription(resolution.body);
|
|
162
280
|
const argumentHint = stringField(frontmatter, "argument-hint") ?? stringField(frontmatter, "argumentHint");
|
|
163
281
|
const sourceInfo: ResourceSourceInfo = sourceInfoForRoot(root, filePath);
|
|
164
282
|
const template: PromptTemplate = {
|
|
165
283
|
name,
|
|
166
284
|
description,
|
|
167
|
-
content: body.trim(),
|
|
285
|
+
content: resolution.body.trim(),
|
|
168
286
|
filePath,
|
|
169
287
|
sourceInfo,
|
|
170
288
|
trusted: root.trusted !== false,
|
|
@@ -178,10 +296,42 @@ function loadPromptRoot(
|
|
|
178
296
|
diagnostics: ResourceDiagnostic[],
|
|
179
297
|
): ResourceCandidate<PromptTemplate>[] {
|
|
180
298
|
const candidates: ResourceCandidate<PromptTemplate>[] = [];
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
299
|
+
const pending = [root.path];
|
|
300
|
+
while (pending.length > 0) {
|
|
301
|
+
const directory = pending.pop();
|
|
302
|
+
if (!directory) continue;
|
|
303
|
+
let entries: import("node:fs").Dirent[];
|
|
304
|
+
try {
|
|
305
|
+
const stat = lstatSync(directory);
|
|
306
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
307
|
+
diagnostics.push({
|
|
308
|
+
type: "warning",
|
|
309
|
+
message: "prompt template root contains a non-directory or symbolic link",
|
|
310
|
+
path: directory,
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
317
|
+
diagnostics.push({
|
|
318
|
+
type: "warning",
|
|
319
|
+
message: `prompt template directory could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
320
|
+
path: directory,
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
for (const entry of [...entries].reverse()) {
|
|
325
|
+
const filePath = path.join(directory, entry.name);
|
|
326
|
+
if (entry.isSymbolicLink()) {
|
|
327
|
+
diagnostics.push({ type: "warning", message: "prompt template symbolic link was ignored", path: filePath });
|
|
328
|
+
} else if (entry.isDirectory()) {
|
|
329
|
+
pending.push(filePath);
|
|
330
|
+
} else if (entry.isFile()) {
|
|
331
|
+
const candidate = loadPromptFile(filePath, root, diagnostics);
|
|
332
|
+
if (candidate) candidates.push(candidate);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
185
335
|
}
|
|
186
336
|
return candidates;
|
|
187
337
|
}
|
|
@@ -210,13 +360,18 @@ export function loadPromptTemplates(input: LoadPromptTemplatesInput = {}): Promp
|
|
|
210
360
|
}
|
|
211
361
|
|
|
212
362
|
function parsePromptCommand(input: string): { name: string; rest: string } | null {
|
|
213
|
-
const
|
|
214
|
-
if (!
|
|
215
|
-
const withoutSlash =
|
|
363
|
+
const framed = input.trimStart();
|
|
364
|
+
if (!framed.startsWith("/")) return null;
|
|
365
|
+
const withoutSlash = framed.slice(1);
|
|
216
366
|
const separator = withoutSlash.search(/\s/);
|
|
217
367
|
const name = separator === -1 ? withoutSlash : withoutSlash.slice(0, separator);
|
|
218
368
|
if (name.length === 0 || name.includes("/")) return null;
|
|
219
|
-
|
|
369
|
+
// Leading whitespace before `/` and the first whitespace after the command
|
|
370
|
+
// are framing. A CRLF pair is one delimiter. Every subsequent byte belongs
|
|
371
|
+
// to the argument payload, including indentation, blank lines, and trailing
|
|
372
|
+
// whitespace, so raw `$ARGUMENTS` substitution can be exact at its boundary.
|
|
373
|
+
const delimiterWidth = separator !== -1 && withoutSlash.slice(separator, separator + 2) === "\r\n" ? 2 : 1;
|
|
374
|
+
const rest = separator === -1 ? "" : withoutSlash.slice(separator + delimiterWidth);
|
|
220
375
|
return { name, rest };
|
|
221
376
|
}
|
|
222
377
|
|
|
@@ -225,6 +380,18 @@ export function expandPromptTemplateInput(input: string, templates: PromptTempla
|
|
|
225
380
|
if (!command) return { expanded: false, text: input, args: [], diagnostics: templates.diagnostics };
|
|
226
381
|
const template = templates.items.find((entry) => entry.name === command.name);
|
|
227
382
|
if (!template) return { expanded: false, text: input, args: [], diagnostics: templates.diagnostics };
|
|
383
|
+
// A load-time failure refuses before anything else, and before the trust
|
|
384
|
+
// check, because there is no body here to be trusted or untrusted.
|
|
385
|
+
if (template.unavailable !== undefined) {
|
|
386
|
+
const message = `prompt template /${template.name} cannot run: ${template.unavailable}`;
|
|
387
|
+
return {
|
|
388
|
+
expanded: false,
|
|
389
|
+
text: input,
|
|
390
|
+
args: [],
|
|
391
|
+
diagnostics: templates.diagnostics,
|
|
392
|
+
refusal: { template, message },
|
|
393
|
+
};
|
|
394
|
+
}
|
|
228
395
|
if (!template.trusted) {
|
|
229
396
|
const message = `prompt template ${template.name} comes from an untrusted project root; set skills.trustProjectCompatRoots to use it`;
|
|
230
397
|
return {
|
|
@@ -238,7 +405,7 @@ export function expandPromptTemplateInput(input: string, templates: PromptTempla
|
|
|
238
405
|
const args = parseCommandArgs(command.rest);
|
|
239
406
|
return {
|
|
240
407
|
expanded: true,
|
|
241
|
-
text: substituteArgs(template.content, args),
|
|
408
|
+
text: substituteArgs(template.content, args, command.rest),
|
|
242
409
|
args,
|
|
243
410
|
template,
|
|
244
411
|
diagnostics: templates.diagnostics,
|
|
@@ -31,6 +31,58 @@ function oneLine(value: string): string {
|
|
|
31
31
|
return value.replace(/\s+/g, " ").trim();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** Columns one tab occupies when a multi-line surface expands it. */
|
|
35
|
+
const DISPLAY_TAB_WIDTH = 4;
|
|
36
|
+
|
|
37
|
+
/** Stands in for a control byte that was neutralized, so the cut is visible. */
|
|
38
|
+
const CONTROL_BYTE_PLACEHOLDER = "·";
|
|
39
|
+
|
|
40
|
+
export interface SanitizedDisplayText {
|
|
41
|
+
text: string;
|
|
42
|
+
/** A control byte or escape sequence was neutralized. */
|
|
43
|
+
neutralized: boolean;
|
|
44
|
+
/** A tab was expanded, so column positions are display positions. */
|
|
45
|
+
tabsExpanded: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Neutralize escape sequences and control bytes while keeping line structure.
|
|
50
|
+
* {@link sanitizeCallTargetText} is the one-line form for a card; this is the
|
|
51
|
+
* form a multi-line operator surface needs, where the newlines are the content
|
|
52
|
+
* rather than whitespace to collapse. Tabs become spaces because a bordered
|
|
53
|
+
* frame measures visible width and a literal tab makes that measurement a lie.
|
|
54
|
+
* Every other C0 byte and DEL becomes a visible placeholder, so a payload
|
|
55
|
+
* cannot style, reposition, or spoof the surface that is approving it.
|
|
56
|
+
*/
|
|
57
|
+
export function sanitizeMultilineDisplayText(value: string): SanitizedDisplayText {
|
|
58
|
+
const escapesStripped = value.replace(OSC_PATTERN, "").replace(CSI_PATTERN, "");
|
|
59
|
+
let neutralized = escapesStripped.length !== value.length;
|
|
60
|
+
// CRLF is line structure, not an injection, so it normalizes rather than
|
|
61
|
+
// showing a placeholder at the end of every line of a Windows-authored file.
|
|
62
|
+
const stripped = escapesStripped.replace(/\r\n/g, "\n");
|
|
63
|
+
let tabsExpanded = false;
|
|
64
|
+
let out = "";
|
|
65
|
+
for (const ch of stripped) {
|
|
66
|
+
if (ch === "\n") {
|
|
67
|
+
out += ch;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === "\t") {
|
|
71
|
+
out += " ".repeat(DISPLAY_TAB_WIDTH);
|
|
72
|
+
tabsExpanded = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
76
|
+
if (code < 0x20 || code === 0x7f) {
|
|
77
|
+
out += CONTROL_BYTE_PLACEHOLDER;
|
|
78
|
+
neutralized = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
out += ch;
|
|
82
|
+
}
|
|
83
|
+
return { text: out, neutralized, tabsExpanded };
|
|
84
|
+
}
|
|
85
|
+
|
|
34
86
|
/**
|
|
35
87
|
* Neutralize escape sequences and control bytes in a pre-composed target
|
|
36
88
|
* string and collapse it to one line. Used where a description crosses a
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { artifactDefaultPath } from "../../core/artifact-paths.js";
|
|
4
|
+
import { pathBoundaryCovers, resolvePathBoundary } from "../../core/path-boundary.js";
|
|
4
5
|
import { canonicalizeExistingPath } from "../../core/path-canonical.js";
|
|
5
6
|
import { ToolNames } from "../../core/tool-names.js";
|
|
6
7
|
import { clioConfigDir } from "../../core/xdg.js";
|
|
@@ -89,7 +90,8 @@ export interface SafetyPolicyEngineOptions {
|
|
|
89
90
|
rulePacks?: RulePacks;
|
|
90
91
|
projectPolicy?: LoadedProjectSafetyPolicy;
|
|
91
92
|
/**
|
|
92
|
-
* Absolute
|
|
93
|
+
* Absolute path boundaries a write-class tool call is confined to for this run.
|
|
94
|
+
* Exact files omit a trailing slash and subtrees retain one.
|
|
93
95
|
* When present and non-empty, a write/edit target outside every root is a
|
|
94
96
|
* final BLOCK (reason code "write-root"). Empty or absent disables the check.
|
|
95
97
|
* Enforced at the worker safety seam so both the native worker registry and
|
|
@@ -182,9 +184,7 @@ function evaluateWriteRoots(roots: ReadonlyArray<string>, writeRootCwd: string,
|
|
|
182
184
|
const target = writeRootTargetPath(call);
|
|
183
185
|
if (target === null) return null;
|
|
184
186
|
const resolved = path.resolve(writeRootCwd, target);
|
|
185
|
-
|
|
186
|
-
if (resolved === root || resolved.startsWith(`${root}${path.sep}`)) return null;
|
|
187
|
-
}
|
|
187
|
+
if (pathBoundaryCovers(roots, resolved)) return null;
|
|
188
188
|
return `write target '${target}' resolves to '${resolved}', which is outside the permitted write roots for this run: ${roots.join(", ")}`;
|
|
189
189
|
}
|
|
190
190
|
|
|
@@ -193,7 +193,7 @@ export function createSafetyPolicyEngine(options: SafetyPolicyEngineOptions = {}
|
|
|
193
193
|
// Write-root containment resolves lexically, so it keeps its own un-canonicalized
|
|
194
194
|
// cwd and roots to compare like against like (the design mandates no symlink chasing).
|
|
195
195
|
const writeRootCwd = path.resolve(options.cwd ?? process.cwd());
|
|
196
|
-
const writeRoots = (options.writeRoots ?? []).map((root) =>
|
|
196
|
+
const writeRoots = (options.writeRoots ?? []).map((root) => resolvePathBoundary(writeRootCwd, root));
|
|
197
197
|
const packs = options.rulePacks ?? getCachedDefaultRulePacks();
|
|
198
198
|
const projectPolicy = options.projectPolicy ?? loadProjectSafetyPolicy(cwd);
|
|
199
199
|
const projectPolicyRoot =
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { ToolNames } from "../../core/tool-names.js";
|
|
3
|
+
import { classify } from "./action-classifier.js";
|
|
3
4
|
import { typedValidationSummary } from "./finish-contract.js";
|
|
4
5
|
import {
|
|
5
6
|
detectValidationCommand,
|
|
@@ -25,6 +26,13 @@ import {
|
|
|
25
26
|
* bash command that writes through a script) leaves the path absent from the
|
|
26
27
|
* set and the validator falls back to the filesystem rather than to a failed
|
|
27
28
|
* conformance.
|
|
29
|
+
*
|
|
30
|
+
* `writeRecordComplete` is the other half of that sentence, and it exists for
|
|
31
|
+
* the reader that cannot fall back to the filesystem. The write boundary uses
|
|
32
|
+
* this record the other way round, to decide that a path the run did *not*
|
|
33
|
+
* write is somebody else's and must survive. Reading an under-reporting set as
|
|
34
|
+
* a closed list would clear a step of a write it really made, so the recorder
|
|
35
|
+
* says outright whether the set is closed.
|
|
28
36
|
*/
|
|
29
37
|
export interface RunEffects {
|
|
30
38
|
/** Absolute paths the run's successful tool calls aimed a mutation at. */
|
|
@@ -56,6 +64,35 @@ export interface RunEffects {
|
|
|
56
64
|
* wider set.
|
|
57
65
|
*/
|
|
58
66
|
verificationCommands: ReadonlySet<string>;
|
|
67
|
+
/**
|
|
68
|
+
* False once the run made a successful call to a tool that can mutate a path
|
|
69
|
+
* its own arguments do not name. After such a call `mutatedPaths` is a lower
|
|
70
|
+
* bound rather than the whole list, so a reader that would treat an absent
|
|
71
|
+
* path as proof the run did not write it has to stop treating it that way.
|
|
72
|
+
*
|
|
73
|
+
* The downgrade covers the whole run rather than the one call, because an
|
|
74
|
+
* opaque call's effect is unbounded. A shell command, a project script, or a
|
|
75
|
+
* spawned worker can write anywhere the process can reach, so once one has
|
|
76
|
+
* run there is no path left in the checkout that the record can clear.
|
|
77
|
+
*/
|
|
78
|
+
writeRecordComplete: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Successful opaque calls that made the write record incomplete. The call id
|
|
81
|
+
* and tool name are retained so a later write-boundary verdict can explain
|
|
82
|
+
* exactly why attribution was downgraded instead of exposing only a boolean.
|
|
83
|
+
*/
|
|
84
|
+
writeRecordDowngrades: ReadonlyArray<WriteRecordDowngrade>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface WriteRecordDowngrade {
|
|
88
|
+
reason: "opaque_tool_succeeded";
|
|
89
|
+
tool: string;
|
|
90
|
+
toolCallId: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface RunEffectsRecorderOptions {
|
|
94
|
+
/** Fires once, when the first successful opaque call opens the record. */
|
|
95
|
+
onWriteRecordDowngraded?(downgrade: WriteRecordDowngrade): void;
|
|
59
96
|
}
|
|
60
97
|
|
|
61
98
|
export interface RunEffectsRecorder {
|
|
@@ -67,9 +104,44 @@ export interface RunEffectsRecorder {
|
|
|
67
104
|
}
|
|
68
105
|
|
|
69
106
|
interface PendingEffects {
|
|
107
|
+
tool: string;
|
|
108
|
+
toolCallId: string;
|
|
70
109
|
paths: ReadonlyArray<string>;
|
|
71
110
|
validationCommand: string | null;
|
|
72
111
|
verificationCommand: string | null;
|
|
112
|
+
opaque: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Action classes whose successful calls write only paths their own arguments
|
|
117
|
+
* name. `read` covers the observe, retrieve, interact, and orchestrate-bookkeeping
|
|
118
|
+
* tools, none of which touch a source-workspace file: `git` is a closed
|
|
119
|
+
* status/diff/log surface, and `tasks`, `ledger`, and `context` write Clio's own
|
|
120
|
+
* state. `write` covers `write`, `edit`, and `artifact`, whose target
|
|
121
|
+
* `toolMutationPaths` resolves from the call itself, including the artifact
|
|
122
|
+
* tool's default path.
|
|
123
|
+
*/
|
|
124
|
+
const ENUMERABLE_ACTION_CLASSES: ReadonlySet<string> = new Set(["read", "write"]);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whether a successful call to this tool can mutate a path its own recorded
|
|
128
|
+
* arguments do not name.
|
|
129
|
+
*
|
|
130
|
+
* Asked of the classifier rather than of a hand-written list, because
|
|
131
|
+
* `src/tools/policy.ts` asserts at bootstrap that every registered tool's
|
|
132
|
+
* `baseActionClass` equals what this classifier returns and what the plane
|
|
133
|
+
* table declares. A tool added to the execute or orchestrate-dispatch plane is
|
|
134
|
+
* therefore opaque the day it is registered, with no list to remember to update.
|
|
135
|
+
*
|
|
136
|
+
* The classifier is asked about the tool name alone. Bash's argument scan can
|
|
137
|
+
* escalate a single call to `system_modify` or `git_destructive`, and those are
|
|
138
|
+
* still bash; the question here is what the tool can do, not what one call was
|
|
139
|
+
* caught doing. An unregistered name (a dynamic or MCP tool) classifies as
|
|
140
|
+
* `unknown` and is opaque, which is the fail-closed answer for a tool whose
|
|
141
|
+
* arguments this process has no schema for.
|
|
142
|
+
*/
|
|
143
|
+
export function toolWritesOpaquely(toolName: string): boolean {
|
|
144
|
+
return !ENUMERABLE_ACTION_CLASSES.has(classify({ tool: toolName }).actionClass);
|
|
73
145
|
}
|
|
74
146
|
|
|
75
147
|
/**
|
|
@@ -151,24 +223,31 @@ function validationCommandOf(
|
|
|
151
223
|
* when its result comes back clean: a blocked write and a red command are not
|
|
152
224
|
* evidence that the workspace changed or that a check passed.
|
|
153
225
|
*/
|
|
154
|
-
export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
|
|
226
|
+
export function createRunEffectsRecorder(cwd: string, options: RunEffectsRecorderOptions = {}): RunEffectsRecorder {
|
|
155
227
|
const pending = new Map<string, PendingEffects>();
|
|
156
228
|
const mutatedPaths = new Set<string>();
|
|
157
229
|
const failedMutationPaths = new Set<string>();
|
|
158
230
|
const validationCommands = new Set<string>();
|
|
159
231
|
const verificationCommands = new Set<string>();
|
|
232
|
+
const writeRecordDowngrades = new Map<string, WriteRecordDowngrade>();
|
|
160
233
|
return {
|
|
161
234
|
start(toolCallId, toolName, args) {
|
|
162
235
|
const paths = mutationTargets(toolName, args);
|
|
163
236
|
const validationCommand = validationCommandOf(toolName, args, "finish-contract");
|
|
164
237
|
const verificationCommand = validationCommandOf(toolName, args, "grounding");
|
|
165
|
-
|
|
238
|
+
const opaque = toolWritesOpaquely(toolName);
|
|
239
|
+
// An opaque call is tracked even when it exposed no path and no
|
|
240
|
+
// command, because the fact that it ran at all is the finding.
|
|
241
|
+
if (!opaque && paths.length === 0 && validationCommand === null && verificationCommand === null) return;
|
|
166
242
|
// A bash call may re-base its relative paths with its own cwd argument.
|
|
167
243
|
const base = typeof args?.cwd === "string" && args.cwd.length > 0 ? path.resolve(cwd, args.cwd) : cwd;
|
|
168
244
|
pending.set(toolCallId, {
|
|
245
|
+
tool: toolName,
|
|
246
|
+
toolCallId,
|
|
169
247
|
paths: paths.map((target) => path.resolve(base, target)),
|
|
170
248
|
validationCommand,
|
|
171
249
|
verificationCommand,
|
|
250
|
+
opaque,
|
|
172
251
|
});
|
|
173
252
|
},
|
|
174
253
|
finish(toolCallId, failed) {
|
|
@@ -179,6 +258,19 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
|
|
|
179
258
|
for (const target of effects.paths) failedMutationPaths.add(target);
|
|
180
259
|
return;
|
|
181
260
|
}
|
|
261
|
+
// Success-gated on purpose. A shell command the safety policy blocked,
|
|
262
|
+
// or one that came back an error, never reached the filesystem, so it
|
|
263
|
+
// leaves the record closed.
|
|
264
|
+
if (effects.opaque) {
|
|
265
|
+
const downgrade: WriteRecordDowngrade = {
|
|
266
|
+
reason: "opaque_tool_succeeded",
|
|
267
|
+
tool: effects.tool,
|
|
268
|
+
toolCallId: effects.toolCallId,
|
|
269
|
+
};
|
|
270
|
+
const recordWasComplete = writeRecordDowngrades.size === 0;
|
|
271
|
+
writeRecordDowngrades.set(effects.toolCallId, downgrade);
|
|
272
|
+
if (recordWasComplete) options.onWriteRecordDowngraded?.({ ...downgrade });
|
|
273
|
+
}
|
|
182
274
|
for (const target of effects.paths) mutatedPaths.add(target);
|
|
183
275
|
if (effects.validationCommand !== null) validationCommands.add(effects.validationCommand);
|
|
184
276
|
if (effects.verificationCommand !== null) verificationCommands.add(effects.verificationCommand);
|
|
@@ -191,6 +283,8 @@ export function createRunEffectsRecorder(cwd: string): RunEffectsRecorder {
|
|
|
191
283
|
failedMutationPaths: new Set([...failedMutationPaths].filter((target) => !mutatedPaths.has(target))),
|
|
192
284
|
validationCommands: new Set(validationCommands),
|
|
193
285
|
verificationCommands: new Set(verificationCommands),
|
|
286
|
+
writeRecordComplete: writeRecordDowngrades.size === 0,
|
|
287
|
+
writeRecordDowngrades: [...writeRecordDowngrades.values()].map((entry) => ({ ...entry })),
|
|
194
288
|
};
|
|
195
289
|
},
|
|
196
290
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { asDirectoryPathBoundary, pathBoundaryEntryCovers, resolvePathBoundary } from "../../core/path-boundary.js";
|
|
2
3
|
import type { ActionClass } from "./action-classifier.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -15,15 +16,7 @@ export interface ScopeSpec {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
function normalizeRoot(root: string): string {
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function isUnder(child: string, parent: string): boolean {
|
|
22
|
-
const c = normalizeRoot(child);
|
|
23
|
-
const p = normalizeRoot(parent);
|
|
24
|
-
if (c === p) return true;
|
|
25
|
-
const rel = path.relative(p, c);
|
|
26
|
-
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
19
|
+
return resolvePathBoundary(process.cwd(), root);
|
|
27
20
|
}
|
|
28
21
|
|
|
29
22
|
export function isSubset(worker: ScopeSpec, orchestrator: ScopeSpec): boolean {
|
|
@@ -31,7 +24,9 @@ export function isSubset(worker: ScopeSpec, orchestrator: ScopeSpec): boolean {
|
|
|
31
24
|
if (!orchestrator.allowedActions.has(action)) return false;
|
|
32
25
|
}
|
|
33
26
|
for (const root of worker.allowedWriteRoots) {
|
|
34
|
-
const covered = orchestrator.allowedWriteRoots.some((outer) =>
|
|
27
|
+
const covered = orchestrator.allowedWriteRoots.some((outer) =>
|
|
28
|
+
pathBoundaryEntryCovers(normalizeRoot(outer), normalizeRoot(root)),
|
|
29
|
+
);
|
|
35
30
|
if (!covered) return false;
|
|
36
31
|
}
|
|
37
32
|
if (worker.allowNetwork && !orchestrator.allowNetwork) return false;
|
|
@@ -48,14 +43,14 @@ export const READONLY_SCOPE: ScopeSpec = {
|
|
|
48
43
|
|
|
49
44
|
export const WORKSPACE_SCOPE: ScopeSpec = {
|
|
50
45
|
allowedActions: new Set<ActionClass>(["read", "write", "execute", "dispatch"]),
|
|
51
|
-
allowedWriteRoots: [process.cwd()],
|
|
46
|
+
allowedWriteRoots: [asDirectoryPathBoundary(path.resolve(process.cwd()))],
|
|
52
47
|
allowNetwork: true,
|
|
53
48
|
allowDispatch: true,
|
|
54
49
|
};
|
|
55
50
|
|
|
56
51
|
export const CONFIRMED_SCOPE: ScopeSpec = {
|
|
57
52
|
allowedActions: new Set<ActionClass>(["read", "write", "execute", "dispatch", "system_modify"]),
|
|
58
|
-
allowedWriteRoots: [process.cwd()],
|
|
53
|
+
allowedWriteRoots: [asDirectoryPathBoundary(path.resolve(process.cwd()))],
|
|
59
54
|
allowNetwork: true,
|
|
60
55
|
allowDispatch: true,
|
|
61
56
|
};
|
|
@@ -72,6 +72,19 @@ export interface ContextSnapshot {
|
|
|
72
72
|
splits: Record<string, "estimated" | "exact" | "reconciled">;
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Divergence record. `estimatedTokens` is the chars/4 prompt-side total this
|
|
77
|
+
* snapshot was captured with and is never rewritten by a reconcile, so the
|
|
78
|
+
* two figures stay comparable. `reconciledTokens` is the provider's own
|
|
79
|
+
* prompt token count for the call this snapshot was reconciled against
|
|
80
|
+
* (cached prompt tokens folded back in), and `divergenceRatio` is
|
|
81
|
+
* `reconciledTokens / estimatedTokens`. A ratio above 1 is the estimator
|
|
82
|
+
* under-counting, which is what issue #227 recorded as a provider overflow.
|
|
83
|
+
*/
|
|
84
|
+
estimatedTokens?: number | undefined;
|
|
85
|
+
reconciledTokens?: number | undefined;
|
|
86
|
+
divergenceRatio?: number | undefined;
|
|
87
|
+
|
|
75
88
|
promptHash?: string | undefined;
|
|
76
89
|
toolSignature?: string | undefined;
|
|
77
90
|
messageRange?: { start: number; end: number } | undefined;
|
|
@@ -332,6 +345,31 @@ export function getLatestContextSnapshot(meta: SessionMeta): ContextSnapshot | n
|
|
|
332
345
|
return list.at(-1) ?? null;
|
|
333
346
|
}
|
|
334
347
|
|
|
348
|
+
/**
|
|
349
|
+
* The window a previous process measured this session's prompts against, when
|
|
350
|
+
* the backend itself reported it as the one it had open (issue #227).
|
|
351
|
+
*
|
|
352
|
+
* A resumed session re-resolves its target before discovery has reported a
|
|
353
|
+
* loaded window, so the first turn budgets against the probed server-wide
|
|
354
|
+
* figure, which on a multi-slot backend is a multiple of what the model is
|
|
355
|
+
* actually serving. The snapshot ledger already recorded the number the
|
|
356
|
+
* backend named, so the resume carries it forward instead of re-probing.
|
|
357
|
+
* Scoped to the same target and model: a different selection has no claim on
|
|
358
|
+
* that measurement, and a live loaded window always outranks this one, so a
|
|
359
|
+
* model reloaded at a new size corrects on the next resolution.
|
|
360
|
+
*/
|
|
361
|
+
export function lastLoadedContextWindow(meta: SessionMeta, providerId: string, modelId: string): number | null {
|
|
362
|
+
const snapshots = getContextSnapshots(meta);
|
|
363
|
+
for (let i = snapshots.length - 1; i >= 0; i -= 1) {
|
|
364
|
+
const snapshot = snapshots[i];
|
|
365
|
+
if (snapshot?.contextWindowSource !== "loaded") continue;
|
|
366
|
+
if (snapshot.providerId !== providerId || snapshot.modelId !== modelId) continue;
|
|
367
|
+
const window = snapshot.effectiveContextWindow;
|
|
368
|
+
if (typeof window === "number" && Number.isFinite(window) && window > 0) return window;
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
335
373
|
export function buildSnapshotCategories(inputs: {
|
|
336
374
|
systemPrompt?: string | undefined;
|
|
337
375
|
promptSegments?: ReadonlyArray<{ id: string; tokenEstimate: number }> | undefined;
|
|
@@ -456,6 +494,7 @@ export function captureContextSnapshot(input: CaptureContextSnapshotInput): Cont
|
|
|
456
494
|
compactionThreshold: input.compactionThreshold,
|
|
457
495
|
});
|
|
458
496
|
return {
|
|
497
|
+
estimatedTokens: sumSplitTokens(categories),
|
|
459
498
|
snapshotId: nextSnapshotId(),
|
|
460
499
|
sessionId: input.sessionId,
|
|
461
500
|
turnId: input.turnId,
|
|
@@ -480,9 +519,13 @@ export function captureContextSnapshot(input: CaptureContextSnapshotInput): Cont
|
|
|
480
519
|
};
|
|
481
520
|
}
|
|
482
521
|
|
|
522
|
+
function sumSplitTokens(categories: ContextSnapshot["categories"]): number {
|
|
523
|
+
return SNAPSHOT_SPLIT_KEYS.reduce((sum, key) => sum + (categories[key] ?? 0), 0);
|
|
524
|
+
}
|
|
525
|
+
|
|
483
526
|
/** Sum of the prompt-side categories (everything except reserve/free/streaming). */
|
|
484
527
|
export function snapshotInputTokens(snapshot: ContextSnapshot): number {
|
|
485
|
-
return
|
|
528
|
+
return sumSplitTokens(snapshot.categories);
|
|
486
529
|
}
|
|
487
530
|
|
|
488
531
|
export function reconcileSnapshot(snapshot: ContextSnapshot, usage: Usage): ContextSnapshot {
|
|
@@ -572,9 +615,17 @@ export function reconcileSnapshot(snapshot: ContextSnapshot, usage: Usage): Cont
|
|
|
572
615
|
{} as Record<string, "estimated" | "exact" | "reconciled">,
|
|
573
616
|
);
|
|
574
617
|
|
|
618
|
+
// The estimate this snapshot was captured with, kept whole across repeated
|
|
619
|
+
// reconciles so the recorded divergence always compares the provider count
|
|
620
|
+
// against chars/4 rather than against a previously normalized total.
|
|
621
|
+
const estimatedTokens = snapshot.estimatedTokens ?? sumSplitTokens(snapshot.categories);
|
|
622
|
+
|
|
575
623
|
return {
|
|
576
624
|
...snapshot,
|
|
577
625
|
categories: updatedCategories,
|
|
626
|
+
estimatedTokens,
|
|
627
|
+
reconciledTokens: exactInput,
|
|
628
|
+
divergenceRatio: estimatedTokens > 0 ? Math.round((exactInput / estimatedTokens) * 1000) / 1000 : undefined,
|
|
578
629
|
sources: {
|
|
579
630
|
total: "reconciled",
|
|
580
631
|
splits: splitsSources,
|