@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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `/fleet run <name>` will do, projected before anything dispatches.
|
|
3
|
+
*
|
|
4
|
+
* The projection is the compiled plan and nothing else. It calls the same
|
|
5
|
+
* loader, the same graph and command validation, the same plan compiler, and
|
|
6
|
+
* the same write-boundary preflight the headless `fleet` subcommands call, so
|
|
7
|
+
* the overlay cannot show an operator a run that differs from the one the
|
|
8
|
+
* scheduler would execute. No fleet logic is reimplemented here; this module
|
|
9
|
+
* only arranges the compiled facts into rows a terminal can render.
|
|
10
|
+
*
|
|
11
|
+
* A preflight failure is data, not an exception. Every diagnostic is collected
|
|
12
|
+
* and returned so the overlay can render the whole list with no accept action,
|
|
13
|
+
* rather than surfacing the first failure and hiding the rest.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
FLEET_COMMANDS_REMEDY,
|
|
18
|
+
type FleetCommandRegistry,
|
|
19
|
+
FleetCommandRegistryMissingError,
|
|
20
|
+
type FleetContract,
|
|
21
|
+
fleetStepBoundaries,
|
|
22
|
+
loadFleetCommands,
|
|
23
|
+
loadFleetContract,
|
|
24
|
+
renderFleetPrompt,
|
|
25
|
+
validateFleetCommands,
|
|
26
|
+
validateFleetGraph,
|
|
27
|
+
} from "../domains/agents/index.js";
|
|
28
|
+
import type { AgentSpec } from "../domains/agents/spec.js";
|
|
29
|
+
import type { ExecutionPlan, ExecutionPlanStep } from "../domains/dispatch/execution-plan.js";
|
|
30
|
+
import {
|
|
31
|
+
type AgentRoleFactsResolver,
|
|
32
|
+
requestExecutionRole,
|
|
33
|
+
withAttemptRole,
|
|
34
|
+
} from "../domains/dispatch/execution-role.js";
|
|
35
|
+
import { compileFleetExecutionPlan } from "../domains/dispatch/fleet-plan.js";
|
|
36
|
+
import { preflightWriteBoundaries } from "../domains/dispatch/write-boundary-enforcer.js";
|
|
37
|
+
|
|
38
|
+
/** Where a step would run, as the dispatch domain resolves it today. */
|
|
39
|
+
export interface FleetRunPreviewRoute {
|
|
40
|
+
targetId: string;
|
|
41
|
+
wireModelId: string;
|
|
42
|
+
nodeId: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FleetRunPreviewStep {
|
|
46
|
+
stepId: string;
|
|
47
|
+
kind: ExecutionPlanStep["kind"];
|
|
48
|
+
scope: "readonly" | "workspace";
|
|
49
|
+
/** Agent steps only. */
|
|
50
|
+
agentId?: string;
|
|
51
|
+
/** Resolved route for an agent step; absent when the domain could not resolve one. */
|
|
52
|
+
route?: FleetRunPreviewRoute;
|
|
53
|
+
/** Code steps only: the registered command id and the argv it will run. */
|
|
54
|
+
commandId?: string;
|
|
55
|
+
argv?: ReadonlyArray<string>;
|
|
56
|
+
/** Declared write boundary. Empty is the claim "changes nothing"; undefined is no claim. */
|
|
57
|
+
writes: ReadonlyArray<string> | undefined;
|
|
58
|
+
/** Loop membership for a node a bounded loop unrolled to. */
|
|
59
|
+
loop?: { loopId: string; role: "check" | "repair"; attempt: number };
|
|
60
|
+
target?: string;
|
|
61
|
+
profile?: string;
|
|
62
|
+
gate?: { path: string; commandId: string } | { gateId: string; path: string };
|
|
63
|
+
plan?: { roster: ReadonlyArray<string>; maxTasks: number; proposals: boolean };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface FleetRunPreviewWave {
|
|
67
|
+
index: number;
|
|
68
|
+
steps: ReadonlyArray<FleetRunPreviewStep>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FleetRunPreviewBudget {
|
|
72
|
+
/** Session ceiling this run is admitted under. */
|
|
73
|
+
ceilingUsd: number;
|
|
74
|
+
currentUsd: number;
|
|
75
|
+
/** The contract's own declared ceiling, or null when it declares none. */
|
|
76
|
+
contractUsd: number | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface FleetRunPreview {
|
|
80
|
+
name: string;
|
|
81
|
+
/** Variables rendered into the plan and sealed into its durable run record. */
|
|
82
|
+
vars: Readonly<Record<string, string>>;
|
|
83
|
+
planHash: string;
|
|
84
|
+
waves: ReadonlyArray<FleetRunPreviewWave>;
|
|
85
|
+
budget: FleetRunPreviewBudget;
|
|
86
|
+
/** The compiled plan itself, so an accepted preview dispatches what was shown. */
|
|
87
|
+
plan: ExecutionPlan;
|
|
88
|
+
contract: FleetContract;
|
|
89
|
+
commands: FleetCommandRegistry | null;
|
|
90
|
+
/** Rendered prompt body every agent node carries as its task. */
|
|
91
|
+
task: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type FleetRunPreviewResult =
|
|
95
|
+
| { ok: true; preview: FleetRunPreview }
|
|
96
|
+
| { ok: false; name: string; diagnostics: ReadonlyArray<string> };
|
|
97
|
+
|
|
98
|
+
/** Everything the projection needs from the running process. */
|
|
99
|
+
export interface FleetRunPreviewInput {
|
|
100
|
+
workspaceRoot: string;
|
|
101
|
+
name: string;
|
|
102
|
+
vars: Readonly<Record<string, string>>;
|
|
103
|
+
/** Recipe lookup; a step naming an unknown agent is a diagnostic. */
|
|
104
|
+
getAgentSpec: (agentId: string) => AgentSpec | null;
|
|
105
|
+
/** Role facts for the execution role each node runs under. */
|
|
106
|
+
roleFacts: AgentRoleFactsResolver;
|
|
107
|
+
/** Session budget state; absent leaves the ceiling unknown and checks nothing. */
|
|
108
|
+
budget?: { ceilingUsd: number; currentUsd: number; verdict: "under" | "at" | "over" };
|
|
109
|
+
/** Effective route for an agent step. Absent or null renders the step without one. */
|
|
110
|
+
resolveRoute?: (step: {
|
|
111
|
+
stepId: string;
|
|
112
|
+
agentId: string;
|
|
113
|
+
/** The compiled step task; a route preview validates the request it would send, and an empty task is refused. */
|
|
114
|
+
task: string;
|
|
115
|
+
scope: "readonly" | "workspace";
|
|
116
|
+
target?: string;
|
|
117
|
+
profile?: string;
|
|
118
|
+
}) => FleetRunPreviewRoute | null;
|
|
119
|
+
/** Contract and registry loaders, overridable so tests project a fixture. */
|
|
120
|
+
load?: (workspaceRoot: string, name: string) => { contract: FleetContract; commands: FleetCommandRegistry | null };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function defaultLoad(
|
|
124
|
+
workspaceRoot: string,
|
|
125
|
+
name: string,
|
|
126
|
+
): { contract: FleetContract; commands: FleetCommandRegistry | null } {
|
|
127
|
+
const contract = loadFleetContract(workspaceRoot, name);
|
|
128
|
+
// loadFleetContract already refused any unregistered command id; this read
|
|
129
|
+
// is the binding the runner executes.
|
|
130
|
+
return { contract, commands: loadFleetCommands(workspaceRoot) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function describeError(error: unknown): string {
|
|
134
|
+
if (error instanceof FleetCommandRegistryMissingError) return `${error.message}\n${FLEET_COMMANDS_REMEDY}`;
|
|
135
|
+
return error instanceof Error ? error.message : String(error);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The contract position a compiled node came from; a loop half keeps its position id. */
|
|
139
|
+
function positionId(step: ExecutionPlanStep): string {
|
|
140
|
+
return step.loop === undefined ? step.id : `${step.loop.loopId}.${step.loop.role}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Compile the plan and project it into waves. Returns diagnostics instead of
|
|
145
|
+
* throwing: an operator meets a broken contract in the overlay, with every
|
|
146
|
+
* reason it cannot run, and no accept key.
|
|
147
|
+
*/
|
|
148
|
+
export function compileFleetRunPreview(input: FleetRunPreviewInput): FleetRunPreviewResult {
|
|
149
|
+
const diagnostics: string[] = [];
|
|
150
|
+
const fail = (): FleetRunPreviewResult => ({ ok: false, name: input.name, diagnostics });
|
|
151
|
+
|
|
152
|
+
let contract: FleetContract;
|
|
153
|
+
let commands: FleetCommandRegistry | null;
|
|
154
|
+
try {
|
|
155
|
+
({ contract, commands } = (input.load ?? defaultLoad)(input.workspaceRoot, input.name));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
diagnostics.push(describeError(error));
|
|
158
|
+
return fail();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let task: string;
|
|
162
|
+
try {
|
|
163
|
+
validateFleetGraph(contract);
|
|
164
|
+
validateFleetCommands(contract, commands);
|
|
165
|
+
task = renderFleetPrompt(contract.body, input.vars);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
diagnostics.push(describeError(error));
|
|
168
|
+
return fail();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let plan: ExecutionPlan;
|
|
172
|
+
try {
|
|
173
|
+
plan = compileFleetExecutionPlan({
|
|
174
|
+
contract,
|
|
175
|
+
task,
|
|
176
|
+
resolveAgent(context) {
|
|
177
|
+
const spec = input.getAgentSpec(context.agentId);
|
|
178
|
+
if (spec === null) {
|
|
179
|
+
throw new Error(`unknown agent '${context.agentId}' (step '${context.stepId}' must name a recipe from /agents)`);
|
|
180
|
+
}
|
|
181
|
+
if (spec.capabilityClass === "orchestration" || spec.capabilityClass === "internal") {
|
|
182
|
+
throw new Error(`fleet step '${context.stepId}' has no automatable agent authority`);
|
|
183
|
+
}
|
|
184
|
+
const requestRole = requestExecutionRole({
|
|
185
|
+
agentId: context.agentId,
|
|
186
|
+
resolveFacts: input.roleFacts,
|
|
187
|
+
...(context.gateRole !== undefined ? { gateRole: context.gateRole } : {}),
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
requestedAuthority: spec.capabilityClass,
|
|
191
|
+
approvedAuthority: spec.capabilityClass,
|
|
192
|
+
// A gate decider answers the coordinator's question, so its
|
|
193
|
+
// postcondition is the gate result contract, not its recipe's.
|
|
194
|
+
expectedResultContract:
|
|
195
|
+
context.planRole === true
|
|
196
|
+
? "delegation-plan"
|
|
197
|
+
: context.gateAuthorRole === true
|
|
198
|
+
? "artifact-report"
|
|
199
|
+
: context.gateRole === "reviewer"
|
|
200
|
+
? "verifier-report"
|
|
201
|
+
: spec.resultContract.kind,
|
|
202
|
+
executionRole: withAttemptRole(requestRole, context.attempt),
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
diagnostics.push(describeError(error));
|
|
208
|
+
return fail();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// A declared boundary is verified against the checkout, so the checkout has
|
|
212
|
+
// to be one this can read. Refused here, before any dispatch, rather than
|
|
213
|
+
// discovered as an unenforceable claim halfway through the run.
|
|
214
|
+
try {
|
|
215
|
+
preflightWriteBoundaries(plan, input.workspaceRoot);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
diagnostics.push(describeError(error));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const budget: FleetRunPreviewBudget = {
|
|
221
|
+
ceilingUsd: input.budget?.ceilingUsd ?? 0,
|
|
222
|
+
currentUsd: input.budget?.currentUsd ?? 0,
|
|
223
|
+
contractUsd: contract.budgetUsd,
|
|
224
|
+
};
|
|
225
|
+
if (input.budget) {
|
|
226
|
+
if (input.budget.verdict === "over" || input.budget.verdict === "at") {
|
|
227
|
+
diagnostics.push(`budget ceiling crossed: $${budget.currentUsd.toFixed(4)} / $${budget.ceilingUsd.toFixed(4)}`);
|
|
228
|
+
} else if (contract.budgetUsd !== null) {
|
|
229
|
+
const remaining = budget.ceilingUsd - budget.currentUsd;
|
|
230
|
+
if (contract.budgetUsd > remaining) {
|
|
231
|
+
diagnostics.push(
|
|
232
|
+
`fleet budget $${contract.budgetUsd.toFixed(2)} exceeds remaining session budget $${remaining.toFixed(2)}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const boundaries = new Map(fleetStepBoundaries(contract).map((entry) => [entry.id, entry.writes]));
|
|
239
|
+
const byId = new Map(plan.steps.map((step) => [step.id, step]));
|
|
240
|
+
const waves: FleetRunPreviewWave[] = plan.waves.map((wave, index) => ({
|
|
241
|
+
index,
|
|
242
|
+
steps: wave.flatMap((stepId): FleetRunPreviewStep[] => {
|
|
243
|
+
const step = byId.get(stepId);
|
|
244
|
+
if (step === undefined) return [];
|
|
245
|
+
const declared = boundaries.has(positionId(step)) ? boundaries.get(positionId(step)) : step.writes;
|
|
246
|
+
const base = {
|
|
247
|
+
stepId: step.id,
|
|
248
|
+
scope: step.scope,
|
|
249
|
+
writes: declared,
|
|
250
|
+
...(step.loop !== undefined ? { loop: { ...step.loop } } : {}),
|
|
251
|
+
...(step.gate !== undefined ? { gate: { ...step.gate } } : {}),
|
|
252
|
+
};
|
|
253
|
+
if (step.kind === "code") {
|
|
254
|
+
const command = commands?.commands.get(step.commandId);
|
|
255
|
+
return [
|
|
256
|
+
{
|
|
257
|
+
...base,
|
|
258
|
+
kind: "code" as const,
|
|
259
|
+
commandId: step.commandId,
|
|
260
|
+
...(command !== undefined ? { argv: [...command.argv] } : {}),
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
let route: FleetRunPreviewRoute | null = null;
|
|
265
|
+
try {
|
|
266
|
+
route =
|
|
267
|
+
input.resolveRoute?.({
|
|
268
|
+
stepId: step.id,
|
|
269
|
+
agentId: step.agentId,
|
|
270
|
+
task: step.task,
|
|
271
|
+
scope: step.scope,
|
|
272
|
+
...(step.target !== undefined ? { target: step.target } : {}),
|
|
273
|
+
...(step.profile !== undefined ? { profile: step.profile } : {}),
|
|
274
|
+
}) ?? null;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
diagnostics.push(`step '${step.id}' route preflight failed: ${describeError(error)}`);
|
|
277
|
+
}
|
|
278
|
+
return [
|
|
279
|
+
{
|
|
280
|
+
...base,
|
|
281
|
+
kind: "agent" as const,
|
|
282
|
+
agentId: step.agentId,
|
|
283
|
+
...(step.target !== undefined ? { target: step.target } : {}),
|
|
284
|
+
...(step.profile !== undefined ? { profile: step.profile } : {}),
|
|
285
|
+
...(step.plan !== undefined ? { plan: { ...step.plan, roster: [...step.plan.roster] } } : {}),
|
|
286
|
+
...(route !== null ? { route } : {}),
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
}),
|
|
290
|
+
}));
|
|
291
|
+
if (diagnostics.length > 0) return fail();
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
ok: true,
|
|
295
|
+
preview: {
|
|
296
|
+
name: contract.name,
|
|
297
|
+
vars: { ...input.vars },
|
|
298
|
+
planHash: plan.hash,
|
|
299
|
+
waves,
|
|
300
|
+
budget,
|
|
301
|
+
plan,
|
|
302
|
+
contract,
|
|
303
|
+
commands,
|
|
304
|
+
task,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
@@ -79,6 +79,225 @@ export function classifyNoticeLevel(text: string): NotificationLevel {
|
|
|
79
79
|
return "info";
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Desktop notification: a content-free nudge to a terminal that is not on
|
|
84
|
+
* screen right now.
|
|
85
|
+
*
|
|
86
|
+
* The payload is deliberately not a message. The title is fixed and the body
|
|
87
|
+
* comes from a closed vocabulary, so a notification that lands on a shared
|
|
88
|
+
* screen, a phone mirroring notifications, or a corporate notification log
|
|
89
|
+
* cannot leak a prompt, a path, a model answer, or a worker's task text. What
|
|
90
|
+
* it carries is that something needs the operator, and nothing more.
|
|
91
|
+
*/
|
|
92
|
+
export const DESKTOP_NOTIFY_TITLE = "clio-coder";
|
|
93
|
+
|
|
94
|
+
/** Maximum body length in bytes after sanitization. */
|
|
95
|
+
export const DESKTOP_NOTIFY_BODY_MAX_BYTES = 128;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The three events that earn a notification. `batch` carries the batch's short
|
|
99
|
+
* id because an operator running several fan-outs needs to know which one came
|
|
100
|
+
* back; the id is a generated identifier, never operator or model text.
|
|
101
|
+
*/
|
|
102
|
+
export type DesktopNotifyEvent =
|
|
103
|
+
| { kind: "turn-finished" }
|
|
104
|
+
| { kind: "batch-settled"; shortId: string }
|
|
105
|
+
| { kind: "approval-needed" };
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Terminals whose OSC 777 support is absent or unreliable and which understand
|
|
109
|
+
* OSC 9 instead. Matched case-insensitively against TERM_PROGRAM and, for
|
|
110
|
+
* Windows Terminal, against the presence of WT_SESSION.
|
|
111
|
+
*/
|
|
112
|
+
const OSC9_TERM_PROGRAMS: ReadonlyArray<string> = [
|
|
113
|
+
"iterm.app",
|
|
114
|
+
"iterm2",
|
|
115
|
+
"windowsterminal",
|
|
116
|
+
"windows terminal",
|
|
117
|
+
"conemu",
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const BEL = "\u0007";
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Strip C0/C1 controls and the `;` an OSC payload uses as its own separator.
|
|
124
|
+
*
|
|
125
|
+
* The classification is by code point rather than by a character-class regex:
|
|
126
|
+
* a literal control range inside a pattern is exactly the thing the payload
|
|
127
|
+
* must not carry, and spelling one out is how it gets copied in by accident.
|
|
128
|
+
*/
|
|
129
|
+
function sanitizeNotifyText(text: string): string {
|
|
130
|
+
let out = "";
|
|
131
|
+
for (const char of text) {
|
|
132
|
+
const code = char.codePointAt(0) ?? 0;
|
|
133
|
+
const isControl = code <= 0x1f || (code >= 0x7f && code <= 0x9f);
|
|
134
|
+
out += isControl || char === ";" ? " " : char;
|
|
135
|
+
}
|
|
136
|
+
return out.replace(/\s+/gu, " ").trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Clamp to the byte bound without splitting a multi-byte character. */
|
|
140
|
+
function boundNotifyBytes(text: string, maxBytes: number): string {
|
|
141
|
+
const encoder = new TextEncoder();
|
|
142
|
+
if (encoder.encode(text).length <= maxBytes) return text;
|
|
143
|
+
let out = "";
|
|
144
|
+
let used = 0;
|
|
145
|
+
for (const char of text) {
|
|
146
|
+
const size = encoder.encode(char).length;
|
|
147
|
+
if (used + size > maxBytes) break;
|
|
148
|
+
out += char;
|
|
149
|
+
used += size;
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The closed-vocabulary body for one event. */
|
|
155
|
+
export function desktopNotifyBody(event: DesktopNotifyEvent): string {
|
|
156
|
+
if (event.kind === "turn-finished") return "turn finished";
|
|
157
|
+
if (event.kind === "approval-needed") return "approval needed";
|
|
158
|
+
const shortId = boundNotifyBytes(sanitizeNotifyText(event.shortId), 32);
|
|
159
|
+
return shortId.length > 0 ? `batch ${shortId} settled` : "batch settled";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** True when the environment names a terminal that answers OSC 9 rather than OSC 777. */
|
|
163
|
+
export function prefersOsc9(env: Readonly<Record<string, string | undefined>>): boolean {
|
|
164
|
+
if (typeof env.WT_SESSION === "string" && env.WT_SESSION.length > 0) return true;
|
|
165
|
+
const program = (env.TERM_PROGRAM ?? "").trim().toLowerCase();
|
|
166
|
+
if (program.length === 0) return false;
|
|
167
|
+
return OSC9_TERM_PROGRAMS.some((candidate) => program === candidate || program.includes(candidate));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build the one escape sequence for an event. OSC 777 is the default and
|
|
172
|
+
* carries both the fixed title and the body; OSC 9 carries only the body and is
|
|
173
|
+
* chosen for the terminals that implement it instead. Exactly one sequence is
|
|
174
|
+
* returned, so a single event can never produce two notifications.
|
|
175
|
+
*/
|
|
176
|
+
export function buildDesktopNotifySequence(
|
|
177
|
+
event: DesktopNotifyEvent,
|
|
178
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
179
|
+
): string {
|
|
180
|
+
const body = boundNotifyBytes(sanitizeNotifyText(desktopNotifyBody(event)), DESKTOP_NOTIFY_BODY_MAX_BYTES);
|
|
181
|
+
if (prefersOsc9(env)) return `\u001b]9;${body}${BEL}`;
|
|
182
|
+
return `\u001b]777;notify;${DESKTOP_NOTIFY_TITLE};${body}${BEL}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface DesktopNotifierDeps {
|
|
186
|
+
/**
|
|
187
|
+
* Protocol write on the terminal owner. It happens outside a render
|
|
188
|
+
* transaction, so the trace records it with `frameId: null` and it never
|
|
189
|
+
* lands inside a frame.
|
|
190
|
+
*/
|
|
191
|
+
write: (data: string) => void;
|
|
192
|
+
/** Live `terminal.notify`, re-read per event so a hot config reload takes effect. */
|
|
193
|
+
enabled: () => boolean;
|
|
194
|
+
/**
|
|
195
|
+
* True only on the interactive TTY path. Headless, ACP, and non-TTY runs
|
|
196
|
+
* pass false (or wire no notifier at all) and never emit a sequence.
|
|
197
|
+
*/
|
|
198
|
+
interactiveTty: () => boolean;
|
|
199
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface DesktopNotifier {
|
|
203
|
+
notify(event: DesktopNotifyEvent): void;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Emit content-free desktop notifications for the three operator-waiting
|
|
208
|
+
* events. Every call re-checks the setting and the surface, so turning
|
|
209
|
+
* `terminal.notify` off mid-session silences the next event.
|
|
210
|
+
*/
|
|
211
|
+
export function createDesktopNotifier(deps: DesktopNotifierDeps): DesktopNotifier {
|
|
212
|
+
const env = deps.env ?? process.env;
|
|
213
|
+
return {
|
|
214
|
+
notify(event) {
|
|
215
|
+
if (!deps.interactiveTty()) return;
|
|
216
|
+
if (!deps.enabled()) return;
|
|
217
|
+
try {
|
|
218
|
+
deps.write(buildDesktopNotifySequence(event, env));
|
|
219
|
+
} catch {
|
|
220
|
+
// A terminal that refuses the write is not a reason to interrupt the
|
|
221
|
+
// turn that produced the event.
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Minimal projection of a detached batch, matching `DetachedBatchNudgeView`. */
|
|
228
|
+
export interface DesktopNotifyBatchView {
|
|
229
|
+
id: string;
|
|
230
|
+
total: number;
|
|
231
|
+
terminal: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface InteractiveDesktopNotificationsDeps extends DesktopNotifierDeps {
|
|
235
|
+
/**
|
|
236
|
+
* Live detached-batch progress. Read on every dispatch terminal event so a
|
|
237
|
+
* batch whose last run just finished is announced exactly once.
|
|
238
|
+
*/
|
|
239
|
+
getOpenBatches?: () => ReadonlyArray<DesktopNotifyBatchView>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface InteractiveDesktopNotifications {
|
|
243
|
+
/** A model turn reached its end. */
|
|
244
|
+
turnEnded(): void;
|
|
245
|
+
/** A dispatch run reached a terminal state; announces any batch that settled with it. */
|
|
246
|
+
dispatchSettled(): void;
|
|
247
|
+
/** A worker permission or an ask_user request parked waiting for the operator. */
|
|
248
|
+
approvalParked(): void;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Batch ids stay short in the payload: enough to tell two fan-outs apart. */
|
|
252
|
+
const BATCH_SHORT_ID_CHARS = 8;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The three notification events, wired to the surfaces that produce them.
|
|
256
|
+
*
|
|
257
|
+
* Batch settlement has no event of its own on the bus, so it is derived: every
|
|
258
|
+
* dispatch terminal event recomputes open-batch progress and any batch that
|
|
259
|
+
* crossed from running to fully terminal since the last look is announced. The
|
|
260
|
+
* announced set is remembered, so a batch that stays collectible for another
|
|
261
|
+
* ten minutes does not re-notify on every later run.
|
|
262
|
+
*/
|
|
263
|
+
export function createInteractiveDesktopNotifications(
|
|
264
|
+
deps: InteractiveDesktopNotificationsDeps,
|
|
265
|
+
): InteractiveDesktopNotifications {
|
|
266
|
+
const notifier = createDesktopNotifier(deps);
|
|
267
|
+
const announcedBatches = new Set<string>();
|
|
268
|
+
return {
|
|
269
|
+
turnEnded(): void {
|
|
270
|
+
notifier.notify({ kind: "turn-finished" });
|
|
271
|
+
},
|
|
272
|
+
dispatchSettled(): void {
|
|
273
|
+
const getOpenBatches = deps.getOpenBatches;
|
|
274
|
+
if (!getOpenBatches) return;
|
|
275
|
+
let views: ReadonlyArray<DesktopNotifyBatchView>;
|
|
276
|
+
try {
|
|
277
|
+
views = getOpenBatches();
|
|
278
|
+
} catch {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const open = new Set<string>();
|
|
282
|
+
for (const view of views) {
|
|
283
|
+
open.add(view.id);
|
|
284
|
+
if (view.total <= 0 || view.terminal < view.total) continue;
|
|
285
|
+
if (announcedBatches.has(view.id)) continue;
|
|
286
|
+
announcedBatches.add(view.id);
|
|
287
|
+
notifier.notify({ kind: "batch-settled", shortId: view.id.slice(0, BATCH_SHORT_ID_CHARS) });
|
|
288
|
+
}
|
|
289
|
+
// A collected batch leaves the open list; forget it so its id can be
|
|
290
|
+
// announced again if the store ever reuses it.
|
|
291
|
+
for (const id of [...announcedBatches]) {
|
|
292
|
+
if (!open.has(id)) announcedBatches.delete(id);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
approvalParked(): void {
|
|
296
|
+
notifier.notify({ kind: "approval-needed" });
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
82
301
|
function resolveExpiry(level: NotificationLevel, addedAt: number, ttlMs: number | undefined): number | null {
|
|
83
302
|
if (ttlMs !== undefined) return ttlMs <= 0 ? null : addedAt + ttlMs;
|
|
84
303
|
return level === "info" || level === "success" ? addedAt + DEFAULT_INFO_TTL_MS : null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/handoff` extraction round: one model call beside the session, no tools.
|
|
3
|
+
*
|
|
4
|
+
* It rides the same out-of-turn seam `/btw` uses, for the same reason: the
|
|
5
|
+
* input is the compiled message history the next turn would see, and the round
|
|
6
|
+
* must not enter the turn state machine, the transcript, or the ledger. The
|
|
7
|
+
* only difference is the instruction and the output contract, which is JSON
|
|
8
|
+
* validated against `HANDOFF_RESPONSE_SCHEMA` on the way back.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { HANDOFF_RESPONSE_SCHEMA } from "../domains/session/handoff.js";
|
|
12
|
+
import type { AgentMessage, EngineModel } from "../engine/types.js";
|
|
13
|
+
import { runOutOfTurnRound, type SideQuestionResult } from "./side-question.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The whole system prompt for the round. The session's own compiled prompt is
|
|
17
|
+
* not reused: it carries the working agreement for a turn that edits files and
|
|
18
|
+
* calls tools, and this round does neither.
|
|
19
|
+
*/
|
|
20
|
+
export const HANDOFF_SYSTEM_PROMPT = [
|
|
21
|
+
"You are extracting a handoff record from a coding session that is about to end.",
|
|
22
|
+
"The conversation above is read-only context.",
|
|
23
|
+
"Answer with one JSON object and nothing else: no prose, no code fence, no explanation.",
|
|
24
|
+
"decisions are choices this session settled, each with a summary and an optional rationale.",
|
|
25
|
+
"facts are durable statements about the work that the next session needs.",
|
|
26
|
+
"files name paths this session actually opened, edited, or searched, each with why it matters.",
|
|
27
|
+
"commands are shell invocations worth repeating, each with why.",
|
|
28
|
+
"openQuestions are what is still unresolved.",
|
|
29
|
+
"Never invent a file path. Every path must be one this conversation shows a tool call touching.",
|
|
30
|
+
"Leave a list empty rather than filling it with guesses.",
|
|
31
|
+
`The object must match this JSON schema exactly: ${JSON.stringify(HANDOFF_RESPONSE_SCHEMA)}`,
|
|
32
|
+
].join(" ");
|
|
33
|
+
|
|
34
|
+
/** A handoff record is a list of short entries, so the budget is modest. */
|
|
35
|
+
export const HANDOFF_MAX_TOKENS = 4096;
|
|
36
|
+
|
|
37
|
+
export interface HandoffRoundInput {
|
|
38
|
+
model: EngineModel;
|
|
39
|
+
/** Read-only. The round copies before appending its own message. */
|
|
40
|
+
messages: ReadonlyArray<AgentMessage>;
|
|
41
|
+
goal: string;
|
|
42
|
+
apiKey?: string;
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
maxTokens?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Run the round. Provider failures reject; the caller renders the reason. */
|
|
48
|
+
export async function runHandoffRound(input: HandoffRoundInput): Promise<SideQuestionResult> {
|
|
49
|
+
const { goal, ...rest } = input;
|
|
50
|
+
return runOutOfTurnRound({
|
|
51
|
+
...rest,
|
|
52
|
+
maxTokens: input.maxTokens ?? HANDOFF_MAX_TOKENS,
|
|
53
|
+
systemPrompt: HANDOFF_SYSTEM_PROMPT,
|
|
54
|
+
userText: `The next session's goal is: ${goal}\n\nExtract the handoff record for that goal now.`,
|
|
55
|
+
});
|
|
56
|
+
}
|