@iowarp/clio-coder 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +22 -6
- package/dist/{acp-2BEHC4DL.js → acp-U67UHUK2.js} +14 -14
- package/dist/{agents-LNNFTM53.js → agents-YU6SGALZ.js} +42 -35
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-KXXFI2VS.js → auth-5ZPJOIVG.js} +32 -24
- package/dist/builtins-C6JMZVV6.js +17 -0
- package/dist/{chunk-E25LMLRW.js → chunk-26LEYJZH.js} +2 -2
- package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
- package/dist/{chunk-PBTHKCPN.js → chunk-2HFZQUHL.js} +7 -7
- package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
- package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
- package/dist/{chunk-CKXWIANG.js → chunk-4SPRNWDE.js} +18 -16
- package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
- package/dist/{chunk-XF5N4U5A.js → chunk-5DHKRSMQ.js} +9 -8
- package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
- package/dist/{chunk-EKY57CSP.js → chunk-5H3GB5BO.js} +68 -772
- package/dist/{chunk-6XXKFVSN.js → chunk-5Q2VVUKB.js} +4 -4
- package/dist/{chunk-DJVECN66.js → chunk-7RFXX52T.js} +295 -3705
- package/dist/{chunk-ZXF4XRKW.js → chunk-7RGZWPB6.js} +158 -8
- package/dist/{chunk-FYYLNIL5.js → chunk-A2NJGIB3.js} +2 -2
- package/dist/{chunk-XXQNGV4M.js → chunk-A3WNZD3P.js} +548 -72
- package/dist/{chunk-VEZEGCGW.js → chunk-B5CSFE7B.js} +23 -21
- package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
- package/dist/chunk-DR52UMZW.js +21 -0
- package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
- package/dist/{chunk-QKMUKYO7.js → chunk-E77JEWSD.js} +270 -125
- package/dist/{chunk-LYF7OHWH.js → chunk-EMYUUSFG.js} +12 -467
- package/dist/{chunk-4VP4KH3K.js → chunk-EQ63NRB7.js} +8 -8
- package/dist/chunk-FBVTI2TJ.js +518 -0
- package/dist/{chunk-R46L2BIR.js → chunk-FHJEP5SW.js} +19 -25
- package/dist/{chunk-WR67VIZY.js → chunk-GN57SG4G.js} +66 -8
- package/dist/{chunk-RD5U66HV.js → chunk-GPIEI3LY.js} +9 -9
- package/dist/{chunk-PCZJO5TI.js → chunk-GU2UIAFZ.js} +13 -178
- package/dist/chunk-GWS3VEIW.js +195 -0
- package/dist/chunk-H7IXIC72.js +103 -0
- package/dist/{chunk-3BPUFZDL.js → chunk-HLE42MG7.js} +3 -3
- package/dist/{chunk-24I7BN55.js → chunk-IFBNV6H6.js} +3 -3
- package/dist/{chunk-QNQHSOLF.js → chunk-IGWKHNIQ.js} +114 -55
- package/dist/chunk-IIZWH4XA.js +172 -0
- package/dist/{chunk-AD2SYQYC.js → chunk-IJ7RPIYJ.js} +124 -6
- package/dist/chunk-J3YUBZWY.js +382 -0
- package/dist/{chunk-5JGRAMKL.js → chunk-JOZYP4GM.js} +8 -6
- package/dist/{chunk-OH3TOQTB.js → chunk-K4XHGFR5.js} +751 -15
- package/dist/{chunk-EYPA3EGJ.js → chunk-KTYTFRMB.js} +178 -14
- package/dist/chunk-LU7P4LHA.js +33 -0
- package/dist/chunk-ME6CCNFO.js +108 -0
- package/dist/chunk-MXKJU4JB.js +1100 -0
- package/dist/{chunk-RY3LY4J5.js → chunk-N22QMJKY.js} +21 -18
- package/dist/chunk-NMPKI6XL.js +3006 -0
- package/dist/chunk-NUGM5KR6.js +165 -0
- package/dist/{chunk-22NAGB7X.js → chunk-P43ETTHK.js} +5 -94
- package/dist/{chunk-NILBFAPG.js → chunk-PMDBGQSJ.js} +2 -2
- package/dist/chunk-PT7HYKEM.js +165 -0
- package/dist/chunk-RVG5JXAL.js +41 -0
- package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
- package/dist/{chunk-4BPJXDWC.js → chunk-TANS5ZJS.js} +35 -19
- package/dist/{verifiers-NCBTHHN2.js → chunk-TB5666IT.js} +67 -324
- package/dist/{chunk-QM3F2GKX.js → chunk-TLQJPP24.js} +7927 -8088
- package/dist/{chunk-G7MUEIGA.js → chunk-TT36MB5S.js} +2 -1
- package/dist/chunk-TTHACPOM.js +961 -0
- package/dist/{chunk-6US73PDB.js → chunk-TYPGUK6W.js} +7 -7
- package/dist/{chunk-MFFY33HR.js → chunk-U6MBIEMB.js} +554 -209
- package/dist/chunk-VAWNZU7Z.js +242 -0
- package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
- package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
- package/dist/{chunk-XYDYPRZI.js → chunk-VWZOAB7K.js} +10 -10
- package/dist/{chunk-ZRGEBJ4T.js → chunk-WLFILSD5.js} +48 -48
- package/dist/{chunk-K7T3E2SR.js → chunk-WNIJTQQK.js} +12 -11
- package/dist/{chunk-CYQKWTG3.js → chunk-WSB3FPX7.js} +68 -20
- package/dist/{chunk-KHSFENX2.js → chunk-WWCZ5F23.js} +116 -14
- package/dist/{chunk-XE2VEJHX.js → chunk-WXY7KU3G.js} +2 -2
- package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
- package/dist/{chunk-KOHPCX4K.js → chunk-XWSF374K.js} +5 -5
- package/dist/{chunk-CJUB2JJ2.js → chunk-YS5VLNH5.js} +10 -10
- package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
- package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
- package/dist/cli/index.js +33 -31
- package/dist/{clio-M2KGYUFZ.js → clio-QVTYJ57A.js} +10 -10
- package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +5 -5
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-5TTYYX6G.js → components-ZFA3SAER.js} +9 -9
- package/dist/{config-XUUYQIWO.js → config-LW5IJFQN.js} +65 -55
- package/dist/{configure-IHJ7YOMV.js → configure-7XIZCOU4.js} +29 -24
- package/dist/{context-75MIWW3U.js → context-L3WL3X7K.js} +54 -43
- package/dist/{context-ZQ7SIFJV.js → context-N52ZA626.js} +30 -14
- package/dist/{context-74JLXAWD.js → context-Y6Y7QPR6.js} +12 -12
- package/dist/{context-clear-GYKWNUML.js → context-clear-MBQRLSDQ.js} +54 -43
- package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
- package/dist/{context-working-set-UX5KEP4J.js → context-working-set-GS6DSO7F.js} +18 -18
- package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-22ZCNOM3.js} +367 -79
- package/dist/{docs-6FZSCG5B.js → docs-7LQ23DLM.js} +9 -9
- package/dist/{doctor-SVJ5BZCW.js → doctor-M7YEDGAE.js} +27 -23
- package/dist/{eval-CG6LLBLD.js → eval-BEC2WHDA.js} +73 -20
- package/dist/{evidence-ZYFIEN42.js → evidence-REJUMSKM.js} +70 -59
- package/dist/{evolve-QGEXEMDW.js → evolve-PY5ZBA5K.js} +50 -39
- package/dist/{extensions-ADGNCJJD.js → extensions-HVKU65YU.js} +7 -7
- package/dist/{fleet-S5R4ZOQY.js → fleet-7WZEWRFA.js} +236 -377
- package/dist/fleet-commands-UVHWM76J.js +70 -0
- package/dist/fleet-graph-6ULH7PES.js +125 -0
- package/dist/fleet-new-RDVJLHHH.js +48 -0
- package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
- package/dist/fleet-validate-72PC4SLA.js +79 -0
- package/dist/{init-5DRU55YR.js → init-OG3TPGQG.js} +69 -57
- package/dist/library-CNTMPLRF.js +217 -0
- package/dist/{memory-7YKKR6UC.js → memory-6IS7F275.js} +52 -41
- package/dist/{models-ZPOLRU2C.js → models-ENRJDA5W.js} +37 -31
- package/dist/{monitor-US5F5YGZ.js → monitor-XLDVO7TN.js} +79 -42
- package/dist/{orchestrator-E2AL4T5N.js → orchestrator-6KSPYRHA.js} +4126 -762
- package/dist/{paths-E7KYAQWE.js → paths-DBXMZMDU.js} +6 -6
- package/dist/registry-LG64LTF4.js +11 -0
- package/dist/{reset-KZ652EK6.js → reset-RZ4ER727.js} +12 -12
- package/dist/{run-SRNBKDWD.js → run-Y2CNK5RU.js} +89 -76
- package/dist/{share-CGZE33UP.js → share-A55GYP6Z.js} +37 -13
- package/dist/{skills-S2X4DLY5.js → skills-ALC5J6AT.js} +29 -13
- package/dist/{skills-eval-W2GGIC4R.js → skills-eval-JPBEBYQU.js} +60 -48
- package/dist/support-MIETYA5E.js +38 -0
- package/dist/{targets-54SWINWB.js → targets-VGNXIR3S.js} +46 -36
- package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WOBR64YA.js} +4 -4
- package/dist/{uninstall-BVLWXKBT.js → uninstall-ZJF5H5ZN.js} +9 -9
- package/dist/{upgrade-JKAR27XC.js → upgrade-FUSUAGHR.js} +31 -28
- package/dist/{usage-MSAWCLX4.js → usage-N4MKVHKD.js} +140 -68
- package/dist/verifiers-YAWOJ3H2.js +336 -0
- package/dist/{verify-X5HDROLA.js → verify-LTDHYBGY.js} +10 -9
- package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-6M7GHTBJ.js} +75 -62
- package/dist/worker/entry.js +67 -57
- package/docs/README.md +3 -2
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/artifact-versions.md +11 -6
- package/docs/built-in-agents.md +26 -2
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +83 -3
- package/docs/configuration-and-targets.md +86 -4
- package/docs/context-engine.md +1 -1
- package/docs/development-pipeline.md +1 -1
- package/docs/dispatch-architecture-rationale.md +1 -1
- package/docs/documentation-coverage.md +3 -3
- package/docs/documentation-guide.md +3 -3
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +1 -1
- package/docs/evidence-and-memory.md +74 -10
- package/docs/evolution.md +1 -1
- package/docs/exit-codes-and-output.md +4 -1
- package/docs/extensions-and-sharing.md +6 -2
- package/docs/fleet-demo-runbook.md +2 -2
- package/docs/fleet-dispatch.md +228 -16
- package/docs/git-commit-provenance.md +2 -2
- package/docs/glossary.md +22 -2
- package/docs/installation-and-lifecycle.md +2 -2
- package/docs/middleware-and-components.md +2 -1
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +56 -9
- package/docs/proactive-memory.md +1 -1
- package/docs/prompt-envelope-and-tools.md +4 -2
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +83 -65
- package/docs/resource-library.md +59 -0
- package/docs/safety-model.md +2 -2
- package/docs/scientific-validation.md +3 -3
- package/docs/session-lifecycle.md +37 -1
- package/docs/skills-marketplace.md +16 -3
- package/docs/tool-usage.md +14 -7
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/tui-design.md +1 -1
- package/docs/worker-dispatch-mechanics.md +3 -3
- package/package.json +1 -2
- package/src/cli/argv.ts +5 -0
- package/src/cli/configure.ts +107 -23
- package/src/cli/doctor.ts +5 -1
- package/src/cli/evidence.ts +30 -25
- package/src/cli/fleet-commands.ts +37 -0
- package/src/cli/fleet-graph.ts +102 -0
- package/src/cli/fleet-new.ts +36 -0
- package/src/cli/fleet-preflight.ts +111 -0
- package/src/cli/fleet-validate.ts +30 -0
- package/src/cli/fleet.ts +173 -335
- package/src/cli/index.ts +3 -1
- package/src/cli/library.ts +190 -0
- package/src/cli/share.ts +13 -1
- package/src/cli/shared.ts +1 -0
- package/src/cli/targets.ts +4 -1
- package/src/cli/usage.ts +111 -19
- package/src/cli/validate-model.ts +60 -5
- package/src/core/bus-events.ts +29 -0
- package/src/core/commit-attribution.ts +4 -4
- package/src/core/config.ts +130 -0
- package/src/core/defaults.ts +81 -0
- package/src/core/path-boundary.ts +100 -0
- package/src/domains/agents/builtins/architect.md +1 -0
- package/src/domains/agents/builtins/oracle.md +33 -0
- package/src/domains/agents/catalog.ts +13 -1
- package/src/domains/agents/extension.ts +2 -11
- package/src/domains/agents/fleet-contract.ts +304 -24
- package/src/domains/agents/index.ts +14 -0
- package/src/domains/agents/recipe.ts +7 -1
- package/src/domains/agents/registry.ts +73 -5
- package/src/domains/agents/result-contract.ts +360 -15
- package/src/domains/agents/write-boundary.ts +15 -50
- package/src/domains/config/classify.ts +4 -0
- package/src/domains/context/project-rules.ts +51 -1
- package/src/domains/dispatch/active-route-planner.ts +14 -0
- package/src/domains/dispatch/assignment-reconcile.ts +22 -5
- package/src/domains/dispatch/assignment-store.ts +151 -14
- package/src/domains/dispatch/backoff.ts +2 -1
- package/src/domains/dispatch/capability-match.ts +1 -0
- package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
- package/src/domains/dispatch/contract.ts +48 -0
- package/src/domains/dispatch/delegation-plan.ts +164 -0
- package/src/domains/dispatch/execution-plan.ts +76 -5
- package/src/domains/dispatch/execution-role.ts +12 -2
- package/src/domains/dispatch/execution-scheduler.ts +183 -67
- package/src/domains/dispatch/extension.ts +412 -74
- package/src/domains/dispatch/fleet-gate.ts +14 -0
- package/src/domains/dispatch/fleet-plan.ts +63 -3
- package/src/domains/dispatch/fleet-run.ts +791 -0
- package/src/domains/dispatch/gate-role-prompts.ts +47 -0
- package/src/domains/dispatch/host-verification.ts +178 -0
- package/src/domains/dispatch/index.ts +41 -1
- package/src/domains/dispatch/intent-requirements.ts +40 -0
- package/src/domains/dispatch/intent.ts +235 -0
- package/src/domains/dispatch/path-scope.ts +370 -0
- package/src/domains/dispatch/receipt-integrity.ts +9 -4
- package/src/domains/dispatch/state.ts +36 -3
- package/src/domains/dispatch/types.ts +64 -12
- package/src/domains/dispatch/validation.ts +69 -6
- package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
- package/src/domains/dispatch/write-boundary.ts +201 -22
- package/src/domains/eval/metrics/evidence.ts +79 -2
- package/src/domains/eval/runners/clio-run.ts +12 -2
- package/src/domains/evidence/build.ts +69 -11
- package/src/domains/evidence/index.ts +21 -0
- package/src/domains/evidence/provenance.ts +46 -11
- package/src/domains/evidence/trust-projection.ts +274 -0
- package/src/domains/evidence/trust-status.ts +155 -18
- package/src/domains/evidence/types.ts +4 -0
- package/src/domains/extensions/discovery.ts +88 -1
- package/src/domains/extensions/resources.ts +20 -8
- package/src/domains/extensions/state.ts +6 -2
- package/src/domains/extensions/types.ts +4 -1
- package/src/domains/lifecycle/doctor.ts +140 -1
- package/src/domains/middleware/index.ts +15 -0
- package/src/domains/middleware/watchdog.ts +281 -0
- package/src/domains/observability/contract.ts +3 -1
- package/src/domains/observability/cost.ts +12 -1
- package/src/domains/observability/extension.ts +2 -2
- package/src/domains/observability/index.ts +10 -0
- package/src/domains/observability/out-of-turn-usage.ts +223 -0
- package/src/domains/prompts/contract.ts +3 -5
- package/src/domains/providers/extension.ts +30 -2
- package/src/domains/resources/common-loader.ts +3 -0
- package/src/domains/resources/index.ts +20 -0
- package/src/domains/resources/library.ts +326 -0
- package/src/domains/resources/prompts/loader.ts +119 -14
- package/src/domains/resources/skills/marketplace.ts +37 -12
- package/src/domains/safety/policy-engine.ts +5 -5
- package/src/domains/safety/run-effects.ts +64 -1
- package/src/domains/safety/scope.ts +7 -12
- package/src/domains/session/handoff.ts +629 -0
- package/src/domains/share/archive.ts +67 -2
- package/src/engine/acp/server.ts +4 -1
- package/src/engine/prompt-templates.ts +18 -1
- package/src/engine/worker-runtime.ts +6 -3
- package/src/entry/orchestrator.ts +37 -0
- package/src/interactive/bus-notices.ts +26 -0
- package/src/interactive/chat-loop.ts +235 -1
- package/src/interactive/chat-renderer.ts +22 -0
- package/src/interactive/cost-overlay.ts +31 -3
- package/src/interactive/council-dispatch.ts +30 -0
- package/src/interactive/council-grid.ts +213 -0
- package/src/interactive/council.ts +99 -0
- package/src/interactive/dispatch-board.ts +311 -17
- package/src/interactive/fleet-run-preview.ts +307 -0
- package/src/interactive/footer/notifications.ts +219 -0
- package/src/interactive/handoff-round.ts +56 -0
- package/src/interactive/interactive-application.ts +43 -1
- package/src/interactive/interactive-event-projection.ts +23 -1
- package/src/interactive/interactive-slash-runtime.ts +52 -2
- package/src/interactive/interactive-subscriptions.ts +14 -2
- package/src/interactive/oracle.ts +179 -0
- package/src/interactive/overlay-ask-user-lifecycle.ts +6 -0
- package/src/interactive/overlay-general-openers.ts +190 -1
- package/src/interactive/overlay-key-routing.ts +17 -1
- package/src/interactive/overlay-lifecycle.ts +41 -1
- package/src/interactive/overlay-permission-lifecycle.ts +10 -0
- package/src/interactive/overlay-resource-openers.ts +11 -3
- package/src/interactive/overlay-session-lifecycle.ts +234 -2
- package/src/interactive/overlays/fleet-run-approval.ts +208 -0
- package/src/interactive/overlays/handoff-review.ts +185 -0
- package/src/interactive/overlays/library-install-confirm.ts +151 -0
- package/src/interactive/overlays/list-overlay.ts +168 -2
- package/src/interactive/overlays/settings.ts +221 -30
- package/src/interactive/overlays/side-question.ts +139 -0
- package/src/interactive/overlays/skills-hub.ts +401 -15
- package/src/interactive/side-question.ts +171 -0
- package/src/interactive/slash-commands.ts +439 -7
- package/src/interactive/slash-spec.ts +19 -6
- package/src/interactive/theme/tokens.ts +30 -0
- package/src/interactive/turn-middleware.ts +15 -1
- package/src/interactive/view/artifacts.ts +42 -9
- package/src/interactive/view/view-overlay.ts +15 -3
- package/src/interactive/watchdog-run.ts +75 -0
- package/src/interactive/worker-receipts.ts +14 -2
- package/src/interactive/worker-share.ts +56 -1
- package/src/interactive/worker-stream.ts +15 -0
- package/src/tools/bootstrap.ts +3 -0
- package/src/tools/compete-worktrees.ts +13 -79
- package/src/tools/dispatch-admission.ts +251 -18
- package/src/tools/dispatch-arguments.ts +84 -1
- package/src/tools/dispatch-plan.ts +165 -6
- package/src/tools/dispatch-runner.ts +364 -23
- package/src/tools/dispatch-types.ts +20 -1
- package/src/tools/dispatch.ts +72 -2
- package/src/tools/monitor.ts +29 -0
- package/src/tools/profiles.ts +18 -4
- package/src/tools/task-worktree.ts +238 -0
- package/src/tools/verify/authoring.ts +61 -1
- package/src/tools/verify/scripts.ts +62 -0
- package/src/tools/worker-evidence.ts +21 -14
- package/src/worker/spec-contract.ts +3 -1
- package/dist/chunk-HC4CLZ2Y.js +0 -68
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { listInstalledExtensions } from "./state.js";
|
|
4
4
|
import type { ExtensionResourceKind, ExtensionResourceRoot } from "./types.js";
|
|
@@ -7,8 +7,24 @@ export function extensionResourcePath(rootPath: string, resourcePath: string): s
|
|
|
7
7
|
const root = path.resolve(rootPath);
|
|
8
8
|
const full = path.resolve(root, resourcePath);
|
|
9
9
|
const relative = path.relative(root, full);
|
|
10
|
-
if (relative === "" ||
|
|
11
|
-
|
|
10
|
+
if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
const rootReal = realpathSync(root);
|
|
14
|
+
const fullReal = realpathSync(full);
|
|
15
|
+
const canonicalRelative = path.relative(rootReal, fullReal);
|
|
16
|
+
if (
|
|
17
|
+
canonicalRelative === ".." ||
|
|
18
|
+
canonicalRelative.startsWith(`..${path.sep}`) ||
|
|
19
|
+
path.isAbsolute(canonicalRelative)
|
|
20
|
+
) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (!lstatSync(full).isDirectory()) return null;
|
|
24
|
+
return full;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
12
28
|
}
|
|
13
29
|
|
|
14
30
|
export function enabledExtensionResourceRoots(
|
|
@@ -22,15 +38,11 @@ export function enabledExtensionResourceRoots(
|
|
|
22
38
|
if (!rel) continue;
|
|
23
39
|
const full = extensionResourcePath(entry.rootPath, rel);
|
|
24
40
|
if (!full) continue;
|
|
25
|
-
try {
|
|
26
|
-
if (!statSync(full).isDirectory()) continue;
|
|
27
|
-
} catch {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
41
|
roots.push({
|
|
31
42
|
id: entry.id,
|
|
32
43
|
scope: entry.scope,
|
|
33
44
|
path: full,
|
|
45
|
+
rootPath: entry.rootPath,
|
|
34
46
|
source: `extension:${entry.scope}:${entry.id}`,
|
|
35
47
|
});
|
|
36
48
|
}
|
|
@@ -129,7 +129,7 @@ export function installExtension(sourcePath: string, options: ExtensionInstallOp
|
|
|
129
129
|
const cwd = options.cwd ?? process.cwd();
|
|
130
130
|
const source = path.resolve(sourcePath);
|
|
131
131
|
const candidate = loadManifestFromRoot(source);
|
|
132
|
-
if (!candidate.manifest) return { diagnostics: candidate.diagnostics };
|
|
132
|
+
if (!candidate.manifest || !candidate.valid) return { diagnostics: candidate.diagnostics };
|
|
133
133
|
const targetRoot = path.join(extensionBaseDir(scope, cwd), candidate.manifest.id);
|
|
134
134
|
if (existsSync(targetRoot)) {
|
|
135
135
|
if (!options.force) {
|
|
@@ -152,7 +152,11 @@ export function installExtension(sourcePath: string, options: ExtensionInstallOp
|
|
|
152
152
|
rmSync(backupRoot, { recursive: true, force: true });
|
|
153
153
|
cpSync(source, stagingRoot, {
|
|
154
154
|
recursive: true,
|
|
155
|
-
|
|
155
|
+
// `state.json` beside the installed extension directories belongs to
|
|
156
|
+
// Clio's extension manager. An extension may legitimately ship nested
|
|
157
|
+
// resources with the same basename (for example a project-state JSON
|
|
158
|
+
// template), so exclude only a bookkeeping file at the package root.
|
|
159
|
+
filter: (src) => path.relative(source, src) !== "state.json",
|
|
156
160
|
});
|
|
157
161
|
if (existsSync(targetRoot)) {
|
|
158
162
|
renameSync(targetRoot, backupRoot);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export type ExtensionScope = "user" | "project";
|
|
2
|
-
export type ExtensionResourceKind = "skills" | "prompts" | "themes";
|
|
2
|
+
export type ExtensionResourceKind = "skills" | "prompts" | "agents" | "fleets" | "themes";
|
|
3
3
|
|
|
4
4
|
export interface ExtensionManifestResources {
|
|
5
5
|
skills?: string;
|
|
6
6
|
prompts?: string;
|
|
7
|
+
agents?: string;
|
|
8
|
+
fleets?: string;
|
|
7
9
|
themes?: string;
|
|
8
10
|
}
|
|
9
11
|
|
|
@@ -52,6 +54,7 @@ export interface ExtensionResourceRoot {
|
|
|
52
54
|
id: string;
|
|
53
55
|
scope: ExtensionScope;
|
|
54
56
|
path: string;
|
|
57
|
+
rootPath: string;
|
|
55
58
|
source: string;
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -5,8 +5,11 @@ import { initializeClioHome } from "../../core/init.js";
|
|
|
5
5
|
import { resolveClioDirs } from "../../core/xdg.js";
|
|
6
6
|
import { readSessionFileEntries, type SessionJsonlWarning } from "../../engine/session.js";
|
|
7
7
|
import { detectInteropAgents, interopAgentKind, resolveOnPath } from "../interop/index.js";
|
|
8
|
-
import { openAuthStorage } from "../providers/auth/index.js";
|
|
8
|
+
import { openAuthStorage, resolveAuthTarget, targetRequiresAuth } from "../providers/auth/index.js";
|
|
9
|
+
import { credentialsPresent } from "../providers/credentials.js";
|
|
9
10
|
import { fingerprintNativeRuntime } from "../providers/probe/fingerprint.js";
|
|
11
|
+
import type { ProbeContext, ProbeResult, RuntimeDescriptor } from "../providers/types/runtime-descriptor.js";
|
|
12
|
+
import type { TargetDescriptor } from "../providers/types/target-descriptor.js";
|
|
10
13
|
import { loadSkills, type SkillSource } from "../resources/skills/loader.js";
|
|
11
14
|
import { readStateInfoResult } from "./state.js";
|
|
12
15
|
import { getVersionInfo } from "./version.js";
|
|
@@ -534,3 +537,139 @@ export async function runDoctorRuntimeChecks(): Promise<DoctorFinding[]> {
|
|
|
534
537
|
);
|
|
535
538
|
return results.filter((finding): finding is DoctorFinding => finding !== null);
|
|
536
539
|
}
|
|
540
|
+
|
|
541
|
+
/** One configured model pointer at a target, named the way settings.yaml spells it. */
|
|
542
|
+
interface ConfiguredModelRole {
|
|
543
|
+
role: string;
|
|
544
|
+
model: string;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function configuredModelRoles(
|
|
548
|
+
settings: ReturnType<typeof readSettings>,
|
|
549
|
+
target: TargetDescriptor,
|
|
550
|
+
): ConfiguredModelRole[] {
|
|
551
|
+
const roles: ConfiguredModelRole[] = [];
|
|
552
|
+
if (target.defaultModel) roles.push({ role: "defaultModel", model: target.defaultModel });
|
|
553
|
+
if (settings.orchestrator.target === target.id && settings.orchestrator.model) {
|
|
554
|
+
roles.push({ role: "orchestrator.model", model: settings.orchestrator.model });
|
|
555
|
+
}
|
|
556
|
+
if (settings.background.target === target.id && settings.background.model) {
|
|
557
|
+
roles.push({ role: "background.model", model: settings.background.model });
|
|
558
|
+
}
|
|
559
|
+
if (settings.workers.default.target === target.id && settings.workers.default.model) {
|
|
560
|
+
roles.push({ role: "workers.default.model", model: settings.workers.default.model });
|
|
561
|
+
}
|
|
562
|
+
return roles;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Per-request budget for the model sweep. A target that does not answer in
|
|
567
|
+
* this time falls back to the list configure recorded, so a black-holed
|
|
568
|
+
* remote costs doctor a bounded wait rather than the probe's full timeout.
|
|
569
|
+
*/
|
|
570
|
+
const DOCTOR_MODEL_PROBE_TIMEOUT_MS = 2_500;
|
|
571
|
+
|
|
572
|
+
async function doctorProbeContext(target: TargetDescriptor, runtime: RuntimeDescriptor): Promise<ProbeContext> {
|
|
573
|
+
const ctx: ProbeContext = { credentialsPresent: credentialsPresent(), httpTimeoutMs: DOCTOR_MODEL_PROBE_TIMEOUT_MS };
|
|
574
|
+
if (!targetRequiresAuth(target, runtime)) return ctx;
|
|
575
|
+
try {
|
|
576
|
+
const resolution = await openAuthStorage().resolveForTarget(resolveAuthTarget(target, runtime), {
|
|
577
|
+
includeFallback: false,
|
|
578
|
+
});
|
|
579
|
+
if (resolution.apiKey) ctx.authToken = resolution.apiKey;
|
|
580
|
+
} catch {
|
|
581
|
+
// The probe reports its own missing-auth failure; the wireModels fallback covers the check.
|
|
582
|
+
}
|
|
583
|
+
return ctx;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** What the target advertises now, or null when it could not be asked. */
|
|
587
|
+
async function probeAdvertisedModels(
|
|
588
|
+
target: TargetDescriptor,
|
|
589
|
+
runtime: RuntimeDescriptor,
|
|
590
|
+
): Promise<{ advertised: string[]; resident: string[] } | null> {
|
|
591
|
+
if (runtime.kind !== "http" || typeof runtime.probe !== "function" || !target.url) return null;
|
|
592
|
+
let probe: ProbeResult;
|
|
593
|
+
try {
|
|
594
|
+
probe = await runtime.probe(target, await doctorProbeContext(target, runtime));
|
|
595
|
+
} catch {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
if (!probe.ok || !probe.models || probe.models.length === 0) return null;
|
|
599
|
+
const advertised = [...probe.models];
|
|
600
|
+
const resident: string[] = [];
|
|
601
|
+
for (const [id, status] of Object.entries(probe.modelStates ?? {})) {
|
|
602
|
+
if (!advertised.includes(id)) advertised.push(id);
|
|
603
|
+
if (status.state === "loaded" || status.state === "loading") resident.push(id);
|
|
604
|
+
}
|
|
605
|
+
return { advertised, resident };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Model sweep: every model pointer settings.yaml aims at a target with no
|
|
610
|
+
* static catalog is checked against what that target advertises. The live
|
|
611
|
+
* list wins when the target answers; the `wireModels` list configure recorded
|
|
612
|
+
* stands in when it does not, so an unreachable server still gets the
|
|
613
|
+
* placeholder id it was saved with called out. A target with neither list is
|
|
614
|
+
* a WARN, not a pass, because nothing was verified. Network-bound like the
|
|
615
|
+
* runtime sweep, so it is not part of the synchronous `runDoctor()` core.
|
|
616
|
+
*/
|
|
617
|
+
export async function runDoctorModelChecks(): Promise<DoctorFinding[]> {
|
|
618
|
+
let settings: ReturnType<typeof readSettings>;
|
|
619
|
+
try {
|
|
620
|
+
settings = readSettings();
|
|
621
|
+
} catch {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
if (settings.targets.length === 0) return [];
|
|
625
|
+
// Every runtime descriptor and the model catalog sit behind these two
|
|
626
|
+
// imports. They are loaded here, not at module load, so a doctor run on a
|
|
627
|
+
// home with no targets (the `--fix` seed every CLI test starts from) pays
|
|
628
|
+
// nothing for a sweep it has nothing to do.
|
|
629
|
+
const [{ getRuntimeRegistry }, { registerBuiltinRuntimes }, { listKnownModelsForRuntime }] = await Promise.all([
|
|
630
|
+
import("../providers/registry.js"),
|
|
631
|
+
import("../providers/runtimes/builtins.js"),
|
|
632
|
+
import("../providers/support.js"),
|
|
633
|
+
]);
|
|
634
|
+
const registry = getRuntimeRegistry();
|
|
635
|
+
// doctor never loads the providers domain, so the registry is empty here
|
|
636
|
+
// unless another command in this process filled it.
|
|
637
|
+
if (registry.list().length === 0) registerBuiltinRuntimes(registry);
|
|
638
|
+
const results = await Promise.all(
|
|
639
|
+
settings.targets.map(async (target): Promise<DoctorFinding | null> => {
|
|
640
|
+
const runtime = registry.get(target.runtime);
|
|
641
|
+
if (!runtime) return null;
|
|
642
|
+
// Cloud runtimes are validated against their catalog at configure time.
|
|
643
|
+
if (listKnownModelsForRuntime(runtime.id).length > 0) return null;
|
|
644
|
+
const roles = configuredModelRoles(settings, target);
|
|
645
|
+
if (roles.length === 0) return null;
|
|
646
|
+
const live = await probeAdvertisedModels(target, runtime);
|
|
647
|
+
const recorded = target.wireModels ?? [];
|
|
648
|
+
if (live === null && recorded.length === 0) {
|
|
649
|
+
return {
|
|
650
|
+
ok: true,
|
|
651
|
+
level: "warn",
|
|
652
|
+
name: `model ${target.id}`,
|
|
653
|
+
detail: `${roles.map((entry) => `${entry.role} '${entry.model}'`).join(", ")} could not be verified: the target did not answer and configure recorded no model list; run \`clio-coder targets --probe\` once it is up`,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const advertised = live ? live.advertised : recorded;
|
|
657
|
+
const source = live ? `advertised by ${target.url ?? target.id} now` : "recorded by configure at last save";
|
|
658
|
+
const missing = roles.filter((entry) => !advertised.includes(entry.model));
|
|
659
|
+
if (missing.length === 0) {
|
|
660
|
+
return {
|
|
661
|
+
ok: true,
|
|
662
|
+
name: `model ${target.id}`,
|
|
663
|
+
detail: `${roles.map((entry) => `${entry.role} '${entry.model}'`).join(", ")} ${source}`,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const resident = live && live.resident.length > 0 ? live.resident.join(", ") : live ? "none" : "unknown";
|
|
667
|
+
return {
|
|
668
|
+
ok: false,
|
|
669
|
+
name: `model ${target.id}`,
|
|
670
|
+
detail: `${missing.map((entry) => `${entry.role} '${entry.model}'`).join(", ")} not ${source} (${advertised.length} ids). Resident instances: ${resident}. Re-run \`clio-coder configure --id ${target.id} --model <advertised id>\`; \`clio-coder targets --probe\` lists them`,
|
|
671
|
+
};
|
|
672
|
+
}),
|
|
673
|
+
);
|
|
674
|
+
return results.filter((finding): finding is DoctorFinding => finding !== null);
|
|
675
|
+
}
|
|
@@ -152,3 +152,18 @@ export type {
|
|
|
152
152
|
MiddlewareValidationIssue,
|
|
153
153
|
} from "./validate.js";
|
|
154
154
|
export { validateMiddlewareEffect, validateMiddlewareRule } from "./validate.js";
|
|
155
|
+
export {
|
|
156
|
+
coalesceTurnDiff,
|
|
157
|
+
createWatchdogRegistration,
|
|
158
|
+
WATCHDOG_DIFF_MAX_BYTES,
|
|
159
|
+
WATCHDOG_PATHS_MAX,
|
|
160
|
+
WATCHDOG_REGISTRATION_ID,
|
|
161
|
+
WATCHDOG_TASK,
|
|
162
|
+
WATCHDOG_TRUNCATION_MARKER,
|
|
163
|
+
type WatchdogDeps,
|
|
164
|
+
type WatchdogRegistration,
|
|
165
|
+
type WatchdogSettingsView,
|
|
166
|
+
type WatchdogTrigger,
|
|
167
|
+
type WatchdogTriggerReason,
|
|
168
|
+
watchdogBriefing,
|
|
169
|
+
} from "./watchdog.js";
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The opt-in turn-end watchdog.
|
|
3
|
+
*
|
|
4
|
+
* A long agentic turn can leave the tree in a state nobody looked at: the
|
|
5
|
+
* model reports success, the operator reads the report, and the drift between
|
|
6
|
+
* what the board asked for and what the diff actually did stays invisible
|
|
7
|
+
* until someone reads the diff themselves. When `watchdog.enabled` is on, a
|
|
8
|
+
* turn that changed the tree is handed to one read-only verifier run briefed
|
|
9
|
+
* with the turn's coalesced diff and the board's current scope. Its blockers
|
|
10
|
+
* become one transcript notice and nothing else.
|
|
11
|
+
*
|
|
12
|
+
* What it deliberately does not do: it never auto-follows, never carries the
|
|
13
|
+
* turn onward, never queues a turn, and never mutates. A watchdog that could
|
|
14
|
+
* start work would be a second agent nobody asked for; this one can only tell
|
|
15
|
+
* the operator what it saw.
|
|
16
|
+
*
|
|
17
|
+
* The registration itself emits no middleware effects. It owns the decision of
|
|
18
|
+
* when to fire and the coalesced diff it fires with; the caller owns the
|
|
19
|
+
* dispatch and the notice.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { ToolNames } from "../../core/tool-names.js";
|
|
23
|
+
import type { MiddlewareHookRegistration } from "./runtime.js";
|
|
24
|
+
import type { MiddlewareEffect, MiddlewareHookInput } from "./types.js";
|
|
25
|
+
|
|
26
|
+
export const WATCHDOG_REGISTRATION_ID = "observer.watchdog";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Bound on the coalesced diff a watchdog briefing carries, in UTF-8 bytes. The
|
|
30
|
+
* run is meant to be cheap enough to spend on every mutating turn, so it is
|
|
31
|
+
* given the shape of the change rather than every byte of it.
|
|
32
|
+
*/
|
|
33
|
+
export const WATCHDOG_DIFF_MAX_BYTES = 12 * 1024;
|
|
34
|
+
|
|
35
|
+
/** Bound on how many mutated paths the briefing names before it summarizes the rest. */
|
|
36
|
+
export const WATCHDOG_PATHS_MAX = 40;
|
|
37
|
+
|
|
38
|
+
export const WATCHDOG_TRUNCATION_MARKER = "\n[diff truncated]";
|
|
39
|
+
|
|
40
|
+
/** Tools whose successful results carry a file diff worth coalescing. */
|
|
41
|
+
const MUTATING_TOOL_NAMES = new Set<string>([ToolNames.Write, ToolNames.Edit, ToolNames.Artifact]);
|
|
42
|
+
|
|
43
|
+
const NO_EFFECTS: ReadonlyArray<MiddlewareEffect> = [];
|
|
44
|
+
|
|
45
|
+
export type WatchdogTriggerReason = "turn_end" | "cadence";
|
|
46
|
+
|
|
47
|
+
export interface WatchdogTrigger {
|
|
48
|
+
reason: WatchdogTriggerReason;
|
|
49
|
+
/** The turn's coalesced diff, one block per mutated path in first-touch order. */
|
|
50
|
+
diff: string;
|
|
51
|
+
/** Paths the turn mutated, in first-touch order. */
|
|
52
|
+
paths: ReadonlyArray<string>;
|
|
53
|
+
/** The board's current scope, or null when the session has no task board. */
|
|
54
|
+
scope: string | null;
|
|
55
|
+
/** Tool calls seen in this turn when the trigger fired. */
|
|
56
|
+
toolCalls: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The live settings view the registration reads on every boundary, so a hot reload lands immediately. */
|
|
60
|
+
export interface WatchdogSettingsView {
|
|
61
|
+
enabled: boolean;
|
|
62
|
+
target?: string;
|
|
63
|
+
cadenceToolCalls?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface WatchdogDeps {
|
|
67
|
+
getSettings: () => Readonly<WatchdogSettingsView>;
|
|
68
|
+
/** The board's current scope line, or null when there is no board. */
|
|
69
|
+
getScope?: () => string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Dispatch the watchdog run and deliver whatever it found. Resolves when the
|
|
72
|
+
* run has settled; the registration awaits only to know the in-flight slot is
|
|
73
|
+
* free again, never to hold the turn.
|
|
74
|
+
*/
|
|
75
|
+
run: (trigger: WatchdogTrigger) => Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* False on a surface that must never fire the watchdog whatever the setting
|
|
78
|
+
* says. Headless and ACP runs pass false: neither has an operator watching a
|
|
79
|
+
* transcript, so a notice they cannot read is a worker run spent on nothing.
|
|
80
|
+
*/
|
|
81
|
+
firesOnThisSurface?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface WatchdogRegistration extends MiddlewareHookRegistration {
|
|
85
|
+
/** Triggers dropped because a watchdog run was already in flight. */
|
|
86
|
+
droppedTriggers(): number;
|
|
87
|
+
/** True while a watchdog run is outstanding. */
|
|
88
|
+
runInFlight(): boolean;
|
|
89
|
+
/** Resolves once no watchdog run is outstanding. */
|
|
90
|
+
whenIdle(): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stringValue(value: unknown): string | null {
|
|
94
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function firstPath(details: Readonly<Record<string, unknown>> | undefined): string | null {
|
|
98
|
+
const paths = details?.paths;
|
|
99
|
+
if (!Array.isArray(paths)) return null;
|
|
100
|
+
for (const entry of paths) {
|
|
101
|
+
const path = stringValue(entry);
|
|
102
|
+
if (path !== null) return path;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Coalesce the per-call diffs of one turn into one diff.
|
|
109
|
+
*
|
|
110
|
+
* Per-path last-write-wins, in first-touch order. A turn that edited one file
|
|
111
|
+
* six times produced six diffs against six different base contents; replaying
|
|
112
|
+
* all of them tells a reviewer the history of the edit rather than its result,
|
|
113
|
+
* and the result is what the reviewer is being asked about.
|
|
114
|
+
*/
|
|
115
|
+
export function coalesceTurnDiff(
|
|
116
|
+
byPath: ReadonlyMap<string, string>,
|
|
117
|
+
maxBytes: number = WATCHDOG_DIFF_MAX_BYTES,
|
|
118
|
+
): string {
|
|
119
|
+
const blocks: string[] = [];
|
|
120
|
+
let index = 0;
|
|
121
|
+
for (const [path, diff] of byPath) {
|
|
122
|
+
if (index >= WATCHDOG_PATHS_MAX) {
|
|
123
|
+
blocks.push(`[${byPath.size - WATCHDOG_PATHS_MAX} more changed path(s) omitted]`);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
blocks.push(`--- ${path}\n${diff}`);
|
|
127
|
+
index += 1;
|
|
128
|
+
}
|
|
129
|
+
const whole = blocks.join("\n\n");
|
|
130
|
+
if (Buffer.byteLength(whole, "utf8") <= maxBytes) return whole;
|
|
131
|
+
const room = Math.max(0, maxBytes - Buffer.byteLength(WATCHDOG_TRUNCATION_MARKER, "utf8"));
|
|
132
|
+
const buffer = Buffer.from(whole, "utf8");
|
|
133
|
+
let cut = Math.min(room, buffer.byteLength);
|
|
134
|
+
while (cut > 0) {
|
|
135
|
+
const next = buffer[cut];
|
|
136
|
+
if (next === undefined || (next & 0xc0) !== 0x80) break;
|
|
137
|
+
cut -= 1;
|
|
138
|
+
}
|
|
139
|
+
return `${buffer.subarray(0, cut).toString("utf8")}${WATCHDOG_TRUNCATION_MARKER}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The bounded, fixed task every watchdog run is given. It never varies with the diff. */
|
|
143
|
+
export const WATCHDOG_TASK =
|
|
144
|
+
"Review the coalesced diff in the briefing against the scope it names. Report one check per concern: " +
|
|
145
|
+
"a change outside the stated scope, a change the scope required that the diff does not make, or a defect " +
|
|
146
|
+
"visible in the diff itself. Run no commands and change nothing.";
|
|
147
|
+
|
|
148
|
+
/** The briefing body a watchdog run receives. Data, never instructions. */
|
|
149
|
+
export function watchdogBriefing(trigger: WatchdogTrigger): string {
|
|
150
|
+
const scope = trigger.scope === null ? "(no task board; scope is unstated)" : trigger.scope;
|
|
151
|
+
const paths = trigger.paths.length === 0 ? "(none)" : trigger.paths.join("\n");
|
|
152
|
+
return [
|
|
153
|
+
"## Current scope",
|
|
154
|
+
scope,
|
|
155
|
+
"",
|
|
156
|
+
"## Changed paths",
|
|
157
|
+
paths,
|
|
158
|
+
"",
|
|
159
|
+
"## Coalesced diff for this turn",
|
|
160
|
+
trigger.diff,
|
|
161
|
+
].join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build the registration. It is hooked on `after_tool` and `turn_end`:
|
|
166
|
+
* `after_tool` is where the diffs and the tool count come from, and `turn_end`
|
|
167
|
+
* is the boundary a mutating turn fires on.
|
|
168
|
+
*/
|
|
169
|
+
export function createWatchdogRegistration(deps: WatchdogDeps): WatchdogRegistration {
|
|
170
|
+
const diffByPath = new Map<string, string>();
|
|
171
|
+
let toolCalls = 0;
|
|
172
|
+
let dropped = 0;
|
|
173
|
+
let inFlight = false;
|
|
174
|
+
let outstanding: Promise<void> = Promise.resolve();
|
|
175
|
+
|
|
176
|
+
const active = (): WatchdogSettingsView | null => {
|
|
177
|
+
if (deps.firesOnThisSurface === false) return null;
|
|
178
|
+
let settings: Readonly<WatchdogSettingsView>;
|
|
179
|
+
try {
|
|
180
|
+
settings = deps.getSettings();
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return settings.enabled ? settings : null;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const resetTurn = (): void => {
|
|
188
|
+
diffByPath.clear();
|
|
189
|
+
toolCalls = 0;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const fire = (reason: WatchdogTriggerReason): void => {
|
|
193
|
+
if (diffByPath.size === 0) return;
|
|
194
|
+
// One run at a time. A watchdog slower than the turns that trigger it must
|
|
195
|
+
// not queue: a queued review describes a tree that has already moved on,
|
|
196
|
+
// and a backlog of them would outlive the session. The drop is counted so
|
|
197
|
+
// a starved cadence and a quiet one stay distinguishable.
|
|
198
|
+
if (inFlight) {
|
|
199
|
+
dropped += 1;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const trigger: WatchdogTrigger = {
|
|
203
|
+
reason,
|
|
204
|
+
diff: coalesceTurnDiff(diffByPath),
|
|
205
|
+
paths: [...diffByPath.keys()].slice(0, WATCHDOG_PATHS_MAX),
|
|
206
|
+
scope: scopeNow(),
|
|
207
|
+
toolCalls,
|
|
208
|
+
};
|
|
209
|
+
inFlight = true;
|
|
210
|
+
// Started synchronously, so the in-flight slot is genuinely taken before
|
|
211
|
+
// this hook returns. Deferring the start to a microtask would let two
|
|
212
|
+
// triggers in the same tool batch both find the slot free.
|
|
213
|
+
let started: Promise<void>;
|
|
214
|
+
try {
|
|
215
|
+
started = deps.run(trigger);
|
|
216
|
+
} catch {
|
|
217
|
+
inFlight = false;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
outstanding = started
|
|
221
|
+
.catch(() => {
|
|
222
|
+
// A failed watchdog run is not a failed turn. The run's own reporting
|
|
223
|
+
// path already told the operator whatever it could.
|
|
224
|
+
})
|
|
225
|
+
.finally(() => {
|
|
226
|
+
inFlight = false;
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const scopeNow = (): string | null => {
|
|
231
|
+
try {
|
|
232
|
+
return deps.getScope?.() ?? null;
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
id: WATCHDOG_REGISTRATION_ID,
|
|
240
|
+
description: "review a mutating turn's coalesced diff against the board scope with a read-only verifier run",
|
|
241
|
+
hooks: ["after_tool", "turn_end"],
|
|
242
|
+
evaluate(input: MiddlewareHookInput): ReadonlyArray<MiddlewareEffect> {
|
|
243
|
+
const settings = active();
|
|
244
|
+
if (settings === null) {
|
|
245
|
+
// Still clear turn state so a mid-session disable cannot leave a diff
|
|
246
|
+
// behind that a later enable would fire on.
|
|
247
|
+
if (input.hook === "turn_end") resetTurn();
|
|
248
|
+
return NO_EFFECTS;
|
|
249
|
+
}
|
|
250
|
+
if (input.hook === "after_tool") {
|
|
251
|
+
toolCalls += 1;
|
|
252
|
+
recordMutation(input);
|
|
253
|
+
const cadence = settings.cadenceToolCalls;
|
|
254
|
+
// Mid-turn cadence keeps accumulating: turn end still reviews the whole
|
|
255
|
+
// turn, not just what landed after the last cadence firing.
|
|
256
|
+
if (cadence !== undefined && cadence >= 1 && toolCalls % cadence === 0) fire("cadence");
|
|
257
|
+
return NO_EFFECTS;
|
|
258
|
+
}
|
|
259
|
+
if (input.hook !== "turn_end") return NO_EFFECTS;
|
|
260
|
+
// A turn with no file mutations never fires. A middleware continuation
|
|
261
|
+
// re-evaluates turn_end with the accumulator already cleared, so it
|
|
262
|
+
// cannot fire a second run for the same change either.
|
|
263
|
+
fire("turn_end");
|
|
264
|
+
resetTurn();
|
|
265
|
+
return NO_EFFECTS;
|
|
266
|
+
},
|
|
267
|
+
droppedTriggers: () => dropped,
|
|
268
|
+
runInFlight: () => inFlight,
|
|
269
|
+
whenIdle: () => outstanding,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
function recordMutation(input: MiddlewareHookInput): void {
|
|
273
|
+
if (input.metadata?.resultKind !== "ok") return;
|
|
274
|
+
if (input.toolName === undefined || !MUTATING_TOOL_NAMES.has(input.toolName)) return;
|
|
275
|
+
const diff = stringValue(input.toolResultDetails?.diff);
|
|
276
|
+
if (diff === null) return;
|
|
277
|
+
// Map.set on an existing key keeps its insertion position, so the newest
|
|
278
|
+
// diff for a path wins while the path keeps its first-touch order.
|
|
279
|
+
diffByPath.set(firstPath(input.toolResultDetails) ?? input.toolName, diff);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { ResponseModelIdObservationCounts } from "../../core/response-model
|
|
|
2
2
|
import type { TargetStatus } from "../providers/contract.js";
|
|
3
3
|
import type { CostProvenance } from "../providers/index.js";
|
|
4
4
|
import type { AccountabilitySummary } from "./accountability.js";
|
|
5
|
-
import type { CostAggregate, CostEntry, UsageBreakdown } from "./cost.js";
|
|
5
|
+
import type { CostAggregate, CostEntry, CostEntryLabel, UsageBreakdown } from "./cost.js";
|
|
6
6
|
import type { MetricsView } from "./metrics.js";
|
|
7
7
|
import type { TelemetrySnapshot } from "./telemetry.js";
|
|
8
8
|
import type { SessionTurnTrace } from "./trace-store.js";
|
|
@@ -142,6 +142,8 @@ export interface ObservabilityContract {
|
|
|
142
142
|
requestedModelIds: ReadonlyArray<string>;
|
|
143
143
|
responseModelIdObservationCounts: Readonly<ResponseModelIdObservationCounts>;
|
|
144
144
|
},
|
|
145
|
+
/** Marks a priced call that was not an ordinary turn, such as a `/btw` side question. */
|
|
146
|
+
label?: CostEntryLabel,
|
|
145
147
|
): void;
|
|
146
148
|
/** Record final output token throughput for one completed assistant stream. */
|
|
147
149
|
recordTokenThroughput(snapshot: TokenThroughputSnapshot): void;
|
|
@@ -111,6 +111,13 @@ export interface UsageBreakdown {
|
|
|
111
111
|
apiCalls?: number;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* What produced a priced call, when it was not an ordinary turn. `/cost` and
|
|
116
|
+
* the usage surfaces separate these out so an operator can see that money was
|
|
117
|
+
* spent beside the session rather than inside it.
|
|
118
|
+
*/
|
|
119
|
+
export type CostEntryLabel = "side-question" | "handoff";
|
|
120
|
+
|
|
114
121
|
export interface CostEntry {
|
|
115
122
|
providerId: string;
|
|
116
123
|
attributedModelId: string;
|
|
@@ -125,6 +132,8 @@ export interface CostEntry {
|
|
|
125
132
|
cacheWrite: number;
|
|
126
133
|
reasoningTokens: number;
|
|
127
134
|
apiCalls?: number;
|
|
135
|
+
/** Absent on an ordinary turn's call. */
|
|
136
|
+
label?: CostEntryLabel;
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
export interface CostTracker {
|
|
@@ -139,6 +148,7 @@ export interface CostTracker {
|
|
|
139
148
|
requestedModelIds: ReadonlyArray<string>;
|
|
140
149
|
responseModelIdObservationCounts: Readonly<ResponseModelIdObservationCounts>;
|
|
141
150
|
},
|
|
151
|
+
label?: CostEntryLabel,
|
|
142
152
|
): number;
|
|
143
153
|
sessionTotal(): number;
|
|
144
154
|
sessionCost(): CostAggregate;
|
|
@@ -156,7 +166,7 @@ export function createCostTracker(): CostTracker {
|
|
|
156
166
|
let total = 0;
|
|
157
167
|
const totals = emptyBreakdown();
|
|
158
168
|
return {
|
|
159
|
-
accumulate(providerId, attributedModelId, tokens, usd, breakdown, costProvenance, modelIdFacts) {
|
|
169
|
+
accumulate(providerId, attributedModelId, tokens, usd, breakdown, costProvenance, modelIdFacts, label) {
|
|
160
170
|
const resolvedUsd = usd ?? 0;
|
|
161
171
|
const provenance = normalizeCostProvenance(costProvenance);
|
|
162
172
|
const input = breakdown?.input ?? 0;
|
|
@@ -186,6 +196,7 @@ export function createCostTracker(): CostTracker {
|
|
|
186
196
|
cacheWrite,
|
|
187
197
|
reasoningTokens,
|
|
188
198
|
...(apiCalls !== undefined ? { apiCalls } : {}),
|
|
199
|
+
...(label !== undefined ? { label } : {}),
|
|
189
200
|
});
|
|
190
201
|
total += resolvedUsd;
|
|
191
202
|
totals.input += input;
|
|
@@ -259,9 +259,9 @@ export function createObservabilityBundle(
|
|
|
259
259
|
latestThroughput = null;
|
|
260
260
|
projection.refresh();
|
|
261
261
|
},
|
|
262
|
-
recordTokens(providerId, attributedModelId, tokens, costUsd, breakdown, costProvenance, modelIdFacts) {
|
|
262
|
+
recordTokens(providerId, attributedModelId, tokens, costUsd, breakdown, costProvenance, modelIdFacts, label) {
|
|
263
263
|
telemetry.record("counter", "tokens.total", tokens);
|
|
264
|
-
cost.accumulate(providerId, attributedModelId, tokens, costUsd, breakdown, costProvenance, modelIdFacts);
|
|
264
|
+
cost.accumulate(providerId, attributedModelId, tokens, costUsd, breakdown, costProvenance, modelIdFacts, label);
|
|
265
265
|
projection.refresh();
|
|
266
266
|
},
|
|
267
267
|
recordSessionTurn(sessionTurn) {
|
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
type CostAggregate,
|
|
31
31
|
type CostAmount,
|
|
32
32
|
type CostEntry,
|
|
33
|
+
type CostEntryLabel,
|
|
33
34
|
costAggregateForAmount,
|
|
34
35
|
costWasMeasured,
|
|
35
36
|
emptyCostAggregate,
|
|
@@ -40,6 +41,15 @@ export type { EvidenceIndexRow } from "./evidence-index.js";
|
|
|
40
41
|
export { EVIDENCE_INDEX_FILE, MAX_EVIDENCE_INDEX_ROWS, readEvidenceIndex } from "./evidence-index.js";
|
|
41
42
|
export { ObservabilityManifest } from "./manifest.js";
|
|
42
43
|
export type { MetricsView } from "./metrics.js";
|
|
44
|
+
export type { OutOfTurnUsage, OutOfTurnUsageReadResult, OutOfTurnUsageRow } from "./out-of-turn-usage.js";
|
|
45
|
+
export {
|
|
46
|
+
appendOutOfTurnUsageRow,
|
|
47
|
+
MAX_OUT_OF_TURN_USAGE_ROWS,
|
|
48
|
+
OUT_OF_TURN_USAGE_DIR,
|
|
49
|
+
OUT_OF_TURN_USAGE_FILE,
|
|
50
|
+
outOfTurnUsagePath,
|
|
51
|
+
readOutOfTurnUsageRows,
|
|
52
|
+
} from "./out-of-turn-usage.js";
|
|
43
53
|
export type { ObservabilityProjection, ProjectionReadModel } from "./projection.js";
|
|
44
54
|
export {
|
|
45
55
|
createObservabilityProjection,
|