rovecode 0.3.2
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/LICENSE +662 -0
- package/README.md +737 -0
- package/THIRD_PARTY_NOTICES.md +268 -0
- package/bin/rovecode.ts +21 -0
- package/package.json +56 -0
- package/src/acp/server.ts +374 -0
- package/src/cli/auth-login.ts +122 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +82 -0
- package/src/cli/doctor.ts +362 -0
- package/src/cli/export.ts +276 -0
- package/src/cli/help.ts +293 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +583 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-login.ts +141 -0
- package/src/cli/mcp-market-cmd.ts +302 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +250 -0
- package/src/cli/repomap-root.ts +14 -0
- package/src/cli/resume.ts +57 -0
- package/src/cli/run-flags.ts +43 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +931 -0
- package/src/cli/session-arg.ts +30 -0
- package/src/cli/sessions-cmd.ts +145 -0
- package/src/cli/setup.ts +153 -0
- package/src/cli/skills-cmd.ts +194 -0
- package/src/cli/start-chat.ts +65 -0
- package/src/cli/trust-cmd.ts +52 -0
- package/src/coding/bash.ts +148 -0
- package/src/coding/checkpoints.ts +327 -0
- package/src/coding/diff.ts +138 -0
- package/src/coding/files.ts +341 -0
- package/src/coding/hashline.ts +274 -0
- package/src/coding/lsp-gate.ts +254 -0
- package/src/coding/lsp-servers.ts +147 -0
- package/src/coding/lsp.ts +283 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +192 -0
- package/src/coding/repomap.ts +481 -0
- package/src/core/agents.ts +255 -0
- package/src/core/compaction.ts +259 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +454 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +411 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +266 -0
- package/src/core/loop.ts +567 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +245 -0
- package/src/core/proc-group.ts +48 -0
- package/src/core/project-trust.ts +98 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +186 -0
- package/src/core/session-id.ts +24 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session-ops.ts +183 -0
- package/src/core/session-text.ts +29 -0
- package/src/core/session.ts +469 -0
- package/src/core/settings.ts +170 -0
- package/src/core/tasks.ts +646 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tools.ts +309 -0
- package/src/core/trust.ts +104 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +236 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/core/workspace.ts +184 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +215 -0
- package/src/eval/gauntlet-support.ts +84 -0
- package/src/eval/gauntlet-wave3.ts +269 -0
- package/src/eval/gauntlet-wave4.ts +217 -0
- package/src/eval/gauntlet.ts +253 -0
- package/src/index.ts +17 -0
- package/src/lanes/agy.ts +95 -0
- package/src/lanes/approval.ts +24 -0
- package/src/lanes/claude.ts +129 -0
- package/src/lanes/codex.ts +127 -0
- package/src/lanes/events.ts +130 -0
- package/src/lanes/job.ts +142 -0
- package/src/lanes/opencode.ts +122 -0
- package/src/lanes/process.ts +184 -0
- package/src/lanes/progress.ts +183 -0
- package/src/lanes/registry.ts +178 -0
- package/src/lanes/runner.ts +124 -0
- package/src/lanes/types.ts +112 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +449 -0
- package/src/mcp/config.ts +252 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +362 -0
- package/src/mcp/oauth.ts +251 -0
- package/src/mcp/prompts-resources.ts +249 -0
- package/src/mcp/shared.ts +149 -0
- package/src/mcp/status.ts +67 -0
- package/src/mcp/tools.ts +275 -0
- package/src/mcp/transport.ts +122 -0
- package/src/mcp/trust.ts +25 -0
- package/src/memory/blocks.ts +278 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/scope.ts +182 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +119 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +124 -0
- package/src/plugins/manifest.ts +92 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +408 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +421 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/model-list.ts +23 -0
- package/src/providers/models-index.json +1 -0
- package/src/providers/oauth/common.ts +105 -0
- package/src/providers/oauth/device-code.ts +107 -0
- package/src/providers/oauth/github-copilot.ts +146 -0
- package/src/providers/oauth/loopback.ts +158 -0
- package/src/providers/oauth/openai.ts +163 -0
- package/src/providers/oauth/openrouter.ts +89 -0
- package/src/providers/oauth/pkce.ts +45 -0
- package/src/providers/oauth/registry.ts +39 -0
- package/src/providers/oauth/seam.ts +89 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +333 -0
- package/src/providers/responses.ts +209 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +566 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/providers/wire-responses.ts +79 -0
- package/src/providers/wire-select.ts +53 -0
- package/src/server/http.ts +291 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/crew-cards.ts +350 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +187 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/draw-wizard.ts +378 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +521 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +169 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +200 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +456 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +335 -0
- package/src/sextant/sextant-renderer.ts +574 -0
- package/src/sextant/sextant-repo.ts +140 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +493 -0
- package/src/skills/index.ts +387 -0
- package/src/skills/pack.ts +220 -0
- package/src/skills/spec.ts +162 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel-export.ts +122 -0
- package/src/telemetry/otel-lanes.ts +89 -0
- package/src/telemetry/otel-logs.ts +131 -0
- package/src/telemetry/otel-metrics.ts +136 -0
- package/src/telemetry/otel.ts +397 -0
- package/src/telemetry/otlp.ts +76 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/bash-bg.ts +94 -0
- package/src/tools/bash-jobs.ts +237 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +250 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +332 -0
- package/src/tools/websearch.ts +359 -0
- package/src/tui/agents-cmd.ts +41 -0
- package/src/tui/app.ts +749 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/boot-notes.ts +41 -0
- package/src/tui/builtin-prompts.ts +59 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/clipboard.ts +78 -0
- package/src/tui/commands.ts +283 -0
- package/src/tui/config-view.ts +53 -0
- package/src/tui/context-cmds.ts +282 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/crash-guard.ts +173 -0
- package/src/tui/focus-terminal.ts +34 -0
- package/src/tui/git-cmds.ts +273 -0
- package/src/tui/git-plain.ts +58 -0
- package/src/tui/info-cmd.ts +150 -0
- package/src/tui/input-plain.ts +76 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/memory-note.ts +77 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/notify-seq.ts +100 -0
- package/src/tui/notify.ts +318 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +428 -0
- package/src/tui/providers-cmd.ts +377 -0
- package/src/tui/reasoning-view.ts +56 -0
- package/src/tui/renderer.ts +128 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +148 -0
- package/src/tui/session-manage.ts +95 -0
- package/src/tui/sextant-attach.ts +102 -0
- package/src/tui/sextant-io.ts +202 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/shell-cmd.ts +158 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/staged-terminal.ts +50 -0
- package/src/tui/startup.ts +12 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/tui/trust-card.ts +107 -0
- package/src/tui/tui-commands.ts +87 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/** Lane registry (#47): the four adapters by id, the `ROVECODE_LANES_ALLOW` gate, the LaneOpts defaults
|
|
2
|
+
* (+ the user-side knobs that shape a lane's permissions), and the ONE function that turns a lane's
|
|
3
|
+
* options into the approval-card text — the TaskManager stores the same string on the TaskInfo, so the
|
|
4
|
+
* card and the running lane agree.
|
|
5
|
+
*
|
|
6
|
+
* THE GATE IS ON BY DEFAULT (changed 2026-09-07, at the user's request). It used to be an opt-in
|
|
7
|
+
* allow-list: unset meant no lane could start, so a CLI the user had installed was unreachable until
|
|
8
|
+
* they found an env knob nobody reads. Now an UNSET knob allows every adapter and a DEFINED knob is an
|
|
9
|
+
* exact list — `ROVECODE_LANES_ALLOW=` (empty) is the kill switch, and `=codex` still means codex only.
|
|
10
|
+
* Defined-wins is what makes that safe: turning the default on cannot silently widen a machine whose
|
|
11
|
+
* owner had already written a narrower list.
|
|
12
|
+
*
|
|
13
|
+
* What replaces the gate as the thing that stops a lane is INSTALLEDNESS. With lanes off, a missing CLI
|
|
14
|
+
* was academic; with lanes on it is the common case, and the honest place to say so is here, before a
|
|
15
|
+
* worktree exists. Left to the spawn it surfaces as `spawn failed: codex: ENOENT` after the lane has
|
|
16
|
+
* built a worktree and burned a turn — a refusal at the gate costs neither. The probe reads PATH only
|
|
17
|
+
* (plus PATHEXT on win32); it is injectable so no test ever depends on what this machine has installed,
|
|
18
|
+
* and it is memoised per (bin, PATH) because `task start` may be called in a loop.
|
|
19
|
+
*
|
|
20
|
+
* Turning the gate on removes the approval that came WITH it for anyone running permission=auto, where
|
|
21
|
+
* no card is ever shown. `laneStartNote` is the replacement: the text a surface states when a lane
|
|
22
|
+
* starts, at every permission level, so "an external CLI just began working in your repo" is never
|
|
23
|
+
* something the user has to infer from a spinner. It is the card's own sentence — one function builds
|
|
24
|
+
* both, so the note cannot drift from the flags the lane actually got. */
|
|
25
|
+
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import { delimiter, join } from "node:path";
|
|
28
|
+
import { isObj } from "./events.ts";
|
|
29
|
+
import { ADAPTER_IDS, isAdapterId, type AdapterId, type AgentAdapter, type LaneOpts } from "./types.ts";
|
|
30
|
+
import { claudeAdapter } from "./claude.ts";
|
|
31
|
+
import { codexAdapter } from "./codex.ts";
|
|
32
|
+
import { opencodeAdapter } from "./opencode.ts";
|
|
33
|
+
import { agyAdapter } from "./agy.ts";
|
|
34
|
+
|
|
35
|
+
export const ADAPTERS: Readonly<Record<AdapterId, AgentAdapter>> = {
|
|
36
|
+
claude: claudeAdapter, codex: codexAdapter, opencode: opencodeAdapter, agy: agyAdapter,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type Env = Readonly<Record<string, string | undefined>>;
|
|
40
|
+
|
|
41
|
+
// The knobs, by their documented names. The harness this came from routed every read through a central
|
|
42
|
+
// core/env.ts that also consulted a settings file; rovecode has no such reader — it reads ROVECODE_* off
|
|
43
|
+
// the environment in 45 places — so the two helpers below are the whole of it, and a lane knob behaves
|
|
44
|
+
// like every other rovecode knob. `envName` exists so the name is written once and displayed the same
|
|
45
|
+
// way it is read: a refusal that names the wrong variable is worse than no refusal.
|
|
46
|
+
/** the full variable name for a knob: `LANES_ALLOW` → `ROVECODE_LANES_ALLOW` */
|
|
47
|
+
export const envName = (knob: string): string => `ROVECODE_${knob}`;
|
|
48
|
+
/** read a knob; DEFINED wins, even when empty (the call sites treat "" exactly as they treat unset) */
|
|
49
|
+
const readEnv = (knob: string, env: Env = process.env): string | undefined => env[envName(knob)];
|
|
50
|
+
/** comma list of adapter ids that may be STARTED at all; UNSET = all four, empty = none (the kill switch) */
|
|
51
|
+
export const LANES_ALLOW_ENV = envName("LANES_ALLOW");
|
|
52
|
+
/** comma list of adapter ids that get an explicit allow-all (agy: `--dangerously-skip-permissions`) */
|
|
53
|
+
export const LANES_ALLOW_ALL_ENV = envName("LANES_ALLOW_ALL");
|
|
54
|
+
/** claude `--allowedTools` rules, comma-separated (default Read,Edit,Write) */
|
|
55
|
+
export const LANE_CLAUDE_ALLOW_ENV = envName("LANE_CLAUDE_ALLOW");
|
|
56
|
+
/** claude `--bare`: "1" forces it, "0" drops it; unset = on only when ANTHROPIC_API_KEY is set in the lane
|
|
57
|
+
* env (live-verified 2026-09-03: `--bare` authenticates with the key ALONE — the CLI's login is never read) */
|
|
58
|
+
export const LANE_CLAUDE_BARE_ENV = envName("LANE_CLAUDE_BARE");
|
|
59
|
+
/** claude's documented headless login (`claude setup-token`) — the CLI reads it itself when the lane is not
|
|
60
|
+
* `--bare` (process.ts KEPT_ENV_EXACT lets it through); the registry reads it only for the card's auth word */
|
|
61
|
+
export const CLAUDE_OAUTH_TOKEN_ENV = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
62
|
+
/** the knob suffix of a lane's model flag (`--model` / `-m provider/model`): LANE_<ID>_MODEL — read through
|
|
63
|
+
* ROVECODE_LANE_<ID>_MODEL — unset means the lane's CLI picks its own default, which is not ours to guess */
|
|
64
|
+
export const laneModelKnob = (id: AdapterId): string => `LANE_${id.toUpperCase()}_MODEL`;
|
|
65
|
+
/** the documented name of that knob: ROVECODE_LANE_<ID>_MODEL */
|
|
66
|
+
export const laneModelEnv = (id: AdapterId): string => envName(laneModelKnob(id));
|
|
67
|
+
/** codex `--sandbox`: read-only | workspace-write (default) */
|
|
68
|
+
export const LANE_CODEX_SANDBOX_ENV = envName("LANE_CODEX_SANDBOX");
|
|
69
|
+
/** wall-clock budget per lane in ms (default 15 min) */
|
|
70
|
+
export const LANE_TIMEOUT_ENV = envName("LANE_TIMEOUT_MS");
|
|
71
|
+
export const DEFAULT_LANE_TIMEOUT_MS = 15 * 60_000;
|
|
72
|
+
|
|
73
|
+
function idList(raw: string | undefined): Set<AdapterId> {
|
|
74
|
+
const out = new Set<AdapterId>();
|
|
75
|
+
for (const tok of (raw ?? "").split(",")) {
|
|
76
|
+
const id = tok.trim().toLowerCase();
|
|
77
|
+
if (isAdapterId(id)) out.add(id);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Adapter ids that may start. UNSET `ROVECODE_LANES_ALLOW` = all four; a DEFINED value is an exact
|
|
83
|
+
* list, so an empty string allows nothing (the kill switch) and unknown tokens are ignored. */
|
|
84
|
+
export const lanesAllowed = (env: Env = process.env): Set<AdapterId> => {
|
|
85
|
+
const raw = readEnv("LANES_ALLOW", env);
|
|
86
|
+
return raw === undefined ? new Set(ADAPTER_IDS) : idList(raw);
|
|
87
|
+
};
|
|
88
|
+
/** The binary an adapter would run, without building a lane: `command()` is pure and takes no process. */
|
|
89
|
+
export const laneBinary = (id: AdapterId, env: Env = process.env): string =>
|
|
90
|
+
ADAPTERS[id].command({ goal: "probe" }, laneOptsFor(id, ".", env)).bin;
|
|
91
|
+
|
|
92
|
+
/** Is `bin` runnable? PATH only (plus PATHEXT on win32) — the same lookup the OS would do at spawn.
|
|
93
|
+
* Injectable and memoised; a bare name with no PATH at all is assumed present rather than refused,
|
|
94
|
+
* because guessing "missing" would block a lane the OS could in fact have started. */
|
|
95
|
+
export type PathProbe = (bin: string, env: Env) => boolean;
|
|
96
|
+
const probeCache = new Map<string, boolean>();
|
|
97
|
+
export const onPath: PathProbe = (bin, env) => {
|
|
98
|
+
const path = env["PATH"] ?? env["Path"] ?? "";
|
|
99
|
+
const key = `${process.platform}|${bin}|${path}`;
|
|
100
|
+
const hit = probeCache.get(key);
|
|
101
|
+
if (hit !== undefined) return hit;
|
|
102
|
+
const ok = lookup(bin, path, env);
|
|
103
|
+
probeCache.set(key, ok);
|
|
104
|
+
return ok;
|
|
105
|
+
};
|
|
106
|
+
function lookup(bin: string, path: string, env: Env): boolean {
|
|
107
|
+
if (bin.includes("/") || bin.includes("\\")) return existsSync(bin); // an explicit path is not a PATH lookup
|
|
108
|
+
if (path === "") return true; // nothing to search: do not claim it is missing
|
|
109
|
+
const exts = process.platform === "win32"
|
|
110
|
+
? (env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean)
|
|
111
|
+
: [""];
|
|
112
|
+
for (const dir of path.split(delimiter)) {
|
|
113
|
+
if (dir === "") continue;
|
|
114
|
+
for (const ext of exts) if (existsSync(join(dir, bin + ext))) return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
/** test seam: forget what was probed (the cache is keyed by PATH, so only a test needs this) */
|
|
119
|
+
export const resetPathProbe = (): void => { probeCache.clear(); };
|
|
120
|
+
|
|
121
|
+
/** null when `id` may start; otherwise the refusal text `task start` returns (no process is spawned,
|
|
122
|
+
* and no worktree is built — both refusals below land before the lane exists). */
|
|
123
|
+
export function laneRefusal(id: AdapterId, env: Env = process.env, probe: PathProbe = onPath): string | null {
|
|
124
|
+
const allowed = lanesAllowed(env);
|
|
125
|
+
if (!allowed.has(id)) {
|
|
126
|
+
// Only reachable when the user wrote a list themselves, so the refusal talks about THEIR list
|
|
127
|
+
// rather than telling them to opt in to something that is already on by default.
|
|
128
|
+
const now = allowed.size ? [...allowed].join(",") : "none";
|
|
129
|
+
return `external lane '${id}' is not in ${LANES_ALLOW_ENV} (currently allowed: ${now}) — add it, or unset ${LANES_ALLOW_ENV} to allow all of ${ADAPTER_IDS.join(",")}`;
|
|
130
|
+
}
|
|
131
|
+
const bin = laneBinary(id, env);
|
|
132
|
+
if (!probe(bin, env)) return `external lane '${id}' needs the '${bin}' CLI, which is not on PATH — install it, or start a different lane (${ADAPTER_IDS.filter((o) => o !== id).join(", ")})`;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The sentence a surface states when a lane starts — shown at EVERY permission level, because
|
|
137
|
+
* permission=auto shows no approval card and an external CLI beginning work in the user's repo is
|
|
138
|
+
* not something to leave implicit. Built from `lanePermissions`, so it names the flags the lane
|
|
139
|
+
* actually got rather than a description of them. */
|
|
140
|
+
export function laneStartNote(id: AdapterId, env: Env = process.env, over: Partial<LaneOpts> = {}): string {
|
|
141
|
+
return `${lanePermissions(id, env, over)} — started; it works in its own worktree and its diff comes back as a patch`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function laneTimeoutFromEnv(env: Env = process.env): number {
|
|
145
|
+
const n = Number(readEnv("LANE_TIMEOUT_MS", env) ?? "");
|
|
146
|
+
return Number.isInteger(n) && n >= 1_000 ? n : DEFAULT_LANE_TIMEOUT_MS;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The options a lane of `id` runs with in `cwd` (its worktree): env knobs, then `over`; cwd always wins. */
|
|
150
|
+
export function laneOptsFor(id: AdapterId, cwd: string, env: Env = process.env, over: Partial<LaneOpts> = {}): LaneOpts {
|
|
151
|
+
const opts: LaneOpts = { cwd, timeoutMs: laneTimeoutFromEnv(env) };
|
|
152
|
+
const model = (readEnv(laneModelKnob(id), env) ?? "").trim();
|
|
153
|
+
if (model) opts.model = model;
|
|
154
|
+
if (id === "claude") {
|
|
155
|
+
const raw = readEnv("LANE_CLAUDE_ALLOW", env);
|
|
156
|
+
if (raw !== undefined) opts.allowlist = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
157
|
+
const bare = readEnv("LANE_CLAUDE_BARE", env);
|
|
158
|
+
opts.bare = bare === "1" ? true : bare === "0" ? false : (env["ANTHROPIC_API_KEY"] ?? "") !== "";
|
|
159
|
+
if ((env[CLAUDE_OAUTH_TOKEN_ENV] ?? "") !== "") opts.oauthToken = true;
|
|
160
|
+
}
|
|
161
|
+
if (id === "codex") {
|
|
162
|
+
const sb = readEnv("LANE_CODEX_SANDBOX", env);
|
|
163
|
+
opts.sandbox = sb === "read-only" ? "read-only" : "workspace-write";
|
|
164
|
+
}
|
|
165
|
+
if (id === "agy" && idList(readEnv("LANES_ALLOW_ALL", env)).has("agy")) opts.allowAll = true;
|
|
166
|
+
return { ...opts, ...over, cwd };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The approval-card text for a lane — `spawn codex lane · sandbox workspace-write · approval never · worktree`. */
|
|
170
|
+
export function lanePermissions(id: AdapterId, env: Env = process.env, over: Partial<LaneOpts> = {}): string {
|
|
171
|
+
return `spawn ${id} lane · ${ADAPTERS[id].permissionSummary(laneOptsFor(id, "<worktree>", env, over))}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** A `task start` args object naming an external lane → its card text; null for every other call. */
|
|
175
|
+
export function laneApprovalText(args: unknown, env: Env = process.env): string | null {
|
|
176
|
+
if (!isObj(args) || args["action"] !== "start" || !isAdapterId(args["agent"])) return null;
|
|
177
|
+
return lanePermissions(args["agent"], env);
|
|
178
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** Lane runner (#47): ONE external CLI process → LaneResult. Spawns through the injected LaneSpawn
|
|
2
|
+
* (default lanes/process.ts bunLaneSpawn = the executor's tree-kill path), reads stdout line by line,
|
|
3
|
+
* feeds the adapter's parse(), keeps a bounded log ring (the running preview), reports every event to
|
|
4
|
+
* the caller, enforces the wall-clock budget, and cancels in the order the CLIs want: SIGINT first for
|
|
5
|
+
* the ones that finish their turn on it (claude/opencode), the tree kill after a grace — or at once for
|
|
6
|
+
* the others — then the reader is abandoned after ABORT_GRACE_MS so an orphan holding the pipe cannot
|
|
7
|
+
* park the job. Every event also folds into the run's counters (lanes/progress.ts) — tool calls, confirmed
|
|
8
|
+
* file writes, usage — which ride along with each onEvent so a surface can show what the lane has DONE
|
|
9
|
+
* while it is still doing it, not just its last log line. Every timer is cleared on the way out (a pending timer would hold the process; a parked
|
|
10
|
+
* promise with no timer hangs the Bun runner). A signal that is ALREADY aborted when the runner is called
|
|
11
|
+
* spawns nothing at all (the job awaited its worktree meanwhile; executor.ts bunRunner's pre-aborted pin).
|
|
12
|
+
* The verdict: cancelled > timed out > the CLI's own fail/done event > exit-code fallback (≠0 → fail;
|
|
13
|
+
* 0 without a result event → fail, honestly). */
|
|
14
|
+
|
|
15
|
+
import { ABORT_GRACE_MS } from "../core/executor.ts";
|
|
16
|
+
import type { TokenUsage } from "../core/types.ts";
|
|
17
|
+
import { addUsage, renderEvent } from "./events.ts";
|
|
18
|
+
import { LaneTally, type LaneProgress } from "./progress.ts";
|
|
19
|
+
import { bunLaneSpawn, type LaneProcess, type LaneSpawn } from "./process.ts";
|
|
20
|
+
import { newParseState, type AgentAdapter, type LaneEvent, type LaneOpts, type LaneResult, type LaneTask } from "./types.ts";
|
|
21
|
+
|
|
22
|
+
/** rendered event lines kept while a lane runs (the preview + the failure context) */
|
|
23
|
+
export const LOG_RING = 200;
|
|
24
|
+
/** SIGINT → tree kill grace for interruptFirst adapters (docs/test.md §3.6: dead within 5 s) */
|
|
25
|
+
export const INTERRUPT_GRACE_MS = 3_000;
|
|
26
|
+
|
|
27
|
+
export type KillStep = "interrupt" | "kill" | "abandon";
|
|
28
|
+
|
|
29
|
+
export interface LaneRunnerDeps {
|
|
30
|
+
spawn?: LaneSpawn;
|
|
31
|
+
/** cancel: the TaskManager's per-task controller */
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
/** every parsed event, with the current log tail (≤3 lines) and the running counters — what the
|
|
34
|
+
* lane has DONE so far (progress.ts), so a surface can show more than one log line */
|
|
35
|
+
onEvent?: (ev: LaneEvent, tail: readonly string[], progress: LaneProgress) => void;
|
|
36
|
+
graceMs?: number;
|
|
37
|
+
/** the kill choreography, appended as it happens (tests pin the order) */
|
|
38
|
+
steps?: KillStep[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class LogRing {
|
|
42
|
+
private lines: string[] = [];
|
|
43
|
+
constructor(readonly cap: number) {}
|
|
44
|
+
push(line: string): void {
|
|
45
|
+
this.lines.push(line);
|
|
46
|
+
if (this.lines.length > this.cap) this.lines.splice(0, this.lines.length - this.cap);
|
|
47
|
+
}
|
|
48
|
+
tail(n = this.cap): string[] { return this.lines.slice(-n); }
|
|
49
|
+
get length(): number { return this.lines.length; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const msg = (e: unknown): string => (e instanceof Error ? e.message : String(e));
|
|
53
|
+
|
|
54
|
+
export async function runExternalLane(adapter: AgentAdapter, task: LaneTask, opts: LaneOpts, deps: LaneRunnerDeps = {}): Promise<LaneResult> {
|
|
55
|
+
// cancelled before the spawn (the job was still creating its worktree): nothing starts — executor.ts:110 idiom
|
|
56
|
+
if (deps.signal?.aborted) return { status: "cancelled", summary: "", error: "cancelled", exitCode: null, log: [], garbage: 0, progress: new LaneTally().snapshot() };
|
|
57
|
+
const cmd = opts.resume && adapter.resume ? adapter.resume(opts.resume, task.goal, opts) : adapter.command(task, opts);
|
|
58
|
+
const ring = new LogRing(LOG_RING);
|
|
59
|
+
const tally = new LaneTally();
|
|
60
|
+
const st = newParseState();
|
|
61
|
+
let proc: LaneProcess;
|
|
62
|
+
try {
|
|
63
|
+
proc = (deps.spawn ?? bunLaneSpawn)(cmd);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return { status: "failed", summary: "", error: `spawn failed: ${cmd.bin}: ${msg(e)}`, exitCode: null, log: [], garbage: 0, progress: tally.snapshot() };
|
|
66
|
+
}
|
|
67
|
+
let done: Extract<LaneEvent, { kind: "done" }> | undefined, fail: Extract<LaneEvent, { kind: "fail" }> | undefined;
|
|
68
|
+
let usage: TokenUsage | undefined;
|
|
69
|
+
let timedOut = false, cancelled = false, stopping = false;
|
|
70
|
+
// every timer lands here and is cleared in finally — so a SIGINT that ends the turn inside the grace
|
|
71
|
+
// (the runner returns) never fires the pending kill, and no timer outlives the lane
|
|
72
|
+
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
73
|
+
const later = (fn: () => void, ms: number): void => {
|
|
74
|
+
const t = setTimeout(() => { timers.delete(t); fn(); }, ms);
|
|
75
|
+
timers.add(t);
|
|
76
|
+
};
|
|
77
|
+
const step = (s: KillStep): void => { deps.steps?.push(s); };
|
|
78
|
+
const killNow = (): void => {
|
|
79
|
+
step("kill"); proc.kill();
|
|
80
|
+
later(() => { step("abandon"); proc.abandon(); }, ABORT_GRACE_MS);
|
|
81
|
+
};
|
|
82
|
+
const stop = (): void => {
|
|
83
|
+
if (stopping) return;
|
|
84
|
+
stopping = true;
|
|
85
|
+
if (adapter.interruptFirst && proc.interrupt()) { step("interrupt"); later(killNow, deps.graceMs ?? INTERRUPT_GRACE_MS); }
|
|
86
|
+
else killNow();
|
|
87
|
+
};
|
|
88
|
+
const onAbort = (): void => { cancelled = true; stop(); };
|
|
89
|
+
try {
|
|
90
|
+
deps.signal?.addEventListener("abort", onAbort, { once: true }); // not aborted: checked above, and the spawn is synchronous
|
|
91
|
+
later(() => { timedOut = true; stop(); }, Math.max(0, opts.timeoutMs));
|
|
92
|
+
for await (const raw of proc.lines()) {
|
|
93
|
+
const line = raw.trimEnd();
|
|
94
|
+
if (!line) continue;
|
|
95
|
+
let events: LaneEvent[];
|
|
96
|
+
try { events = adapter.parse(line, st); } catch { st.garbage++; continue; }
|
|
97
|
+
for (const ev of events) {
|
|
98
|
+
ring.push(renderEvent(ev));
|
|
99
|
+
tally.add(ev); // the counters and the log line come off the SAME event — nothing is parsed twice
|
|
100
|
+
if (ev.kind === "done") done = ev;
|
|
101
|
+
else if (ev.kind === "fail") fail = ev;
|
|
102
|
+
else if (ev.kind === "usage") usage = addUsage(usage, ev.usage);
|
|
103
|
+
deps.onEvent?.(ev, ring.tail(3), tally.snapshot());
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// EOF: the process is gone or about to be — never wait on it unbounded (an abandoned pipe may
|
|
107
|
+
// belong to a launcher that is still alive)
|
|
108
|
+
let waitTimer: ReturnType<typeof setTimeout> | undefined;
|
|
109
|
+
const code = await Promise.race([proc.exited, new Promise<null>((r) => { waitTimer = setTimeout(() => r(null), ABORT_GRACE_MS); })]);
|
|
110
|
+
clearTimeout(waitTimer);
|
|
111
|
+
const sessionId = done?.sessionId ?? fail?.sessionId ?? st.sessionId;
|
|
112
|
+
const base = { exitCode: code, log: ring.tail(), garbage: st.garbage, progress: tally.snapshot(), ...(usage ? { usage } : {}), ...(sessionId ? { sessionId } : {}) };
|
|
113
|
+
if (cancelled) return { ...base, status: "cancelled", summary: "", error: "cancelled" };
|
|
114
|
+
if (timedOut) return { ...base, status: "failed", summary: "", error: `timed out after ${opts.timeoutMs}ms` };
|
|
115
|
+
if (fail) return { ...base, status: "failed", summary: "", error: fail.error };
|
|
116
|
+
if (done) return { ...base, status: "done", summary: done.summary || st.lastText || "" };
|
|
117
|
+
const err = proc.stderrTail();
|
|
118
|
+
if (code !== 0) return { ...base, status: "failed", summary: "", error: `${cmd.bin} exited with code ${code ?? "?"} without a result${err ? `: ${err.slice(-300)}` : ""}` };
|
|
119
|
+
return { ...base, status: "failed", summary: "", error: `${cmd.bin} exited 0 without a result event${err ? `: ${err.slice(-300)}` : ""}` };
|
|
120
|
+
} finally {
|
|
121
|
+
deps.signal?.removeEventListener("abort", onAbort);
|
|
122
|
+
for (const t of timers) clearTimeout(t);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** External agentic-CLI lanes (port #47): the adapter contract between rovecode's TaskManager and a
|
|
2
|
+
* headless coding CLI — claude code, codex, opencode, antigravity (`agy`). Source: the user's own
|
|
3
|
+
* notes, sextant docs/agentic-clis.md §1 (adapter = command() + parse() + resume()) and §3 (the
|
|
4
|
+
* event → lane mapping table); flags verified against the official docs on 2026-09-02.
|
|
5
|
+
* A lane is a JOB of core/tasks.ts (TaskInfo.kind "external"), never a second loop (ADR-003/013):
|
|
6
|
+
* the CLI's JSONL stdout becomes LaneEvents, the events become TaskInfo updates, the process rides
|
|
7
|
+
* the executor's tree-kill path (lanes/process.ts over core/win-job.ts), and the lane works in its
|
|
8
|
+
* OWN git worktree whose diff merges back as a patch — exactly like an isolated child. */
|
|
9
|
+
|
|
10
|
+
import type { TokenUsage } from "../core/types.ts";
|
|
11
|
+
import type { LaneProgress } from "./progress.ts";
|
|
12
|
+
|
|
13
|
+
export type AdapterId = "claude" | "codex" | "opencode" | "agy";
|
|
14
|
+
export const ADAPTER_IDS: readonly AdapterId[] = ["claude", "codex", "opencode", "agy"];
|
|
15
|
+
export const isAdapterId = (s: unknown): s is AdapterId =>
|
|
16
|
+
typeof s === "string" && (ADAPTER_IDS as readonly string[]).includes(s);
|
|
17
|
+
|
|
18
|
+
export interface LaneTask { goal: string; label?: string }
|
|
19
|
+
|
|
20
|
+
export interface LaneOpts {
|
|
21
|
+
/** the lane's working directory — its OWN isolated worktree, never the parent tree */
|
|
22
|
+
cwd: string;
|
|
23
|
+
/** claude: `--allowedTools` rules ("Read", "Bash(npm test)"); the other CLIs have no allowlist flag */
|
|
24
|
+
allowlist?: string[];
|
|
25
|
+
/** claude: pass `--bare` (the table's CI shape — auth is STRICTLY `ANTHROPIC_API_KEY`; the CLI's own
|
|
26
|
+
* login is never read). Default true; the registry sets false when the lane env has no key so the
|
|
27
|
+
* lane runs on the CLI's login instead (verified 2026-09-03: `--bare` without a key exits 1 at once,
|
|
28
|
+
* `apiKeySource: "none"`; without `--bare` the same CLI reads its OAuth login). */
|
|
29
|
+
bare?: boolean;
|
|
30
|
+
/** claude: the lane env carries `CLAUDE_CODE_OAUTH_TOKEN` (`claude setup-token`, the documented headless
|
|
31
|
+
* login; the process seam keeps it). Label only — the card's auth word becomes `oauth token` instead of
|
|
32
|
+
* `cli login`; under `--bare` the word stays `bare` (`--help`: strictly ANTHROPIC_API_KEY, the token is never read). */
|
|
33
|
+
oauthToken?: boolean;
|
|
34
|
+
/** codex: `--sandbox`; default workspace-write (danger-full-access is deliberately not offered) */
|
|
35
|
+
sandbox?: "read-only" | "workspace-write";
|
|
36
|
+
/** wall-clock cutter enforced by the runner; agy also receives it as `--print-timeout` */
|
|
37
|
+
timeoutMs: number;
|
|
38
|
+
model?: string;
|
|
39
|
+
/** agy ONLY: an explicit allow-all adds `--dangerously-skip-permissions`; default = the soft-deny path */
|
|
40
|
+
allowAll?: boolean;
|
|
41
|
+
/** codex: `-o/--output-last-message` target; default = a file under the OS temp dir */
|
|
42
|
+
lastMessageFile?: string;
|
|
43
|
+
/** opencode: `--attach <url>` of a running `opencode serve` */
|
|
44
|
+
attach?: string;
|
|
45
|
+
/** continue a previous lane session (`--resume` / `exec resume` / `-s` / `--conversation`) */
|
|
46
|
+
resume?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface LaneCommand { bin: string; args: string[]; env?: Record<string, string>; stdin?: string; cwd: string }
|
|
50
|
+
|
|
51
|
+
/** The lane DSL (agentic-clis.md §1/§3): what the crew board and the TaskInfo updates consume. */
|
|
52
|
+
export type LaneEvent =
|
|
53
|
+
| { kind: "log"; text: string }
|
|
54
|
+
/** `wrote` means the CLI CONFIRMED this write (a tool result, a completed change) — never merely that
|
|
55
|
+
* the path appeared in a tool's arguments. lanes/progress.ts counts files on that flag alone. */
|
|
56
|
+
| { kind: "edit"; path: string; op: "edit" | "write" | "delete"; callId?: string; wrote?: true }
|
|
57
|
+
| { kind: "bash"; command: string; output?: string; exitCode?: number; callId?: string }
|
|
58
|
+
/** a tool that is neither a file edit nor a shell command (read, grep, todo, a CLI's own) — kept as a
|
|
59
|
+
* COUNTABLE event rather than folded into `log`, so "12 tool calls" includes the twenty greps a lane
|
|
60
|
+
* really made instead of silently reading zero */
|
|
61
|
+
| { kind: "tool"; name: string; detail?: string; callId?: string }
|
|
62
|
+
| { kind: "ask"; text: string }
|
|
63
|
+
| { kind: "progress"; text: string }
|
|
64
|
+
| { kind: "done"; summary: string; sessionId?: string }
|
|
65
|
+
| { kind: "fail"; error: string; sessionId?: string }
|
|
66
|
+
| { kind: "usage"; usage: TokenUsage };
|
|
67
|
+
|
|
68
|
+
export interface LaneParseState {
|
|
69
|
+
/** lines that were not JSON objects with a `type` — skipped and counted, never thrown */
|
|
70
|
+
garbage: number;
|
|
71
|
+
/** the CLI's own session/thread/conversation id, for resume() */
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
/** the last agent text seen: the summary fallback when the terminal event carries none */
|
|
74
|
+
lastText?: string;
|
|
75
|
+
/** adapter scratch (pending tool ids and their paths, partial text, dedupe keys) */
|
|
76
|
+
scratch: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
export const newParseState = (): LaneParseState => ({ garbage: 0, scratch: {} });
|
|
79
|
+
|
|
80
|
+
export interface AgentAdapter {
|
|
81
|
+
id: AdapterId;
|
|
82
|
+
/** SIGINT finishes the current turn for this CLI: the runner interrupts first, kills after a grace */
|
|
83
|
+
interruptFirst: boolean;
|
|
84
|
+
/** dry-run-able argv — EXACTLY the flags of agentic-clis.md §2, no process started */
|
|
85
|
+
command(task: LaneTask, opts: LaneOpts): LaneCommand;
|
|
86
|
+
/** one stdout line → zero or more lane events; garbage is skipped and counted, never thrown */
|
|
87
|
+
parse(line: string, st: LaneParseState): LaneEvent[];
|
|
88
|
+
/** continue the CLI's own session with a follow-up prompt */
|
|
89
|
+
resume?(sessionId: string, followUp: string, opts: LaneOpts): LaneCommand;
|
|
90
|
+
/** the lane's OWN permission flags, stated on the approval card BEFORE the lane starts */
|
|
91
|
+
permissionSummary(opts: LaneOpts): string;
|
|
92
|
+
/** attached to a `done` whose worktree diff is EMPTY (agy: soft-deny exits 0 having done nothing) */
|
|
93
|
+
emptyDiffNote?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface LaneResult {
|
|
97
|
+
status: "done" | "failed" | "cancelled";
|
|
98
|
+
/** what the lane did, folded from its own events (lanes/progress.ts) — present on every outcome,
|
|
99
|
+
* including a cancel, because "it made 9 calls and wrote 2 files before you stopped it" is exactly
|
|
100
|
+
* what a cancelled lane should still be able to tell you */
|
|
101
|
+
progress: LaneProgress;
|
|
102
|
+
/** the CLI's final message (done); empty otherwise */
|
|
103
|
+
summary: string;
|
|
104
|
+
error?: string;
|
|
105
|
+
usage?: TokenUsage;
|
|
106
|
+
sessionId?: string;
|
|
107
|
+
/** null = the process had not exited when the runner gave up waiting (abandoned pipe) */
|
|
108
|
+
exitCode: number | null;
|
|
109
|
+
/** bounded tail of the rendered event log (runner LOG_RING lines) */
|
|
110
|
+
log: string[];
|
|
111
|
+
garbage: number;
|
|
112
|
+
}
|