@iowarp/clio-coder 0.3.8 → 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 +49 -0
- package/README.md +7 -3
- package/dist/{acp-U67UHUK2.js → acp-7LOELQFP.js} +6 -6
- package/dist/{agents-YU6SGALZ.js → agents-FIBG2SHA.js} +27 -25
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-5ZPJOIVG.js → auth-OI4LIH2I.js} +11 -12
- package/dist/{builtins-C6JMZVV6.js → builtins-AD25UL3C.js} +5 -5
- package/dist/{chunk-5FR74PWO.js → chunk-2JDWVJND.js} +2 -2
- package/dist/chunk-3DPEIQKN.js +113 -0
- package/dist/{chunk-4SPRNWDE.js → chunk-3DUR4WUA.js} +15 -15
- package/dist/{chunk-VHN4MY6O.js → chunk-3MRC2YSQ.js} +2 -2
- package/dist/{chunk-5DHKRSMQ.js → chunk-3UUY7R3Z.js} +11 -7
- package/dist/{chunk-IGWKHNIQ.js → chunk-3V5AYSEQ.js} +8 -8
- package/dist/{chunk-FHJEP5SW.js → chunk-465CC7FK.js} +8 -5
- package/dist/{chunk-5WIGXA4T.js → chunk-47CMYGET.js} +111 -4
- package/dist/{chunk-A3WNZD3P.js → chunk-4H6ULJ3H.js} +67 -21
- package/dist/{chunk-TB5666IT.js → chunk-4LJX2PUC.js} +3 -3
- package/dist/{chunk-XWSF374K.js → chunk-56KB5IJP.js} +2 -2
- package/dist/{chunk-SPULKLCF.js → chunk-5DQRIYDZ.js} +2 -2
- package/dist/{chunk-2HEJ2F35.js → chunk-5HFBWUMU.js} +20 -8
- package/dist/{chunk-WNIJTQQK.js → chunk-5PVQ4SRS.js} +78 -6
- package/dist/{chunk-DYIM5TJT.js → chunk-5QKCQQ3E.js} +262 -6
- package/dist/{chunk-TYPGUK6W.js → chunk-5T7RBWN2.js} +111 -5
- package/dist/{chunk-IIZWH4XA.js → chunk-774ILSRL.js} +2 -2
- package/dist/chunk-7C6RYZGQ.js +391 -0
- package/dist/{chunk-TANS5ZJS.js → chunk-AD7Y7STJ.js} +3 -3
- package/dist/{chunk-RWSI4YD7.js → chunk-AEYBF3TB.js} +33 -12
- package/dist/{chunk-DGSYXYMX.js → chunk-AMKHQW3C.js} +2 -2
- package/dist/{chunk-VWZOAB7K.js → chunk-B5XRQOLB.js} +7 -7
- package/dist/{chunk-WXY7KU3G.js → chunk-BVDVID7E.js} +2 -2
- package/dist/{chunk-PMDBGQSJ.js → chunk-CA42X6KT.js} +2 -2
- package/dist/{chunk-HLE42MG7.js → chunk-D73KXYPF.js} +3 -3
- package/dist/{chunk-5Q2VVUKB.js → chunk-DG4M6ZUE.js} +3 -3
- package/dist/{chunk-ME6CCNFO.js → chunk-EBOC7MT3.js} +6 -6
- package/dist/{chunk-MXKJU4JB.js → chunk-ECUO3KDP.js} +48 -7
- package/dist/{chunk-26LEYJZH.js → chunk-FALJGAWU.js} +2 -2
- package/dist/{chunk-GWS3VEIW.js → chunk-FWDFM5ZU.js} +24 -3
- package/dist/{chunk-7RGZWPB6.js → chunk-GAYUJ7LE.js} +67 -13
- package/dist/{chunk-VCBR6CU7.js → chunk-HAY4ZE2P.js} +2 -2
- package/dist/{chunk-FBVTI2TJ.js → chunk-HCBCAYZU.js} +11 -130
- package/dist/{chunk-WSB3FPX7.js → chunk-HJB5IUKP.js} +32 -136
- package/dist/{chunk-J3YUBZWY.js → chunk-HKO36JWF.js} +33 -5
- package/dist/{chunk-E77JEWSD.js → chunk-HPCTNZM2.js} +6 -36
- package/dist/{chunk-FJ3H4MN5.js → chunk-IHKBWSXF.js} +2 -2
- package/dist/{chunk-NMPKI6XL.js → chunk-JEQQR47K.js} +37 -18
- package/dist/{chunk-3BINW3FP.js → chunk-KV2AOLDF.js} +24 -4
- package/dist/{chunk-YS5VLNH5.js → chunk-LXPJXFM5.js} +7 -7
- package/dist/{chunk-7RFXX52T.js → chunk-MIX5N5AC.js} +271 -41
- package/dist/{chunk-K4XHGFR5.js → chunk-MLOK6ZOS.js} +1297 -218
- package/dist/{chunk-ZNLWCMVZ.js → chunk-MV2VUEJC.js} +2 -2
- package/dist/{chunk-MVVUPGPW.js → chunk-MXHC5QYU.js} +6 -6
- package/dist/{chunk-5H3GB5BO.js → chunk-N3PBVRTZ.js} +4 -382
- package/dist/{chunk-2HFZQUHL.js → chunk-N5XKWMDW.js} +17 -7
- package/dist/{chunk-5C3AQNDW.js → chunk-NNNWO6F2.js} +124 -36
- package/dist/{chunk-GU2UIAFZ.js → chunk-NQ6UCCOD.js} +3 -3
- package/dist/{chunk-N22QMJKY.js → chunk-NZU6YDNV.js} +4 -4
- package/dist/{chunk-ZVJ5BLO2.js → chunk-O6I4CIEU.js} +151 -13
- package/dist/{chunk-XN3L4EYL.js → chunk-OEDBCISO.js} +2 -2
- package/dist/{chunk-VAWNZU7Z.js → chunk-P3JGPQFL.js} +2 -2
- package/dist/{chunk-IJ7RPIYJ.js → chunk-PNY46YEY.js} +20 -3
- package/dist/{chunk-U6MBIEMB.js → chunk-PZ4I4JE2.js} +56 -35
- package/dist/{chunk-GPIEI3LY.js → chunk-QQ7EKM72.js} +2 -2
- package/dist/{chunk-WLFILSD5.js → chunk-R7LNVMCS.js} +66 -28
- package/dist/{chunk-GOXNB3AO.js → chunk-RAPCMZL4.js} +75 -4
- package/dist/chunk-RKKLTLYB.js +45 -0
- package/dist/{chunk-TT36MB5S.js → chunk-RKRLDWD3.js} +3 -1
- package/dist/{chunk-TTHACPOM.js → chunk-S4COXYBG.js} +456 -18
- package/dist/{chunk-WWCZ5F23.js → chunk-T3Z6VAAF.js} +69 -10
- package/dist/{chunk-GN57SG4G.js → chunk-TD7UE2L5.js} +9 -7
- package/dist/{chunk-TLQJPP24.js → chunk-TEO2TLVN.js} +523 -322
- package/dist/{chunk-FCSXB6T2.js → chunk-UOSL25KY.js} +14 -2
- package/dist/{chunk-KTYTFRMB.js → chunk-VKBMFOYV.js} +17 -15
- package/dist/{chunk-PT7HYKEM.js → chunk-VO2LKSTM.js} +2 -2
- package/dist/{chunk-P43ETTHK.js → chunk-VPTUJU4P.js} +2 -2
- package/dist/{chunk-WEH5XRJQ.js → chunk-WIE7ZOSW.js} +2 -2
- package/dist/{chunk-EMYUUSFG.js → chunk-WXCJ7VME.js} +5 -5
- package/dist/{chunk-4DGYLA73.js → chunk-XDOQXGFO.js} +22 -7
- package/dist/{chunk-JOZYP4GM.js → chunk-YKOFT37S.js} +5 -5
- package/dist/chunk-YSEHGPCT.js +127 -0
- package/dist/{chunk-HFSBBKSQ.js → chunk-YW7UVM5V.js} +138 -3
- package/dist/cli/index.js +27 -27
- package/dist/{clio-QVTYJ57A.js → clio-LT5V7SSZ.js} +6 -6
- package/dist/{code-nav-FGGFIE7L.js → code-nav-LMW275PA.js} +4 -4
- package/dist/{config-LW5IJFQN.js → config-RXS5T3JT.js} +71 -43
- package/dist/{configure-7XIZCOU4.js → configure-2WYWSCSD.js} +14 -15
- package/dist/{context-Y6Y7QPR6.js → context-I3BTOTCS.js} +12 -12
- package/dist/{context-L3WL3X7K.js → context-MVOORGMF.js} +34 -33
- package/dist/{context-N52ZA626.js → context-PALKKQYL.js} +20 -20
- package/dist/{context-clear-MBQRLSDQ.js → context-clear-N2WOYZ2K.js} +34 -33
- package/dist/{context-index-HVMFQHK3.js → context-index-HNG3MOME.js} +2 -2
- package/dist/{context-working-set-GS6DSO7F.js → context-working-set-MIEVECVZ.js} +10 -11
- package/dist/{dispatch-runner-22ZCNOM3.js → dispatch-runner-VVA4SRRH.js} +34 -33
- package/dist/doctor-TWBWFK5V.js +165 -0
- package/dist/{eval-BEC2WHDA.js → eval-IJ5VEZDJ.js} +2016 -142
- package/dist/{evidence-REJUMSKM.js → evidence-L5APPXNV.js} +29 -28
- package/dist/{evolve-PY5ZBA5K.js → evolve-RGNKFJ52.js} +29 -28
- package/dist/{extensions-HVKU65YU.js → extensions-7WYWUX5A.js} +9 -3
- package/dist/{fleet-7WZEWRFA.js → fleet-6CNVBZZP.js} +87 -55
- package/dist/{fleet-commands-UVHWM76J.js → fleet-commands-L2SXSYEI.js} +6 -6
- package/dist/{fleet-graph-6ULH7PES.js → fleet-graph-2J3OOIPO.js} +14 -12
- package/dist/{fleet-preflight-J53T6CCE.js → fleet-preflight-CZRJ4JP5.js} +3 -4
- package/dist/{fleet-validate-72PC4SLA.js → fleet-validate-C5RI6DP7.js} +16 -15
- package/dist/{init-OG3TPGQG.js → init-VBN2ACVA.js} +50 -48
- package/dist/{library-CNTMPLRF.js → library-JHGUMLY2.js} +13 -11
- package/dist/{memory-6IS7F275.js → memory-K4OQIYWG.js} +31 -30
- package/dist/{models-ENRJDA5W.js → models-2NCZUWDD.js} +23 -22
- package/dist/{monitor-XLDVO7TN.js → monitor-MMVTJABD.js} +35 -34
- package/dist/{orchestrator-6KSPYRHA.js → orchestrator-ZKBPCHW6.js} +1627 -294
- package/dist/{reset-RZ4ER727.js → reset-DD5JGOY3.js} +3 -3
- package/dist/{run-Y2CNK5RU.js → run-QEGNX7FL.js} +56 -55
- package/dist/{share-A55GYP6Z.js → share-JKD3BQMW.js} +13 -11
- package/dist/{skills-ALC5J6AT.js → skills-LMQIKDOZ.js} +14 -12
- package/dist/{skills-eval-JPBEBYQU.js → skills-eval-I7X2774U.js} +34 -32
- package/dist/{steer-GGWFUJUD.js → steer-CF5TDANS.js} +3 -3
- package/dist/{support-MIETYA5E.js → support-I7LOJLIF.js} +4 -4
- package/dist/{targets-VGNXIR3S.js → targets-RUSR6B5Z.js} +58 -32
- package/dist/{terminal-lease-WOBR64YA.js → terminal-lease-QYVORFR4.js} +6 -4
- package/dist/{trace-PNCASAXC.js → trace-ODOQIVIW.js} +61 -6
- package/dist/{upgrade-FUSUAGHR.js → upgrade-XANW3FXB.js} +17 -16
- package/dist/{usage-N4MKVHKD.js → usage-4H7ZRXQT.js} +88 -44
- package/dist/{verifiers-YAWOJ3H2.js → verifiers-UZXNBZEB.js} +6 -6
- package/dist/{verify-LTDHYBGY.js → verify-BVKWTNDL.js} +5 -5
- package/dist/{wiki-generate-6M7GHTBJ.js → wiki-generate-MY7WV2QI.js} +49 -47
- package/dist/worker/entry.js +29 -33
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/artifact-versions.md +6 -1
- package/docs/built-in-agents.md +1 -1
- package/docs/capacity-and-scheduling.md +23 -2
- package/docs/commands-and-modes.md +1 -1
- package/docs/configuration-and-targets.md +30 -3
- 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 +11 -10
- package/docs/evolution.md +1 -1
- package/docs/extensions-and-sharing.md +3 -1
- package/docs/fleet-dispatch.md +4 -4
- 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 +53 -2
- package/docs/proactive-memory.md +127 -14
- package/docs/prompt-envelope-and-tools.md +19 -1
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +19 -3
- 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 -1
- package/src/cli/agents.ts +1 -1
- package/src/cli/config-inspect.ts +33 -6
- package/src/cli/config.ts +1 -1
- package/src/cli/doctor-state-size.ts +82 -0
- package/src/cli/doctor.ts +3 -1
- package/src/cli/eval.ts +80 -16
- package/src/cli/extensions.ts +5 -1
- package/src/cli/fleet.ts +32 -3
- package/src/cli/targets.ts +44 -13
- package/src/cli/trace.ts +63 -4
- package/src/cli/usage.ts +63 -14
- package/src/core/bus-events.ts +29 -1
- package/src/core/cache-telemetry.ts +42 -0
- 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/residency-target-key.ts +25 -0
- package/src/core/response-schema.ts +36 -2
- package/src/domains/config/classify.ts +3 -0
- package/src/domains/context/codewiki/coordinator.ts +12 -4
- package/src/domains/dispatch/admission.ts +40 -3
- package/src/domains/dispatch/capacity-lease.ts +98 -9
- package/src/domains/dispatch/contract.ts +11 -0
- package/src/domains/dispatch/execution-plan.ts +44 -4
- package/src/domains/dispatch/extension.ts +166 -42
- package/src/domains/dispatch/fleet-run.ts +23 -3
- package/src/domains/dispatch/heartbeat.ts +32 -8
- package/src/domains/dispatch/index.ts +3 -0
- package/src/domains/dispatch/orphan-recovery.ts +5 -0
- package/src/domains/dispatch/reservation-store.ts +116 -8
- package/src/domains/dispatch/state.ts +4 -0
- package/src/domains/dispatch/worker-spawn.ts +25 -11
- package/src/domains/dispatch/write-boundary-enforcer.ts +20 -3
- package/src/domains/dispatch/write-boundary.ts +62 -1
- 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/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 +127 -0
- 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 +2 -13
- package/src/domains/evidence/eval.ts +2 -12
- package/src/domains/evidence/findings-markdown.ts +33 -0
- package/src/domains/evidence/run-trust.ts +7 -113
- package/src/domains/evidence/trust-projection.ts +2 -2
- package/src/domains/extensions/compatibility.ts +285 -0
- package/src/domains/extensions/discovery.ts +38 -3
- package/src/domains/extensions/resources.ts +1 -1
- package/src/domains/extensions/state.ts +12 -3
- package/src/domains/extensions/types.ts +2 -0
- package/src/domains/lifecycle/doctor.ts +69 -1
- 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/providers/endpoint-capacity.ts +96 -0
- 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/prompts/loader.ts +95 -33
- package/src/domains/safety/call-target.ts +52 -0
- package/src/domains/safety/run-effects.ts +35 -4
- 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/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/provider-payload.ts +29 -1
- 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 +84 -12
- 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-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 +35 -1
- 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/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/view-overlay.ts +28 -3
- package/src/tools/ask-user.ts +43 -2
- package/src/tools/dispatch-plan.ts +17 -9
- package/src/tools/dispatch-scout.ts +1 -1
- package/src/tools/registry.ts +16 -0
- package/dist/chunk-AOCYTWAV.js +0 -449
- package/dist/chunk-HWUFFB6L.js +0 -83
- package/dist/chunk-R346GLFC.js +0 -31
- package/dist/doctor-M7YEDGAE.js +0 -91
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local inspection of the exact mutation a parked `write` or `edit` would make.
|
|
3
|
+
*
|
|
4
|
+
* The approval card describes an unlisted argument by type and size, so a
|
|
5
|
+
* parked write read `content=<string 482 bytes>` and a parked edit read
|
|
6
|
+
* `edits=<array 3 items>`: the operator authorized bytes they had never seen,
|
|
7
|
+
* and the only way to read them was the external session ledger (issue #254).
|
|
8
|
+
*
|
|
9
|
+
* Two rules shape everything here. The preview is derived from the same
|
|
10
|
+
* arguments object the decision resumes, and it carries a digest of those
|
|
11
|
+
* arguments so the bytes on screen are verifiably the bytes being approved.
|
|
12
|
+
* And the mutation text is process-local: it is built lazily when the operator
|
|
13
|
+
* asks for it, it is handed only to the overlay body, and it is never placed on
|
|
14
|
+
* {@link import("./permission-overlay.js").ApprovalRequestView}, which is what
|
|
15
|
+
* crosses into the transcript, the notices, and the worker seam.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { ToolNames } from "../core/tool-names.js";
|
|
21
|
+
import { sanitizeCallTargetText, sanitizeMultilineDisplayText } from "../domains/safety/call-target.js";
|
|
22
|
+
import {
|
|
23
|
+
applyEditsToNormalizedContent,
|
|
24
|
+
type Edit,
|
|
25
|
+
generateDiffString,
|
|
26
|
+
normalizeToLF,
|
|
27
|
+
stripBom,
|
|
28
|
+
} from "../tools/edit-diff.js";
|
|
29
|
+
import { resolveToCwd } from "../tools/path-utils.js";
|
|
30
|
+
|
|
31
|
+
export { MUTATION_PREVIEW_KEY } from "./permission-hint.js";
|
|
32
|
+
|
|
33
|
+
/** Rows of mutation shown at once. Anything longer scrolls, as `/handoff` review does. */
|
|
34
|
+
export const MUTATION_PREVIEW_VISIBLE_ROWS = 16;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Characters of proposed content the preview renders. The diff builder has its
|
|
38
|
+
* own byte backstop; this bounds the write path, whose payload is a whole file.
|
|
39
|
+
* Reaching it prints an explicit line naming what was withheld.
|
|
40
|
+
*/
|
|
41
|
+
export const MUTATION_PREVIEW_MAX_CHARS = 262_144;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Facts about a parked mutation that may cross every process boundary: what
|
|
45
|
+
* kind it is, how big it is, and the digest that binds a preview to it. No part
|
|
46
|
+
* of the mutation text appears here, which is why this is the shape the
|
|
47
|
+
* approval view, the transcript row, and any notice may carry.
|
|
48
|
+
*/
|
|
49
|
+
export interface MutationFacts {
|
|
50
|
+
kind: "write" | "edit";
|
|
51
|
+
/** Bytes of proposed content (write), or of the replacement text across every edit. */
|
|
52
|
+
bytes: number;
|
|
53
|
+
/** Replacements in the edit list. One for a write. */
|
|
54
|
+
replacements: number;
|
|
55
|
+
/** Truncated SHA-256 over the exact call arguments this decision applies to. */
|
|
56
|
+
digest: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MutationPreview {
|
|
60
|
+
facts: MutationFacts;
|
|
61
|
+
/** What the body is, named honestly on the overlay's first line. */
|
|
62
|
+
heading: string;
|
|
63
|
+
/** The complete rendered mutation, already sanitized and one entry per display row. */
|
|
64
|
+
body: ReadonlyArray<string>;
|
|
65
|
+
/** A control byte or escape sequence in the payload was neutralized for display. */
|
|
66
|
+
neutralized: boolean;
|
|
67
|
+
/** A tab was expanded, so columns are display columns. */
|
|
68
|
+
tabsExpanded: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the complete preview for the parked call, or explain why it cannot be
|
|
73
|
+
* built. Never throws: an unreadable file or an edit list that will not apply
|
|
74
|
+
* is itself something the operator needs to read before deciding.
|
|
75
|
+
*/
|
|
76
|
+
export type MutationInspector = () => MutationPreview;
|
|
77
|
+
|
|
78
|
+
function canonicalJson(value: unknown): string {
|
|
79
|
+
if (value === null || value === undefined) return "null";
|
|
80
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
81
|
+
if (typeof value === "object") {
|
|
82
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
83
|
+
.filter(([, entry]) => entry !== undefined)
|
|
84
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
85
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
88
|
+
if (typeof value === "number") return Number.isFinite(value) ? JSON.stringify(value) : "null";
|
|
89
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
90
|
+
return "null";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The identity a decision is bound to. Key order and absent optional fields do
|
|
95
|
+
* not change it, so the same call digests the same way wherever it is read;
|
|
96
|
+
* anything else about the arguments does, which is the point: a call that
|
|
97
|
+
* differs from the previewed one cannot inherit its preview.
|
|
98
|
+
*/
|
|
99
|
+
export function callArgumentsDigest(args: Record<string, unknown> | undefined): string {
|
|
100
|
+
return createHash("sha256")
|
|
101
|
+
.update(canonicalJson(args ?? {}), "utf8")
|
|
102
|
+
.digest("hex")
|
|
103
|
+
.slice(0, 16);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readEdits(value: unknown): Edit[] | null {
|
|
107
|
+
if (!Array.isArray(value)) return null;
|
|
108
|
+
const edits: Edit[] = [];
|
|
109
|
+
for (const entry of value) {
|
|
110
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
111
|
+
const record = entry as Record<string, unknown>;
|
|
112
|
+
if (typeof record.oldText !== "string" || typeof record.newText !== "string") return null;
|
|
113
|
+
edits.push({ oldText: record.oldText, newText: record.newText });
|
|
114
|
+
}
|
|
115
|
+
return edits;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The size-and-digest facts for a parked call, or null when the tool is not a
|
|
120
|
+
* file mutation or its arguments are not the shape this can describe. The card
|
|
121
|
+
* keeps rendering its allowlisted target either way.
|
|
122
|
+
*/
|
|
123
|
+
export function mutationFacts(tool: string, args: Record<string, unknown> | undefined): MutationFacts | null {
|
|
124
|
+
if (!args || typeof args.path !== "string" || args.path.length === 0) return null;
|
|
125
|
+
if (tool === ToolNames.Write) {
|
|
126
|
+
if (typeof args.content !== "string") return null;
|
|
127
|
+
return {
|
|
128
|
+
kind: "write",
|
|
129
|
+
bytes: Buffer.byteLength(args.content, "utf8"),
|
|
130
|
+
replacements: 1,
|
|
131
|
+
digest: callArgumentsDigest(args),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (tool === ToolNames.Edit) {
|
|
135
|
+
const edits = readEdits(args.edits);
|
|
136
|
+
if (edits === null || edits.length === 0) return null;
|
|
137
|
+
const bytes = edits.reduce((total, edit) => total + Buffer.byteLength(edit.newText, "utf8"), 0);
|
|
138
|
+
return { kind: "edit", bytes, replacements: edits.length, digest: callArgumentsDigest(args) };
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** `write · 482 B · sha256 3f9a1c22ee04b7d1`, the collapsed card's whole mutation line. */
|
|
144
|
+
export function mutationFactsLine(facts: MutationFacts): string {
|
|
145
|
+
const shape =
|
|
146
|
+
facts.kind === "write"
|
|
147
|
+
? `write ${facts.bytes} B`
|
|
148
|
+
: `edit ${facts.replacements} replacement${facts.replacements === 1 ? "" : "s"} · ${facts.bytes} B`;
|
|
149
|
+
return `${shape} · sha256 ${facts.digest}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function numberedLines(text: string): string[] {
|
|
153
|
+
const raw = text.split("\n");
|
|
154
|
+
const width = String(raw.length).length;
|
|
155
|
+
return raw.map((line, index) => `${String(index + 1).padStart(width, " ")} ${line}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function boundedContent(content: string): { text: string; withheld: number } {
|
|
159
|
+
if (content.length <= MUTATION_PREVIEW_MAX_CHARS) return { text: content, withheld: 0 };
|
|
160
|
+
return {
|
|
161
|
+
text: content.slice(0, MUTATION_PREVIEW_MAX_CHARS),
|
|
162
|
+
withheld: content.length - MUTATION_PREVIEW_MAX_CHARS,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function previewFrom(
|
|
167
|
+
facts: MutationFacts,
|
|
168
|
+
heading: string,
|
|
169
|
+
text: string,
|
|
170
|
+
options: { numbered?: boolean; trailer?: string } = {},
|
|
171
|
+
): MutationPreview {
|
|
172
|
+
const clean = sanitizeMultilineDisplayText(text);
|
|
173
|
+
const body = options.numbered ? numberedLines(clean.text) : clean.text.split("\n");
|
|
174
|
+
if (options.trailer !== undefined) body.push(options.trailer);
|
|
175
|
+
return {
|
|
176
|
+
facts,
|
|
177
|
+
heading,
|
|
178
|
+
body,
|
|
179
|
+
neutralized: clean.neutralized,
|
|
180
|
+
tabsExpanded: clean.tabsExpanded,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function refusal(facts: MutationFacts, heading: string, reason: string): MutationPreview {
|
|
185
|
+
return { facts, heading, body: [reason], neutralized: false, tabsExpanded: false };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The requested replacements, rendered as a diff would render them, for the
|
|
190
|
+
* case where the effective diff cannot be computed. The operator still needs to
|
|
191
|
+
* read what the model asked for; the heading says this is the request rather
|
|
192
|
+
* than the effect.
|
|
193
|
+
*/
|
|
194
|
+
function replacementListing(edits: ReadonlyArray<Edit>): string {
|
|
195
|
+
const blocks: string[] = [];
|
|
196
|
+
for (const [index, edit] of edits.entries()) {
|
|
197
|
+
blocks.push(`@@ replacement ${index + 1} of ${edits.length}`);
|
|
198
|
+
for (const line of normalizeToLF(edit.oldText).split("\n")) blocks.push(`- ${line}`);
|
|
199
|
+
for (const line of normalizeToLF(edit.newText).split("\n")) blocks.push(`+ ${line}`);
|
|
200
|
+
}
|
|
201
|
+
return blocks.join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function editPreview(facts: MutationFacts, path: string, edits: ReadonlyArray<Edit>, read: (path: string) => string) {
|
|
205
|
+
let current: string;
|
|
206
|
+
try {
|
|
207
|
+
current = read(resolveToCwd(path));
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const message = sanitizeCallTargetText(error instanceof Error ? error.message : String(error));
|
|
210
|
+
return previewFrom(
|
|
211
|
+
facts,
|
|
212
|
+
`Requested replacements for ${sanitizeCallTargetText(path)} (the file could not be read: ${message})`,
|
|
213
|
+
replacementListing(edits),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const { text } = stripBom(current);
|
|
218
|
+
const applied = applyEditsToNormalizedContent(normalizeToLF(text), edits, path);
|
|
219
|
+
const diff = generateDiffString(applied.baseContent, applied.newContent);
|
|
220
|
+
return previewFrom(facts, `Effective diff for ${sanitizeCallTargetText(path)}`, diff.diff);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const message = sanitizeCallTargetText(error instanceof Error ? error.message : String(error));
|
|
223
|
+
return previewFrom(
|
|
224
|
+
facts,
|
|
225
|
+
`Requested replacements for ${sanitizeCallTargetText(path)} (they do not apply: ${message})`,
|
|
226
|
+
replacementListing(edits),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface MutationInspectorOptions {
|
|
232
|
+
/** Reads the current file bytes for an edit's effective diff. Injected so the contract test owns the filesystem. */
|
|
233
|
+
readFile?: (path: string) => string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Bind an inspector to one parked call. The returned closure reads the file and
|
|
238
|
+
* computes the diff only when the operator presses the inspect key, and it
|
|
239
|
+
* re-derives the digest from the same arguments it renders: a mismatch means
|
|
240
|
+
* the arguments changed under the decision and the preview refuses rather than
|
|
241
|
+
* showing bytes that are no longer the ones being approved.
|
|
242
|
+
*/
|
|
243
|
+
export function createMutationInspector(
|
|
244
|
+
tool: string,
|
|
245
|
+
args: Record<string, unknown> | undefined,
|
|
246
|
+
facts: MutationFacts,
|
|
247
|
+
options: MutationInspectorOptions = {},
|
|
248
|
+
): MutationInspector {
|
|
249
|
+
const read = options.readFile ?? ((path: string) => readFileSync(path, "utf8"));
|
|
250
|
+
return () => {
|
|
251
|
+
const current = mutationFacts(tool, args);
|
|
252
|
+
if (current === null || current.digest !== facts.digest) {
|
|
253
|
+
return refusal(
|
|
254
|
+
facts,
|
|
255
|
+
"Preview unavailable",
|
|
256
|
+
`These arguments no longer digest to sha256 ${facts.digest}. Deny this call rather than approving bytes that cannot be shown.`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const path = typeof args?.path === "string" ? args.path : "";
|
|
260
|
+
if (facts.kind === "write") {
|
|
261
|
+
const content = typeof args?.content === "string" ? args.content : "";
|
|
262
|
+
const bounded = boundedContent(content);
|
|
263
|
+
return previewFrom(facts, `Proposed content for ${sanitizeCallTargetText(path)}`, bounded.text, {
|
|
264
|
+
numbered: true,
|
|
265
|
+
...(bounded.withheld > 0
|
|
266
|
+
? { trailer: `… ${bounded.withheld} more characters not shown (preview capped at ${MUTATION_PREVIEW_MAX_CHARS})` }
|
|
267
|
+
: {}),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const edits = readEdits(args?.edits) ?? [];
|
|
271
|
+
return editPreview(facts, path, edits, read);
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The scrolled window of a preview, plus the position line that makes the cut
|
|
277
|
+
* visible. Mirrors the `/handoff` review overlay so both scrollable inspection
|
|
278
|
+
* surfaces read the same way.
|
|
279
|
+
*/
|
|
280
|
+
export function mutationPreviewWindow(
|
|
281
|
+
lines: ReadonlyArray<string>,
|
|
282
|
+
scroll: number,
|
|
283
|
+
rows = MUTATION_PREVIEW_VISIBLE_ROWS,
|
|
284
|
+
): { window: ReadonlyArray<string>; position: string | null; maxScroll: number } {
|
|
285
|
+
const visible = Math.max(1, rows);
|
|
286
|
+
const maxScroll = Math.max(0, lines.length - visible);
|
|
287
|
+
const start = Math.max(0, Math.min(scroll, maxScroll));
|
|
288
|
+
const window = lines.slice(start, start + visible);
|
|
289
|
+
if (lines.length <= visible) return { window, position: null, maxScroll };
|
|
290
|
+
return {
|
|
291
|
+
window,
|
|
292
|
+
position: `(${start + 1}-${start + window.length} of ${lines.length} lines)`,
|
|
293
|
+
maxScroll,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
proposeMemoryPromotion,
|
|
13
13
|
} from "../domains/memory/index.js";
|
|
14
14
|
import type { ObservabilityContract } from "../domains/observability/index.js";
|
|
15
|
+
import { foregroundStreamUsage } from "../domains/providers/index.js";
|
|
15
16
|
import type { ContextLedger } from "../domains/session/context-ledger.js";
|
|
16
17
|
import type { SessionMeta } from "../domains/session/index.js";
|
|
17
18
|
import { foldSessionArtifacts } from "../domains/session/session-artifacts.js";
|
|
@@ -147,6 +148,11 @@ export function createOverlayGeneralOpeners(deps: OverlayGeneralOpenersDeps): Ov
|
|
|
147
148
|
deps.transitions.state = "cost";
|
|
148
149
|
deps.transitions.handle = openCostOverlayFactory(deps.tui, deps.observability, {
|
|
149
150
|
sessionId: deps.getSessionId?.() ?? null,
|
|
151
|
+
...(deps.readSessionEntries
|
|
152
|
+
? {
|
|
153
|
+
getSessionEntries: () => deps.readSessionEntries?.() ?? [],
|
|
154
|
+
}
|
|
155
|
+
: {}),
|
|
150
156
|
});
|
|
151
157
|
deps.requestRender();
|
|
152
158
|
};
|
|
@@ -429,10 +435,20 @@ export function createOverlayGeneralOpeners(deps: OverlayGeneralOpenersDeps): Ov
|
|
|
429
435
|
};
|
|
430
436
|
const resolution = deps.dispatch.preview?.(request);
|
|
431
437
|
if (!resolution) return null;
|
|
438
|
+
const foregroundHeld =
|
|
439
|
+
resolution.endpoint === undefined ? 0 : (foregroundStreamUsage()[resolution.endpoint.key] ?? 0);
|
|
432
440
|
return {
|
|
433
441
|
targetId: resolution.targetId,
|
|
434
442
|
wireModelId: resolution.wireModelId,
|
|
435
443
|
nodeId: resolution.node.id,
|
|
444
|
+
...(resolution.endpoint !== undefined
|
|
445
|
+
? {
|
|
446
|
+
endpoint: {
|
|
447
|
+
...resolution.endpoint,
|
|
448
|
+
...(foregroundHeld > 0 ? { foregroundHeld } : {}),
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
: {}),
|
|
436
452
|
};
|
|
437
453
|
},
|
|
438
454
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ClioKeybinding } from "../domains/config/keybindings.js";
|
|
2
2
|
import { isKeyRelease, matchesKey } from "../engine/tui.js";
|
|
3
|
+
import { MUTATION_PREVIEW_KEY } from "./permission-hint.js";
|
|
3
4
|
|
|
4
5
|
export type OverlayState =
|
|
5
6
|
| "closed"
|
|
@@ -47,6 +48,11 @@ export interface PermissionOverlayKeyDeps {
|
|
|
47
48
|
* the call. Only the deletion keys in `isDraftEditKey` arrive here.
|
|
48
49
|
*/
|
|
49
50
|
editDraft?: (data: string) => void;
|
|
51
|
+
/** Whether this card has a mutation the operator can read locally (issue #254). */
|
|
52
|
+
canInspectMutation?: () => boolean;
|
|
53
|
+
isInspectingMutation?: () => boolean;
|
|
54
|
+
toggleMutationInspection?: () => void;
|
|
55
|
+
scrollMutationInspection?: (delta: number) => void;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
export interface DispatchBoardOverlayKeyDeps {
|
|
@@ -85,6 +91,18 @@ function isDraftEditKey(data: string): boolean {
|
|
|
85
91
|
return !isKeyRelease(data) && DRAFT_EDIT_KEYS.some((key) => matchesKey(data, key));
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
/** Rows one page key moves the open mutation. Matches the window the card renders. */
|
|
95
|
+
const MUTATION_PAGE_ROWS = 16;
|
|
96
|
+
|
|
97
|
+
/** How far a key scrolls the open mutation, or 0 when the key is not a scroll key. */
|
|
98
|
+
function mutationScrollDelta(data: string): number {
|
|
99
|
+
if (matchesKey(data, "up")) return -1;
|
|
100
|
+
if (matchesKey(data, "down")) return 1;
|
|
101
|
+
if (matchesKey(data, "pageUp")) return -MUTATION_PAGE_ROWS;
|
|
102
|
+
if (matchesKey(data, "pageDown")) return MUTATION_PAGE_ROWS;
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
/** Pure permission overlay key router: returns true when the input was consumed. */
|
|
89
107
|
export function routePermissionOverlayKey(data: string, deps: PermissionOverlayKeyDeps): boolean {
|
|
90
108
|
if (matchesKey(data, "enter") && !isKeyRelease(data)) {
|
|
@@ -98,6 +116,26 @@ export function routePermissionOverlayKey(data: string, deps: PermissionOverlayK
|
|
|
98
116
|
deps.editDraft(data);
|
|
99
117
|
return true;
|
|
100
118
|
}
|
|
119
|
+
// Reading the mutation is the one thing this dialog does that is not an
|
|
120
|
+
// answer, so it toggles rather than navigating away: Enter, `s`, and Esc keep
|
|
121
|
+
// their meanings the whole time the mutation is on screen, and `v` is what
|
|
122
|
+
// puts it away. A card with nothing local to inspect leaves the key inert.
|
|
123
|
+
if (
|
|
124
|
+
matchesKey(data, MUTATION_PREVIEW_KEY) &&
|
|
125
|
+
!isKeyRelease(data) &&
|
|
126
|
+
(deps.canInspectMutation?.() ?? false) &&
|
|
127
|
+
deps.toggleMutationInspection
|
|
128
|
+
) {
|
|
129
|
+
deps.toggleMutationInspection();
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
if ((deps.isInspectingMutation?.() ?? false) && deps.scrollMutationInspection && !isKeyRelease(data)) {
|
|
133
|
+
const delta = mutationScrollDelta(data);
|
|
134
|
+
if (delta !== 0) {
|
|
135
|
+
deps.scrollMutationInspection(delta);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
101
139
|
if (isEscapeKey(data)) {
|
|
102
140
|
deps.cancelPermission();
|
|
103
141
|
return true;
|
|
@@ -12,6 +12,7 @@ import { createOverlayTransitions } from "./overlay-transitions.js";
|
|
|
12
12
|
import {
|
|
13
13
|
createPermissionOverlayBody,
|
|
14
14
|
PERMISSION_OVERLAY_WIDTH,
|
|
15
|
+
type PermissionOverlayBodyHandle,
|
|
15
16
|
permissionOverlayHint,
|
|
16
17
|
permissionOverlayPlacement,
|
|
17
18
|
permissionOverlayTitle,
|
|
@@ -163,6 +164,12 @@ export interface OverlayLifecycleController {
|
|
|
163
164
|
toggleDispatchBoardOverlay(): void;
|
|
164
165
|
confirmPermission(): void;
|
|
165
166
|
stopTurnFromPermission(): void;
|
|
167
|
+
/** Whether the live permission card has a mutation the operator can read here. */
|
|
168
|
+
canInspectMutation(): boolean;
|
|
169
|
+
/** Whether that mutation is currently open. */
|
|
170
|
+
isInspectingMutation(): boolean;
|
|
171
|
+
toggleMutationInspection(): void;
|
|
172
|
+
scrollMutationInspection(delta: number): void;
|
|
166
173
|
cancelAskUser(): void;
|
|
167
174
|
dispose(): void;
|
|
168
175
|
}
|
|
@@ -210,6 +217,16 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
|
|
|
210
217
|
} = deps;
|
|
211
218
|
let overlayPermission: OverlayPermissionLifecycle | null = null;
|
|
212
219
|
let overlayAskUser: OverlayAskUserLifecycle | null = null;
|
|
220
|
+
/**
|
|
221
|
+
* The live permission card's body, held only while its dialog is on screen.
|
|
222
|
+
* The mutation text it can show lives here and nowhere else, so dropping the
|
|
223
|
+
* reference when the dialog closes drops the text with it.
|
|
224
|
+
*/
|
|
225
|
+
let permissionBody: PermissionOverlayBodyHandle | null = null;
|
|
226
|
+
const inspectionHint = (): import("./permission-hint.js").PermissionInspectionHint => {
|
|
227
|
+
if (permissionBody === null || !permissionBody.canInspect()) return "none";
|
|
228
|
+
return permissionBody.isInspecting() ? "open" : "closed";
|
|
229
|
+
};
|
|
213
230
|
const overlayTransitions = createOverlayTransitions({
|
|
214
231
|
stopDispatchBoardTicker: () => interactiveTickers.stopDispatchBoardTicker(),
|
|
215
232
|
renderContextIsland: () => interactiveTickers.renderContextIsland(),
|
|
@@ -217,7 +234,10 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
|
|
|
217
234
|
requestRender: () => tui.requestRender(),
|
|
218
235
|
cancelPendingAskUser: () => overlayAskUser?.cancelPending() ?? false,
|
|
219
236
|
finishAuth: (dismiss) => overlayAuth.finish(dismiss),
|
|
220
|
-
onPermissionOverlayClosed: () =>
|
|
237
|
+
onPermissionOverlayClosed: () => {
|
|
238
|
+
permissionBody = null;
|
|
239
|
+
overlayPermission?.onPermissionOverlayClosed();
|
|
240
|
+
},
|
|
221
241
|
onOverlayClosed: () => overlayPermission?.retryPending(),
|
|
222
242
|
});
|
|
223
243
|
const closeOverlay = overlayTransitions.close;
|
|
@@ -248,17 +268,20 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
|
|
|
248
268
|
dispatch: deps.app.dispatch,
|
|
249
269
|
getAutonomy: () => deps.app.getSettings?.().autonomy ?? "auto-edit",
|
|
250
270
|
getOverlayState: () => overlayTransitions.state,
|
|
251
|
-
openPermissionOverlay: (view) => {
|
|
271
|
+
openPermissionOverlay: (view, inspect) => {
|
|
252
272
|
if (overlayTransitions.state !== "closed") return false;
|
|
253
273
|
overlayTransitions.state = "permission-confirm";
|
|
254
|
-
|
|
274
|
+
const body = createPermissionOverlayBody(view, inspect);
|
|
275
|
+
permissionBody = body;
|
|
276
|
+
overlayTransitions.handle = showOverlayFrame(tui, body, {
|
|
255
277
|
...permissionOverlayPlacement(tui, editor, footer.view),
|
|
256
278
|
width: PERMISSION_OVERLAY_WIDTH,
|
|
257
279
|
title: permissionOverlayTitle(view),
|
|
258
280
|
tone: permissionOverlayTone(view),
|
|
259
281
|
// Read per frame: the footer names what Enter does right now, and
|
|
260
|
-
// that depends on whether the composer holds a draft
|
|
261
|
-
|
|
282
|
+
// that depends on whether the composer holds a draft and on whether
|
|
283
|
+
// the mutation is open.
|
|
284
|
+
footerHint: (innerWidth) => permissionOverlayHint(innerWidth, editor.getText().length > 0, inspectionHint()),
|
|
262
285
|
});
|
|
263
286
|
tui.requestRender();
|
|
264
287
|
return true;
|
|
@@ -464,6 +487,16 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
|
|
|
464
487
|
footer.refresh();
|
|
465
488
|
tui.requestRender();
|
|
466
489
|
},
|
|
490
|
+
canInspectMutation: () => permissionBody?.canInspect() ?? false,
|
|
491
|
+
isInspectingMutation: () => permissionBody?.isInspecting() ?? false,
|
|
492
|
+
toggleMutationInspection: () => {
|
|
493
|
+
permissionBody?.toggleInspect();
|
|
494
|
+
tui.requestRender();
|
|
495
|
+
},
|
|
496
|
+
scrollMutationInspection: (delta) => {
|
|
497
|
+
permissionBody?.scrollInspect(delta);
|
|
498
|
+
tui.requestRender();
|
|
499
|
+
},
|
|
467
500
|
cancelAskUser: overlayAskUser.cancel,
|
|
468
501
|
dispose: () => {
|
|
469
502
|
overlayPermission?.dispose();
|
|
@@ -11,6 +11,7 @@ import type { PermissionRequiredMeta, ToolRegistry } from "../tools/registry.js"
|
|
|
11
11
|
import { approvalParkedNotice, autonomyDeniedNotice, workerEscalationNotice } from "./bus-notices.js";
|
|
12
12
|
import type { ToolApprovalStateEvent } from "./chat-loop.js";
|
|
13
13
|
import type { NoticeLevel } from "./command-output.js";
|
|
14
|
+
import { createMutationInspector, type MutationInspector, mutationFacts } from "./mutation-preview.js";
|
|
14
15
|
import type { OverlayState } from "./overlay-key-routing.js";
|
|
15
16
|
import { type ApprovalRequestView, askAxis, describeCallTarget } from "./permission-overlay.js";
|
|
16
17
|
|
|
@@ -43,7 +44,12 @@ export interface OverlayPermissionLifecycleDeps {
|
|
|
43
44
|
dispatch: Pick<DispatchContract, "resolveWorkerPermission">;
|
|
44
45
|
getAutonomy(): string;
|
|
45
46
|
getOverlayState(): OverlayState;
|
|
46
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Open the dialog for one parked call. `inspect` carries the complete
|
|
49
|
+
* mutation text and is the only channel that does; it is process-local by
|
|
50
|
+
* construction, so it is passed here rather than folded into the view.
|
|
51
|
+
*/
|
|
52
|
+
openPermissionOverlay(view: ApprovalRequestView, inspect?: MutationInspector): boolean;
|
|
47
53
|
closeOverlay(): void;
|
|
48
54
|
appendNotice(level: NoticeLevel, text: string): void;
|
|
49
55
|
applyApprovalState(event: ToolApprovalStateEvent): void;
|
|
@@ -131,6 +137,9 @@ function mainApprovalRequestView(
|
|
|
131
137
|
? { kind: "net" as const, ruleId: axisFromDecision.ruleId }
|
|
132
138
|
: { kind: "autonomy" as const, level: autonomy });
|
|
133
139
|
const target = describeCallTarget(call.tool, call.args);
|
|
140
|
+
// Facts only. The mutation text stays in the inspector the overlay opener
|
|
141
|
+
// gets; this object reaches the transcript row and the approval-state event.
|
|
142
|
+
const mutation = mutationFacts(call.tool, call.args);
|
|
134
143
|
return {
|
|
135
144
|
requestId: meta?.requestId ?? "permission-pending",
|
|
136
145
|
tool: call.tool,
|
|
@@ -144,10 +153,21 @@ function mainApprovalRequestView(
|
|
|
144
153
|
: {}),
|
|
145
154
|
...(call.tool === ToolNames.AskUser ? { exposure: askUserExposure(call.args) } : {}),
|
|
146
155
|
...(target.length > 0 ? { target } : {}),
|
|
156
|
+
...(mutation !== null ? { mutation } : {}),
|
|
147
157
|
...(queueDepth !== undefined && queueDepth > 1 ? { queueDepth } : {}),
|
|
148
158
|
};
|
|
149
159
|
}
|
|
150
160
|
|
|
161
|
+
/**
|
|
162
|
+
* The inspector for a main-agent ask, or undefined when there is nothing local
|
|
163
|
+
* to read. A worker escalation never gets one: its arguments stay in the
|
|
164
|
+
* worker, and the card says so rather than implying an inspection it cannot do.
|
|
165
|
+
*/
|
|
166
|
+
function mainMutationInspector(call: ClassifierCall, view: ApprovalRequestView): MutationInspector | undefined {
|
|
167
|
+
if (view.mutation === undefined) return undefined;
|
|
168
|
+
return createMutationInspector(call.tool, call.args, view.mutation);
|
|
169
|
+
}
|
|
170
|
+
|
|
151
171
|
function workerEscalationEntry(payload: PermissionRequestedPayload, autonomy: string): WorkerEscalationEntry | null {
|
|
152
172
|
if (!isLiveWorkerEscalationRequest(payload) || typeof payload.requestId !== "string") return null;
|
|
153
173
|
const origin = typeof payload.origin === "string" ? payload.origin : undefined;
|
|
@@ -240,7 +260,7 @@ export function createOverlayPermissionLifecycle(deps: OverlayPermissionLifecycl
|
|
|
240
260
|
announceParked();
|
|
241
261
|
return;
|
|
242
262
|
}
|
|
243
|
-
if (!deps.openPermissionOverlay(view)) {
|
|
263
|
+
if (!deps.openPermissionOverlay(view, mainMutationInspector(call, view))) {
|
|
244
264
|
announceParked();
|
|
245
265
|
return;
|
|
246
266
|
}
|
|
@@ -403,19 +403,83 @@ export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps)
|
|
|
403
403
|
void runHandoffExtraction(session, fromSessionId, verdict.goal);
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
/** Characters of the second round's answer quoted in the terminal refusal. */
|
|
407
|
+
const HANDOFF_REFUSAL_QUOTE_CHARS = 200;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* A refusal that says what was asked for and what came back, not only that no
|
|
411
|
+
* JSON object arrived. Both rounds are named, so an operator reading it knows
|
|
412
|
+
* two model calls were spent and on what.
|
|
413
|
+
*/
|
|
414
|
+
function terminalHandoffRefusal(firstReason: string, secondReason: string, secondText: string): string {
|
|
415
|
+
const answered = secondText.replace(/\s+/g, " ").trim().slice(0, HANDOFF_REFUSAL_QUOTE_CHARS);
|
|
416
|
+
return [
|
|
417
|
+
"the extraction round could not produce a handoff record after one repair attempt.",
|
|
418
|
+
`Asked for: one JSON object with decisions, facts, files, commands, and openQuestions.`,
|
|
419
|
+
`Round 1: ${firstReason}.`,
|
|
420
|
+
`Round 2: ${secondReason}.`,
|
|
421
|
+
`Round 2 returned: ${answered.length > 0 ? answered : "(nothing)"}`,
|
|
422
|
+
].join(" ");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Extract, then repair once.
|
|
427
|
+
*
|
|
428
|
+
* A local model that answers with prose around the object, or with nothing
|
|
429
|
+
* parseable, used to end `/handoff` outright: every downstream behavior (the
|
|
430
|
+
* dropped-path listing, the `e` editor, accept-mints-a-session) was
|
|
431
|
+
* unreachable and the operator had paid for the round either way (issue
|
|
432
|
+
* #223). Exactly one repair round follows, quoting what came back and what
|
|
433
|
+
* the parser objected to, and both rounds bill through the same out-of-turn
|
|
434
|
+
* usage store.
|
|
435
|
+
*/
|
|
436
|
+
async function extractWithOneRepair(
|
|
437
|
+
goal: string,
|
|
438
|
+
): Promise<
|
|
439
|
+
| { ok: true; parsed: Extract<ReturnType<typeof parseHandoffExtraction>, { ok: true }> }
|
|
440
|
+
| { ok: false; level: "error" | "warn"; reason: string }
|
|
441
|
+
> {
|
|
442
|
+
const first = await deps.chat.extractHandoff(goal);
|
|
443
|
+
if (first.status !== "answered") {
|
|
444
|
+
return {
|
|
445
|
+
ok: false,
|
|
446
|
+
level: first.status === "failed" ? "error" : "warn",
|
|
447
|
+
reason: first.status === "aborted" ? "the extraction round was cancelled" : first.reason,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const parsedFirst = parseHandoffExtraction(first.text);
|
|
451
|
+
if (parsedFirst.ok) return { ok: true, parsed: parsedFirst };
|
|
452
|
+
|
|
453
|
+
const second = await deps.chat.extractHandoff(goal, {
|
|
454
|
+
repair: { complaint: parsedFirst.reason, previous: first.text },
|
|
455
|
+
});
|
|
456
|
+
if (second.status !== "answered") {
|
|
457
|
+
return {
|
|
458
|
+
ok: false,
|
|
459
|
+
level: second.status === "failed" ? "error" : "warn",
|
|
460
|
+
reason:
|
|
461
|
+
second.status === "aborted"
|
|
462
|
+
? "the repair round was cancelled"
|
|
463
|
+
: `round 1 could not be read (${parsedFirst.reason}); the repair round then failed: ${second.reason}`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const parsedSecond = parseHandoffExtraction(second.text);
|
|
467
|
+
if (parsedSecond.ok) return { ok: true, parsed: parsedSecond };
|
|
468
|
+
return {
|
|
469
|
+
ok: false,
|
|
470
|
+
level: "error",
|
|
471
|
+
reason: terminalHandoffRefusal(parsedFirst.reason, parsedSecond.reason, second.text),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
406
475
|
async function runHandoffExtraction(session: SessionContract, fromSessionId: string, goal: string): Promise<void> {
|
|
407
476
|
const notice = deps.getSlashNotice();
|
|
408
|
-
const
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
emitCommandNotice(notice, outcome.status === "failed" ? "error" : "warn", "handoff", reason);
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
const parsed = parseHandoffExtraction(outcome.text);
|
|
415
|
-
if (!parsed.ok) {
|
|
416
|
-
emitCommandNotice(notice, "error", "handoff", parsed.reason);
|
|
477
|
+
const extraction = await extractWithOneRepair(goal);
|
|
478
|
+
if (!extraction.ok) {
|
|
479
|
+
emitCommandNotice(notice, extraction.level, "handoff", extraction.reason);
|
|
417
480
|
return;
|
|
418
481
|
}
|
|
482
|
+
const parsed = extraction.parsed;
|
|
419
483
|
const meta = session.current();
|
|
420
484
|
const cwd = typeof meta?.cwd === "string" && meta.cwd.length > 0 ? meta.cwd : null;
|
|
421
485
|
const entries = deps.readStructuredEntries(fromSessionId);
|