@lmctl-ai/lmctl 0.1.342 → 0.2.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.
Files changed (182) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +11 -51
  3. package/data/price-aliases.json +1 -0
  4. package/data/price-data.json +1 -1
  5. package/dist/cli/batch.d.ts +2 -0
  6. package/dist/cli/busy-evidence.d.ts +27 -0
  7. package/dist/cli/chat-cycle.d.ts +59 -0
  8. package/dist/cli/chat-exec.d.ts +83 -0
  9. package/dist/cli/chat-policy.d.ts +22 -0
  10. package/dist/cli/chat.d.ts +23 -0
  11. package/dist/cli/code-resolver.d.ts +8 -0
  12. package/dist/cli/common.d.ts +54 -0
  13. package/dist/cli/context.d.ts +12 -0
  14. package/dist/cli/cost.d.ts +2 -0
  15. package/dist/cli/export.d.ts +2 -0
  16. package/dist/cli/index.d.ts +3 -0
  17. package/dist/cli/index.js +1062 -1400
  18. package/dist/cli/infra.d.ts +78 -0
  19. package/dist/cli/ls.d.ts +2 -0
  20. package/dist/cli/mailbox-cli.d.ts +6 -0
  21. package/dist/cli/managed.d.ts +62 -0
  22. package/dist/cli/mcp-install.d.ts +23 -0
  23. package/dist/cli/mcp.d.ts +2 -0
  24. package/dist/cli/paths.d.ts +50 -0
  25. package/dist/cli/plan-wizard.d.ts +42 -0
  26. package/dist/cli/plan.d.ts +20 -0
  27. package/dist/cli/queue-delivery.d.ts +107 -0
  28. package/dist/cli/ratelimit.d.ts +3 -0
  29. package/dist/cli/remote-fetch.d.ts +7 -0
  30. package/dist/cli/run-queries.d.ts +82 -0
  31. package/dist/cli/running.d.ts +5 -0
  32. package/dist/cli/schema.sql +20 -1
  33. package/dist/cli/script.d.ts +6 -0
  34. package/dist/cli/script.js +2097 -0
  35. package/dist/cli/self-identity.d.ts +25 -0
  36. package/dist/cli/session-idle.d.ts +118 -0
  37. package/dist/cli/status.d.ts +22 -0
  38. package/dist/cli/tail.d.ts +18 -0
  39. package/dist/cli/target-resolver.d.ts +139 -0
  40. package/dist/cli/team-status.d.ts +91 -0
  41. package/dist/cli/teamfile-compat.d.ts +71 -0
  42. package/dist/cli/teamfile-store.d.ts +18 -0
  43. package/dist/cli/teamfile-template.d.ts +40 -0
  44. package/dist/cli/template-env.d.ts +25 -0
  45. package/dist/cli/terminal-lock.d.ts +25 -0
  46. package/dist/cli/terminal.d.ts +23 -0
  47. package/dist/cli/time-filter.d.ts +10 -0
  48. package/dist/cli/update-notifier.d.ts +11 -0
  49. package/dist/cli/version-util.d.ts +2 -0
  50. package/dist/config/seed_templates.d.ts +1 -0
  51. package/dist/db/backfill-event-log.d.ts +26 -0
  52. package/dist/db/delegation.d.ts +37 -0
  53. package/dist/db/escalation_vocab.d.ts +45 -0
  54. package/dist/db/event-log.d.ts +40 -0
  55. package/dist/db/index.d.ts +1213 -0
  56. package/dist/db/inflight-acquire.d.ts +70 -0
  57. package/dist/db/invocation-event-projections.d.ts +65 -0
  58. package/dist/db/ledger-identity.test.d.ts +1 -0
  59. package/dist/db/live-event-projections.d.ts +92 -0
  60. package/dist/db/migrate.d.ts +51 -0
  61. package/dist/db/open-readonly.d.ts +2 -0
  62. package/dist/db/process-liveness.d.ts +37 -0
  63. package/dist/db/sqlite.d.ts +47 -0
  64. package/dist/db/sqlite.test.d.ts +1 -0
  65. package/dist/db/terminal-lock.d.ts +14 -0
  66. package/dist/db/tracked-invocation-reconcile.d.ts +52 -0
  67. package/dist/db/tracked-invocation.d.ts +116 -0
  68. package/dist/mcp/dispatch.d.ts +45 -0
  69. package/dist/mcp/mediation.d.ts +128 -0
  70. package/dist/mcp/path-warning.d.ts +7 -0
  71. package/dist/mcp/server.d.ts +63 -0
  72. package/dist/mcp/tools.d.ts +24 -0
  73. package/dist/providers/adapter.d.ts +265 -0
  74. package/dist/providers/adapters/agy-adapter.d.ts +17 -0
  75. package/dist/providers/adapters/claude-adapter.d.ts +20 -0
  76. package/dist/providers/adapters/codex-adapter.d.ts +22 -0
  77. package/dist/providers/adapters/copilot-adapter.d.ts +16 -0
  78. package/dist/providers/adapters/dsh-adapter.d.ts +89 -0
  79. package/dist/providers/adapters/gemini-adapter.d.ts +44 -0
  80. package/dist/providers/adapters/hermes-adapter.d.ts +71 -0
  81. package/dist/providers/adapters/index.d.ts +35 -0
  82. package/dist/providers/adapters/kimi-adapter.d.ts +16 -0
  83. package/dist/providers/adapters/lmplayer-adapter.d.ts +14 -0
  84. package/dist/providers/adapters/opencode-adapter.d.ts +11 -0
  85. package/dist/providers/adapters/pi-adapter.d.ts +17 -0
  86. package/dist/providers/adapters/print-turn.d.ts +29 -0
  87. package/dist/providers/adapters/qwen-adapter.d.ts +11 -0
  88. package/dist/providers/agy.d.ts +32 -0
  89. package/dist/providers/base-adapter.d.ts +107 -0
  90. package/dist/providers/base.d.ts +12 -0
  91. package/dist/providers/binary-check.d.ts +102 -0
  92. package/dist/providers/bundle.d.ts +118 -0
  93. package/dist/providers/claude-mock.d.ts +90 -0
  94. package/dist/providers/claude.d.ts +21 -0
  95. package/dist/providers/codex-config.d.ts +15 -0
  96. package/dist/providers/codex.d.ts +25 -0
  97. package/dist/providers/config/model-selection-lint.d.ts +45 -0
  98. package/dist/providers/config/team-config.d.ts +183 -0
  99. package/dist/providers/config/types.d.ts +146 -0
  100. package/dist/providers/config/user-config.d.ts +88 -0
  101. package/dist/providers/copilot.d.ts +21 -0
  102. package/dist/providers/debug.d.ts +128 -0
  103. package/dist/providers/dsh-sidecar.d.ts +65 -0
  104. package/dist/providers/dsh-zstd.d.ts +27 -0
  105. package/dist/providers/dsh.d.ts +37 -0
  106. package/dist/providers/frontmatter.d.ts +46 -0
  107. package/dist/providers/gemini-session-reader.d.ts +151 -0
  108. package/dist/providers/gemini.d.ts +39 -0
  109. package/dist/providers/hermes.d.ts +90 -0
  110. package/dist/providers/index.d.ts +421 -0
  111. package/dist/providers/kimi.d.ts +37 -0
  112. package/dist/providers/model-catalog.d.ts +141 -0
  113. package/dist/providers/one-shot-timeout.d.ts +1 -0
  114. package/dist/providers/opencode-config.d.ts +62 -0
  115. package/dist/providers/opencode.d.ts +40 -0
  116. package/dist/providers/parsers/acp.d.ts +47 -0
  117. package/dist/providers/parsers/codex-app-server.d.ts +60 -0
  118. package/dist/providers/parsers/stream-json.d.ts +57 -0
  119. package/dist/providers/passive-readers/agy.d.ts +155 -0
  120. package/dist/providers/passive-readers/copilot.d.ts +173 -0
  121. package/dist/providers/passive-readers/dsh.d.ts +34 -0
  122. package/dist/providers/passive-readers/kimi.d.ts +44 -0
  123. package/dist/providers/passive-readers/opencode.d.ts +89 -0
  124. package/dist/providers/passive-readers/qwen.d.ts +151 -0
  125. package/dist/providers/passive-readers/shared.d.ts +138 -0
  126. package/dist/providers/passive-session-reader.d.ts +188 -0
  127. package/dist/providers/path-utils.d.ts +1 -0
  128. package/dist/providers/pi.d.ts +32 -0
  129. package/dist/providers/pricing.d.ts +107 -0
  130. package/dist/providers/qwen.d.ts +22 -0
  131. package/dist/providers/ratelimit-readings.d.ts +32 -0
  132. package/dist/providers/registry.d.ts +31 -0
  133. package/dist/providers/resume.d.ts +43 -0
  134. package/dist/providers/seed-instruction.d.ts +3 -0
  135. package/dist/providers/self-session-env.d.ts +21 -0
  136. package/dist/providers/session-limit.d.ts +9 -0
  137. package/dist/providers/strip-ansi.d.ts +5 -0
  138. package/dist/providers/transport-errors.d.ts +94 -0
  139. package/dist/providers/transport-lifecycle.d.ts +63 -0
  140. package/dist/providers/transport-wrappers.d.ts +110 -0
  141. package/dist/providers/transports/acp-profiles/config-option-step.d.ts +24 -0
  142. package/dist/providers/transports/acp-profiles/copilot.d.ts +12 -0
  143. package/dist/providers/transports/acp-profiles/gemini.d.ts +22 -0
  144. package/dist/providers/transports/acp-profiles/hermes.d.ts +27 -0
  145. package/dist/providers/transports/acp-profiles/index.d.ts +19 -0
  146. package/dist/providers/transports/acp-profiles/kimi.d.ts +24 -0
  147. package/dist/providers/transports/acp-profiles/opencode.d.ts +26 -0
  148. package/dist/providers/transports/acp-profiles/types.d.ts +258 -0
  149. package/dist/providers/transports/acp-rpc.d.ts +18 -0
  150. package/dist/providers/transports/acp.d.ts +215 -0
  151. package/dist/providers/transports/codex-app-server.d.ts +190 -0
  152. package/dist/providers/transports/hold-open.d.ts +120 -0
  153. package/dist/providers/transports/manager.d.ts +21 -0
  154. package/dist/providers/transports/stream-json.d.ts +162 -0
  155. package/dist/providers/transports/types.d.ts +56 -0
  156. package/dist/providers/types.d.ts +154 -0
  157. package/dist/providers/yolo-contract.d.ts +131 -0
  158. package/dist/script/adapter.d.ts +101 -0
  159. package/dist/script/batch-script.d.ts +7 -0
  160. package/dist/script/index.d.ts +3 -0
  161. package/dist/script/lmscript.d.ts +550 -0
  162. package/dist/shared/policy.d.ts +74 -0
  163. package/dist/shared-lib/db.d.ts +25 -0
  164. package/dist/shared-lib/passive-session-reader.d.ts +2 -0
  165. package/dist/shared-lib/provider-parse.d.ts +6 -0
  166. package/dist/templates/basedir.d.ts +52 -0
  167. package/dist/uri/lmctl_uri.d.ts +38 -0
  168. package/durable-memory/model_selection.md +108 -25
  169. package/examples/README.md +103 -0
  170. package/examples/anonymous-panel.lms +3 -0
  171. package/examples/batch-prompts.txt +9 -0
  172. package/examples/batchscript.lms +67 -0
  173. package/examples/n8n/agent-lib.lms +56 -0
  174. package/examples/n8n/git-lib.lms +93 -0
  175. package/examples/n8n/mock-support.db +0 -0
  176. package/examples/n8n/multi-agent-review.lms +107 -0
  177. package/examples/n8n/news-digest.lms +159 -0
  178. package/examples/n8n/rag-search.lms +163 -0
  179. package/examples/n8n/support-agent.lms +88 -0
  180. package/examples/n8n/ticket-triage.lms +156 -0
  181. package/package.json +13 -17
  182. package/dist/cli/side_effect_classifier.json +0 -19
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Codex app-server transport.
3
+ *
4
+ * Spawns `codex app-server --listen stdio://` and communicates via
5
+ * JSON-RPC 2.0 over stdin/stdout. The process stays alive for the
6
+ * full conversation — no one-shot timeout.
7
+ *
8
+ * Protocol:
9
+ * initialize → thread/start or thread/resume → turn/start
10
+ * ← item/agentMessage/delta (streaming)
11
+ * ← turn/completed
12
+ */
13
+ import { type HoldOpenOptions } from "./hold-open.js";
14
+ import { type PermissionMode } from "../yolo-contract.js";
15
+ import { type CodexInitializePayload } from "../parsers/codex-app-server.js";
16
+ export { buildCodexThreadResumeParams, buildCodexThreadStartParams, classifyCodexErrorDisposition, codexSandboxForPermissionMode, createCodexAgentAccumulator, createCodexTurnState, flushCodexAgentAccumulator, processCodexAgentNotification, recordCodexErrorNotification, resolveCodexTurnCompletion, } from "../parsers/codex-app-server.js";
17
+ export type { AgentMessageItemState, AgentMessagePhase, CodexAgentAccumulatorState, CodexErrorDisposition, CodexInitializePayload, CodexItemMismatchInfo, CodexTurnState, } from "../parsers/codex-app-server.js";
18
+ /**
19
+ * Build the JSON-RPC `initialize` params for the codex app-server.
20
+ * Mirrors the reference client at
21
+ * openai/codex-plugin-cc/plugins/codex/scripts/lib/app-server.mjs:25-41.
22
+ *
23
+ * Includes `clientInfo.title` and `capabilities.experimentalApi: false`
24
+ * explicitly. `version` is sourced from package.json at load time rather
25
+ * than a string literal so bumps aren't silently out-of-sync.
26
+ *
27
+ * We intentionally do NOT set `capabilities.optOutNotificationMethods`
28
+ * (which their plugin uses to suppress agentMessage/reasoning deltas) —
29
+ * lmctl consumes every delta for live `onDelta` progress.
30
+ */
31
+ export declare function buildInitializePayload(): CodexInitializePayload;
32
+ /**
33
+ * Backward-compat shim for the prior local helper. Forwards to
34
+ * `transportRpcErrorFromResponse` from the shared transport-errors
35
+ * module so existing tests / external callers continue to work.
36
+ *
37
+ * The returned object is now a `TransportRpcError` instance — still
38
+ * an `Error`, still has `.rpcCode` and `.data` (mapped to `.rpcData`),
39
+ * but the latter are typed and named consistently with the other
40
+ * transports.
41
+ *
42
+ * @deprecated Construct `TransportRpcError` directly via
43
+ * `transportRpcErrorFromResponse(message, errorObj)`.
44
+ */
45
+ export declare function createRpcError(message: string, data?: unknown): Error & {
46
+ rpcCode?: number;
47
+ data?: unknown;
48
+ };
49
+ export interface CodexAppServerResult {
50
+ result: string;
51
+ threadId: string;
52
+ turnId: string;
53
+ durationMs: number;
54
+ exitCode: number | null;
55
+ }
56
+ export interface CodexAppServerOptions {
57
+ /** Codex thread ID to resume. If provided, uses thread/resume with writable sandbox. */
58
+ threadId?: string;
59
+ cwd: string;
60
+ env?: NodeJS.ProcessEnv;
61
+ /** Called for each text delta (for live output) */
62
+ onDelta?: (text: string) => void;
63
+ /** Called for each raw JSON-RPC event */
64
+ onEvent?: (event: Record<string, unknown>) => void;
65
+ /**
66
+ * Override the spawned codex binary path. Used for troubleshooting
67
+ * with a custom codex-rs build. When set, the transport spawns this
68
+ * exact path instead of looking up `codex` on PATH. (Task 63 fix 3.)
69
+ */
70
+ binaryPath?: string;
71
+ /** Abort signal — kills the provider process when aborted */
72
+ signal?: AbortSignal;
73
+ /** Provider-specific model name, passed through Codex config and RPC overrides. */
74
+ model?: string;
75
+ /**
76
+ * Teamfile alias of the spawned member. Used only as a fallback for
77
+ * old callers that did not pass `isLead`.
78
+ */
79
+ alias?: string;
80
+ /** Role-based Lead signal from teamfile resolution. */
81
+ isLead?: boolean;
82
+ /**
83
+ * Permission mode for this turn (default "yolo").
84
+ *
85
+ * Codex's permission selection is wire-protocol level — both
86
+ * `thread/start` and `thread/resume` carry a `sandbox` param:
87
+ *
88
+ * yolo → "danger-full-access" (historical default)
89
+ * plan → "read-only" (codex refuses workspace writes)
90
+ *
91
+ * `approvalPolicy` stays `"never"` in both modes because lmctl
92
+ * has no UI for forwarding permission prompts.
93
+ */
94
+ permissionMode?: PermissionMode;
95
+ }
96
+ /**
97
+ * Fork a codex thread via the app-server `thread/fork` JSON-RPC method.
98
+ *
99
+ * Why app-server and not the bundled `codex fork` CLI: `codex fork` is
100
+ * unconditionally TUI-only (`run_interactive_tui` in
101
+ * codex-rs/cli/src/main.rs:793-817), refusing non-TTY stdin with
102
+ * "Error: stdin is not a terminal". The app-server protocol exposes
103
+ * the same fork capability as a clean RPC.
104
+ *
105
+ * Empirical response shape (Phase B0 verified, codex-cli 0.121.0):
106
+ *
107
+ * { id, result: { thread: {
108
+ * id: "<NEW-THREAD-ID>",
109
+ * forkedFromId: "<source-id>",
110
+ * path: "~/.codex/sessions/.../<new-id>.jsonl",
111
+ * turns: [...carried-over history...],
112
+ * cwd, status: { type: "idle" }, ... } } }
113
+ *
114
+ * The forked thread is persisted to disk by codex BEFORE the response
115
+ * returns — no `turn/start` needed to materialize it.
116
+ *
117
+ * @param sourceThreadId Existing codex thread id to fork from.
118
+ * @param opts.cwd Working directory for the spawned app-server.
119
+ * Codex inherits this into the forked thread's cwd.
120
+ * @param opts.binaryPath Optional override of the codex binary path.
121
+ * @returns The new thread id codex minted for the fork.
122
+ */
123
+ export declare function forkCodexThread(sourceThreadId: string, opts?: {
124
+ cwd?: string;
125
+ binaryPath?: string;
126
+ }): Promise<string>;
127
+ /**
128
+ * Send a prompt to Codex via the app-server protocol.
129
+ * Returns the assembled response text when the turn completes.
130
+ */
131
+ export declare function sendCodexPrompt(prompt: string, opts: CodexAppServerOptions): Promise<CodexAppServerResult>;
132
+ export interface CodexSessionOptions extends CodexAppServerOptions {
133
+ /**
134
+ * Shutdown cap: how long close() waits for a natural exit after stdin
135
+ * EOF before escalating to the (internally bounded) terminate ladder.
136
+ * Default 2000ms.
137
+ */
138
+ closeTimeoutMs?: number;
139
+ /**
140
+ * Outstanding-work check run ONCE at close() before stdin ends (same
141
+ * policy as the ACP/stream-json bundles: the persistence IS the hold,
142
+ * close honors the final turn's background dispatches).
143
+ */
144
+ holdOpen?: HoldOpenOptions;
145
+ }
146
+ export interface CodexSessionTurnOptions {
147
+ onDelta?: (text: string) => void;
148
+ onEvent?: (event: Record<string, unknown>) => void;
149
+ /**
150
+ * Abort THIS turn: rejects the turn with `Error("Aborted")` and sends a
151
+ * best-effort `turn/interrupt` — the app-server has a real cancel verb,
152
+ * so the bundle stays usable for the next turn (unlike stream-json).
153
+ */
154
+ signal?: AbortSignal;
155
+ }
156
+ export interface CodexSessionTurnResult {
157
+ text: string;
158
+ sessionId: string;
159
+ turnId: string;
160
+ durationMs: number;
161
+ }
162
+ /**
163
+ * An open codex app-server bundle session: one process serving many
164
+ * prompts via repeated `turn/start` on the resumed thread. The lifecycle
165
+ * contract is in src/providers/bundle.ts; the codex-specific decisions:
166
+ *
167
+ * - Per-turn completion boundary: the `turn/completed` notification
168
+ * (codex's authoritative end-of-turn signal, including its deferred
169
+ * terminal-error resolution via resolveCodexTurnCompletion).
170
+ * - Identity: thread/resume must return the EXACT requested thread id.
171
+ * The resume→thread/start fallback is DISABLED in bundle mode — a
172
+ * failed resume aborts the open, never silently starts a new thread
173
+ * (requireResume parity with the ACP bundle).
174
+ * - Cancellation: `turn/interrupt` keeps the bundle usable.
175
+ */
176
+ export interface CodexBundleSession {
177
+ readonly provider: "codex";
178
+ readonly sessionId: string;
179
+ readonly completedTurns: number;
180
+ sendTurn(prompt: string, opts?: CodexSessionTurnOptions): Promise<CodexSessionTurnResult>;
181
+ close(): Promise<void>;
182
+ }
183
+ /**
184
+ * Open a persistent codex app-server session. See {@link CodexBundleSession}.
185
+ */
186
+ export declare function openCodexSession(opts: CodexSessionOptions): Promise<CodexBundleSession>;
187
+ /**
188
+ * Check if a tool should use the Codex app-server transport.
189
+ */
190
+ export declare function supportsCodexAppServer(tool: string): boolean;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Transport-agnostic "hold stdin open past the turn's result while the session's
3
+ * own dispatched work is still outstanding" mechanism. Extracted verbatim from
4
+ * stream-json.ts (task #68) so every transport — stream-json AND acp — shares
5
+ * ONE copy of this subtle concurrency code instead of re-deriving it.
6
+ *
7
+ * The ALGORITHM lives here once; each transport supplies its own {@link
8
+ * HoldOpenHooks} over its own child/abort/event state. Behavior is identical to
9
+ * the original inline stream-json implementation (its full test suite is the
10
+ * regression proof).
11
+ */
12
+ export type HoldOpenReleaseReason = "cleared" | "max-hold" | "check-error" | "child-exited" | "aborted";
13
+ export interface HoldOpenOptions {
14
+ /**
15
+ * True while the session being run still has outstanding dispatched work
16
+ * that must survive this process. Checked once on the first result event
17
+ * (fast path: false → close immediately), then every `pollMs` while true.
18
+ * A thrown/rejected check counts as "no outstanding work" — the check
19
+ * must never be able to wedge the turn open. A check that never settles
20
+ * cannot wedge it either: each call races the remaining `maxHoldMs`
21
+ * budget and the abort signal, so the ceiling is a real hard bound.
22
+ */
23
+ hasOutstandingWork: () => Promise<boolean> | boolean;
24
+ /** Poll interval while work is outstanding. Default 2000ms. */
25
+ pollMs?: number;
26
+ /**
27
+ * Quiescence window required AFTER a hold before the close proceeds: the
28
+ * check must keep reporting "no outstanding work" for this long. Measured
29
+ * from the LATER of "the work cleared" and "the child last emitted an event",
30
+ * and never expires while a turn is mid-flight. Covers the dispatched work's
31
+ * lease-release → process-exit → completion-notification hand-off, which a
32
+ * window measured from the lease alone would close stdin in the middle of.
33
+ * Deliberately applies ONLY after an actual hold. Default 5000ms; 0 disables.
34
+ */
35
+ settleMs?: number;
36
+ /**
37
+ * Hard ceiling on how long the close may be deferred — last-resort insurance
38
+ * against a row that stays "live" forever. Callers default this to the
39
+ * agent_inflight TTL. On expiry the close proceeds (today's behavior), never
40
+ * a hang. Default 8 hours.
41
+ */
42
+ maxHoldMs?: number;
43
+ /**
44
+ * Short, bounded window to wait for the FIRST agent_inflight lease to appear
45
+ * when a background job was launched this turn (see
46
+ * HoldOpenHooks.wasBackgroundJobLaunched) but no lease has registered yet — a
47
+ * nested `lmctl chat` has real startup latency before it takes its lease.
48
+ * Only ever applies before any hold, and only when a background job was
49
+ * observed; short-circuits the instant the lease appears (zero cost to a
50
+ * fast-registering dispatch). Distinct from `maxHoldMs`: this bounds ONLY the
51
+ * first-appearance race, not a held wait. Default 1000ms.
52
+ */
53
+ backgroundGraceMs?: number;
54
+ /** Called once when the close is first deferred (for logging). */
55
+ onHold?: () => void;
56
+ /**
57
+ * Called EXACTLY ONCE per turn once the hold path has finished, including the
58
+ * fast path (`held: false`, reason `"cleared"`). Callers use it both to log
59
+ * and to dispose resources the check acquired, so it must not be conditional
60
+ * on having actually held — decide what is worth logging from `held`/`reason`.
61
+ */
62
+ onRelease?: (info: {
63
+ elapsedMs: number;
64
+ held: boolean;
65
+ reason: HoldOpenReleaseReason;
66
+ }) => void;
67
+ }
68
+ /**
69
+ * Per-transport state the hold loop reads. Each transport wires these to its own
70
+ * abort flag, child-exit flag, turn-in-flight tracking, last-activity timestamp,
71
+ * and stdin-close primitive.
72
+ */
73
+ export interface HoldOpenHooks {
74
+ /** The turn was aborted (caller signalled). */
75
+ isAborted: () => boolean;
76
+ /** The child process has already exited on its own. */
77
+ hasChildExited: () => boolean;
78
+ /** A turn is currently mid-flight (events seen since the last result). The
79
+ * settle window never expires while this is true. */
80
+ isTurnInFlight: () => boolean;
81
+ /** Epoch ms of the child's last emitted event/notification. */
82
+ lastEventAtMs: () => number;
83
+ /** Close the child's stdin (idempotent / harmless if already gone). */
84
+ closeStdin: () => void;
85
+ /** Abort signal, raced inside each never-settling check. */
86
+ signal?: AbortSignal;
87
+ /** Provider label for the max-hold errorLog + verbose logs. */
88
+ label: string;
89
+ /**
90
+ * True once a background job (e.g. an ACP `run_in_background` tool call) was
91
+ * observed launching this turn, and stays true for the rest of the turn. When
92
+ * true, the first "nothing outstanding" result gets a short grace window for
93
+ * the nested lease to register instead of the zero-latency fast close. Absent
94
+ * (stream-json, and any turn with no background launch) ⇒ treated as false ⇒
95
+ * the fast path is byte-for-byte unchanged.
96
+ */
97
+ wasBackgroundJobLaunched?: () => boolean;
98
+ }
99
+ /** Shared with the result-grace-timeout fallback (transports/stream-json.ts)
100
+ * so the two independent uses of `hasOutstandingWork` can't drift apart. */
101
+ export declare const DEFAULT_MAX_HOLD_MS: number;
102
+ /** Sentinel for "the check did not settle within its allotted budget". */
103
+ declare const CHECK_TIMED_OUT: unique symbol;
104
+ /**
105
+ * Await `check` but never longer than `budgetMs`, and never past an abort.
106
+ * Without this a `hasOutstandingWork` that never settles would park the hold
107
+ * loop forever — `maxHoldMs` is only consulted between awaits, so it would not
108
+ * be the hard ceiling it advertises. Exported for the result-grace-timeout
109
+ * fallback (transports/stream-json.ts), which awaits the SAME
110
+ * `hasOutstandingWork` check and needs the identical never-wedge guarantee.
111
+ */
112
+ export declare function settleCheckWithin(check: Promise<boolean> | boolean, budgetMs: number, signal: AbortSignal | undefined): Promise<boolean | typeof CHECK_TIMED_OUT>;
113
+ /**
114
+ * Schedule the turn's stdin close, optionally deferred while the session's own
115
+ * dispatched work is outstanding. Without `hold` this is the exact pre-existing
116
+ * synchronous `closeStdin()` — no promise, no extra tick. Call AT MOST ONCE per
117
+ * turn (callers guard their own first-result transition).
118
+ */
119
+ export declare function scheduleHoldOpenClose(hold: HoldOpenOptions | undefined, hooks: HoldOpenHooks): void;
120
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Transport manager — selects and creates transport handles.
3
+ *
4
+ * Replaces the provider-specific if/else branching in work-cmd.ts
5
+ * with a single unified interface.
6
+ */
7
+ import type { TransportHandle, TransportType, TransportOptions } from "./types.js";
8
+ import type { UserConfig } from "../config/types.js";
9
+ export declare function selectTransport(tool: string): TransportType;
10
+ export declare function transportLabel(type: TransportType, tool: string): string;
11
+ export interface CreateTransportOpts extends TransportOptions {
12
+ /** User config — needed for one-shot fallback command building */
13
+ userConfig?: UserConfig;
14
+ /** Profile override — for one-shot */
15
+ profile?: string;
16
+ }
17
+ /**
18
+ * Create a transport handle for the given tool.
19
+ * Automatically selects the best transport.
20
+ */
21
+ export declare function createTransport(opts: CreateTransportOpts): TransportHandle;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Claude Code stream-json transport.
3
+ *
4
+ * Spawns `claude -p --input-format stream-json --output-format stream-json`
5
+ * and communicates via NDJSON on stdin/stdout. The process stays alive for
6
+ * the full conversation — no one-shot timeout.
7
+ *
8
+ * Protocol (reverse-engineered from Claude Agent SDK):
9
+ * stdin: {"type":"user","message":{"role":"user","content":"..."}}
10
+ * stdout: {"type":"system","subtype":"init",...}
11
+ * {"type":"assistant","message":{...},...}
12
+ * {"type":"result","subtype":"success","result":"...","session_id":"..."}
13
+ */
14
+ import { type PermissionMode } from "../yolo-contract.js";
15
+ import { type HoldOpenOptions } from "./hold-open.js";
16
+ export type { HoldOpenReleaseReason } from "./hold-open.js";
17
+ export type StreamJsonHoldOpenOptions = HoldOpenOptions;
18
+ import { type StreamJsonResult } from "../parsers/stream-json.js";
19
+ export { aggregateResultEvents, createStreamJsonProcessor, joinResultTurns, supportsStreamJson, } from "../parsers/stream-json.js";
20
+ export type { AggregatedResultEvents, StreamJsonProcessor, StreamJsonResult, } from "../parsers/stream-json.js";
21
+ /**
22
+ * Deferred stdin-close contract (BUG-chat-client-death-interrupts-receiver-
23
+ * live-session, "root cause pinned exactly" section).
24
+ *
25
+ * Closing `child.stdin` on the first `result` event makes `claude -p` exit
26
+ * the moment its visible turn text is done — with zero regard for any
27
+ * `run_in_background` dispatches the session fired during that turn. Those
28
+ * children die with the exiting host process and their completion
29
+ * notifications are never delivered. The durable signal for "this session
30
+ * still has outstanding dispatched work" is an `agent_inflight` row whose
31
+ * SENDER is this session's own identity, held by a live pid — state lmctl
32
+ * already owns.
33
+ *
34
+ * The transport stays DB-free: the caller supplies `hasOutstandingWork` as
35
+ * an opaque callback (queue-delivery builds it from the receiver identity +
36
+ * DAL). When the option is absent, the close is the same synchronous
37
+ * `child.stdin.end()` as before — zero added latency. When present, ONE
38
+ * check runs on the first result event; only if it reports outstanding work
39
+ * does the transport enter a poll loop.
40
+ */
41
+ export interface StreamJsonOptions {
42
+ sessionId?: string;
43
+ cwd: string;
44
+ env?: NodeJS.ProcessEnv;
45
+ /** Called for each raw NDJSON event from stdout (for logging/debugging) */
46
+ onEvent?: (event: Record<string, unknown>) => void;
47
+ /** Called when streaming text arrives (for live output) */
48
+ onText?: (text: string) => void;
49
+ /**
50
+ * Permission mode for this turn.
51
+ *
52
+ * - "yolo" (default) — historical behavior; append the provider's
53
+ * permission-bypass flag (`--dangerously-skip-permissions` for
54
+ * claude, `--approval-mode yolo` for qwen). Required for
55
+ * non-interactive runs; if the provider asks for a permission
56
+ * mid-turn lmctl has no UI to forward the request.
57
+ * - "plan" — append the provider's plan-mode flag
58
+ * instead (`--permission-mode plan` for claude,
59
+ * `--approval-mode plan` for qwen). The provider should refuse
60
+ * workspace-modifying tool calls in this mode. Use for research /
61
+ * exploration sessions.
62
+ *
63
+ * Default is "yolo" so callers that omit the field preserve the
64
+ * pre-existing argv shape exactly.
65
+ */
66
+ permissionMode?: PermissionMode;
67
+ /** Tool name — determines the binary and permission flags */
68
+ tool?: string;
69
+ /**
70
+ * Override the spawned binary path. When set, this exact path is
71
+ * spawned instead of looking up `tool` in TOOL_BINARY. Permission
72
+ * flags are still resolved via `tool`. Used for troubleshooting
73
+ * with custom Claude Code builds. (Task 63 fix 3.)
74
+ */
75
+ binaryPath?: string;
76
+ /** Abort signal — kills the provider process when aborted */
77
+ signal?: AbortSignal;
78
+ /** Provider-specific model name, appended as `--model <name>`. */
79
+ model?: string;
80
+ /**
81
+ * When set, the first-result stdin close is deferred while the caller
82
+ * reports outstanding dispatched work. See {@link StreamJsonHoldOpenOptions}.
83
+ */
84
+ holdOpen?: StreamJsonHoldOpenOptions;
85
+ }
86
+ export declare function formatProviderNoResultError(params: {
87
+ tool: string;
88
+ exitCode: number | null;
89
+ stderr: string;
90
+ }): string;
91
+ /**
92
+ * Send a prompt via the stream-json protocol (Claude Code / Qwen).
93
+ * Returns the final result text when the turn completes.
94
+ */
95
+ export declare function sendPrompt(prompt: string, opts: StreamJsonOptions): Promise<StreamJsonResult>;
96
+ export interface StreamJsonSessionOptions extends StreamJsonOptions {
97
+ /**
98
+ * Shutdown cap: how long close() waits for a natural exit after stdin
99
+ * EOF before escalating to the (internally bounded) terminate ladder.
100
+ * Also bounds the open-phase wait for the provider's init event.
101
+ * Default 2000ms (close) / 30000ms (init).
102
+ */
103
+ closeTimeoutMs?: number;
104
+ /**
105
+ * Override the session-file roots used by the bundle identity check.
106
+ * Default: the provider's standard projects dir under the user home
107
+ * (`~/.claude/projects`, `~/.qwen/projects`). Exists for tests and
108
+ * custom provider homes.
109
+ */
110
+ sessionFileRoots?: string[];
111
+ }
112
+ export interface StreamJsonSessionTurnOptions {
113
+ onText?: (text: string) => void;
114
+ onEvent?: (event: Record<string, unknown>) => void;
115
+ /**
116
+ * Abort THIS turn. stream-json has no verified cancel verb, so
117
+ * cancellation is TERMINAL for the bundle: the turn rejects with
118
+ * `Error("Aborted")` and the child gets the same graceful-shutdown
119
+ * ladder a one-shot abort gets. Later turns fail fast.
120
+ */
121
+ signal?: AbortSignal;
122
+ }
123
+ export interface StreamJsonSessionTurnResult {
124
+ result: string;
125
+ sessionId: string;
126
+ durationMs: number;
127
+ numTurns: number;
128
+ /** Stall-grace fallback marker (text_done), same convention as
129
+ * StreamJsonResult.warnings. */
130
+ warnings?: string[];
131
+ }
132
+ /**
133
+ * An open stream-json bundle session: one provider process serving many
134
+ * prompts. The lifecycle contract is in src/providers/bundle.ts; the
135
+ * transport-specific decisions:
136
+ *
137
+ * - Per-turn completion boundary: the turn's `result` event. A fresh
138
+ * StreamJsonProcessor per turn, so accumulated text/results never leak
139
+ * across turns; events arriving with no turn in flight update activity
140
+ * bookkeeping only (never accumulated, never armed timers).
141
+ * - The stall-grace machinery (text_done / tool_use_silence timers) is
142
+ * retained and re-armed per turn; a text_done fire resolves the turn
143
+ * from accumulated text with a warning exactly like the one-shot path
144
+ * (and ends the process, so the bundle is dead afterwards).
145
+ * - Identity: the session FILE's header is checked at open (requireResume
146
+ * parity — no file, no bundle) and re-checked before every sendTurn
147
+ * (catches an external re-seed mid-bundle); the provider's init event
148
+ * must agree with the requested session id.
149
+ * - Cancellation is terminal (no verified cancel verb in stream-json).
150
+ */
151
+ export interface StreamJsonSession {
152
+ readonly tool: string;
153
+ readonly sessionId: string;
154
+ readonly completedTurns: number;
155
+ sendTurn(prompt: string, opts?: StreamJsonSessionTurnOptions): Promise<StreamJsonSessionTurnResult>;
156
+ close(): Promise<void>;
157
+ }
158
+ /**
159
+ * Open a persistent stream-json session (`claude`/`qwen`). See
160
+ * {@link StreamJsonSession} for the contract.
161
+ */
162
+ export declare function openStreamJsonSession(opts: StreamJsonSessionOptions): Promise<StreamJsonSession>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared transport interfaces.
3
+ *
4
+ * All provider transports implement TransportHandle so the caller
5
+ * (work-cmd, serve) doesn't need provider-specific branching.
6
+ */
7
+ export type TransportType = "stream-json" | "codex-app-server" | "acp" | "one-shot";
8
+ export interface TurnResult {
9
+ text: string;
10
+ sessionId: string;
11
+ durationMs: number;
12
+ stopReason?: string;
13
+ numTurns?: number;
14
+ }
15
+ export interface TransportOptions {
16
+ sessionId?: string;
17
+ cwd: string;
18
+ env?: NodeJS.ProcessEnv;
19
+ tool: string;
20
+ /**
21
+ * If true, the transport must resume the given sessionId or fail.
22
+ * If false, a failed resume may fall back to a new session.
23
+ * Set to true for `--session`, false for `--team` or new sessions.
24
+ */
25
+ requireResume?: boolean;
26
+ /**
27
+ * Called with each text delta as it arrives from the provider.
28
+ * Used by the SSE streaming endpoint to push incremental text to the browser.
29
+ */
30
+ onDelta?: (text: string) => void;
31
+ /**
32
+ * Signal to abort/cancel the turn. When aborted, the transport should
33
+ * kill the provider process and reject the sendTurn promise.
34
+ */
35
+ signal?: AbortSignal;
36
+ /** Provider-specific model override, when the selected transport supports it. */
37
+ model?: string;
38
+ /** Provider-specific effort / variant override, when the selected transport supports it. */
39
+ effort?: string;
40
+ /** Teamfile alias, when provider setup needs alias-specific behavior. */
41
+ alias?: string;
42
+ /** Provider binary override. */
43
+ progPath?: string;
44
+ /**
45
+ * Permission mode — `"yolo"` (default) or `"plan"`. The transport
46
+ * substitutes the per-provider argv / JSON-RPC params accordingly.
47
+ * See `src/providers/yolo-contract.ts`.
48
+ */
49
+ permissionMode?: import("../yolo-contract.js").PermissionMode;
50
+ }
51
+ export interface TransportHandle {
52
+ readonly type: TransportType;
53
+ readonly tool: string;
54
+ /** Send a prompt and get the response. Manages the full lifecycle internally. */
55
+ sendTurn(prompt: string): Promise<TurnResult>;
56
+ }