indusagi-coding-agent 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/package.json +2 -1
  2. package/src/_decls/entry.ts +18 -0
  3. package/src/_decls/guardrails.ts +35 -0
  4. package/src/_decls/index.ts +26 -0
  5. package/src/addons/contract.ts +236 -0
  6. package/src/addons/dispatch/event-dispatcher.ts +164 -0
  7. package/src/addons/dispatch/index.ts +25 -0
  8. package/src/addons/dispatch/tool-interceptor.ts +208 -0
  9. package/src/addons/host.ts +225 -0
  10. package/src/addons/index.ts +112 -0
  11. package/src/addons/manifest.ts +158 -0
  12. package/src/addons/sandbox.ts +170 -0
  13. package/src/addons/surface.ts +78 -0
  14. package/src/boot/auth-vault.ts +195 -0
  15. package/src/boot/boot.ts +138 -0
  16. package/src/boot/contract.ts +238 -0
  17. package/src/boot/heap.ts +59 -0
  18. package/src/boot/index.ts +28 -0
  19. package/src/boot/invocation.ts +93 -0
  20. package/src/boot/runners/addon-wiring.ts +153 -0
  21. package/src/boot/runners/checkpoint.ts +169 -0
  22. package/src/boot/runners/delegate-runner.ts +294 -0
  23. package/src/boot/runners/index.ts +13 -0
  24. package/src/boot/runners/link-runner.ts +45 -0
  25. package/src/boot/runners/memdir.ts +168 -0
  26. package/src/boot/runners/oneshot-runner.ts +58 -0
  27. package/src/boot/runners/read-state.ts +90 -0
  28. package/src/boot/runners/registry.ts +42 -0
  29. package/src/boot/runners/repl-runner.ts +143 -0
  30. package/src/boot/runners/server-mode.ts +121 -0
  31. package/src/boot/runners/session.ts +641 -0
  32. package/src/boot/server-token.ts +148 -0
  33. package/src/boot/stages.ts +167 -0
  34. package/src/boot/upgrade/apply.ts +94 -0
  35. package/src/boot/upgrade/index.ts +13 -0
  36. package/src/boot/upgrade/upgrades.ts +289 -0
  37. package/src/briefing/compose.ts +150 -0
  38. package/src/briefing/context-docs.ts +19 -0
  39. package/src/briefing/contract.ts +717 -0
  40. package/src/briefing/index.ts +31 -0
  41. package/src/briefing/macros.ts +97 -0
  42. package/src/briefing/skills.ts +47 -0
  43. package/src/capability-deck/bridge-ledger/index.ts +27 -0
  44. package/src/capability-deck/bridge-ledger/key.ts +67 -0
  45. package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
  46. package/src/capability-deck/bridge-ledger/network.ts +117 -0
  47. package/src/capability-deck/builtin-bridge.ts +312 -0
  48. package/src/capability-deck/cards/bg-process-card.ts +335 -0
  49. package/src/capability-deck/cards/index.ts +115 -0
  50. package/src/capability-deck/cards/memory-card.ts +146 -0
  51. package/src/capability-deck/cards/plan-file.ts +97 -0
  52. package/src/capability-deck/cards/plan-tools.ts +185 -0
  53. package/src/capability-deck/cards/saas-card.ts +183 -0
  54. package/src/capability-deck/cards/task-card.ts +207 -0
  55. package/src/capability-deck/cards/todo-card.ts +168 -0
  56. package/src/capability-deck/cards/workflow-card.ts +247 -0
  57. package/src/capability-deck/contract.ts +388 -0
  58. package/src/capability-deck/index.ts +48 -0
  59. package/src/capability-deck/manifest.ts +109 -0
  60. package/src/capability-deck/provision.ts +169 -0
  61. package/src/channels/contract.ts +191 -0
  62. package/src/channels/framer.ts +50 -0
  63. package/src/channels/index.ts +101 -0
  64. package/src/channels/link/dialog.ts +129 -0
  65. package/src/channels/link/driver.ts +190 -0
  66. package/src/channels/link/index.ts +34 -0
  67. package/src/channels/link/server.ts +134 -0
  68. package/src/channels/oneshot.ts +90 -0
  69. package/src/channels/ops.ts +65 -0
  70. package/src/channels/session-ops.ts +81 -0
  71. package/src/conductor/bash-guard.ts +599 -0
  72. package/src/conductor/catalog/catalog.ts +116 -0
  73. package/src/conductor/catalog/index.ts +8 -0
  74. package/src/conductor/catalog/matcher.ts +134 -0
  75. package/src/conductor/conductor.ts +234 -0
  76. package/src/conductor/contract.ts +842 -0
  77. package/src/conductor/diagnostics.ts +227 -0
  78. package/src/conductor/index.ts +33 -0
  79. package/src/conductor/permissions.ts +588 -0
  80. package/src/conductor/quota-error.ts +49 -0
  81. package/src/conductor/signal-hub/hub.ts +46 -0
  82. package/src/conductor/signal-hub/index.ts +2 -0
  83. package/src/conductor/signal-hub/translate.test.ts +81 -0
  84. package/src/conductor/signal-hub/translate.ts +74 -0
  85. package/src/conductor/skill-parse/index.ts +2 -0
  86. package/src/conductor/skill-parse/parse.ts +108 -0
  87. package/src/conductor/transcript-store/index.ts +22 -0
  88. package/src/conductor/transcript-store/serialize.ts +116 -0
  89. package/src/conductor/transcript-store/store.ts +205 -0
  90. package/src/console/auth-status.ts +56 -0
  91. package/src/console/components/AgentsView.ts +165 -0
  92. package/src/console/components/BackgroundAgents.ts +155 -0
  93. package/src/console/components/Banner.ts +334 -0
  94. package/src/console/components/Composer.ts +94 -0
  95. package/src/console/components/StatusBar.ts +49 -0
  96. package/src/console/components/TerminalConsole.ts +1090 -0
  97. package/src/console/components/WorkingIndicator.ts +98 -0
  98. package/src/console/components/banner-sweep.ts +24 -0
  99. package/src/console/components/welcome.ts +74 -0
  100. package/src/console/contract.ts +630 -0
  101. package/src/console/index.ts +34 -0
  102. package/src/console/input/complete.ts +127 -0
  103. package/src/console/input/dir-reader.ts +34 -0
  104. package/src/console/input/index.ts +23 -0
  105. package/src/console/input/keymap.ts +159 -0
  106. package/src/console/input/paste.ts +104 -0
  107. package/src/console/mount.ts +56 -0
  108. package/src/console/overlays/approval-queue.ts +57 -0
  109. package/src/console/overlays/approval.ts +130 -0
  110. package/src/console/overlays/auth.ts +342 -0
  111. package/src/console/overlays/boards.ts +308 -0
  112. package/src/console/overlays/host.ts +36 -0
  113. package/src/console/overlays/index.ts +26 -0
  114. package/src/console/overlays/pickers.ts +258 -0
  115. package/src/console/overlays/sessions.ts +190 -0
  116. package/src/console/reducer.ts +182 -0
  117. package/src/console/slash/builtins.ts +81 -0
  118. package/src/console/slash/commands/dynamic.ts +83 -0
  119. package/src/console/slash/commands/integrations.ts +695 -0
  120. package/src/console/slash/commands/shared.ts +75 -0
  121. package/src/console/slash/commands/transcript.ts +263 -0
  122. package/src/console/slash/commands/workbench.ts +246 -0
  123. package/src/console/slash/index.ts +15 -0
  124. package/src/console/slash/registry.ts +70 -0
  125. package/src/console/slash/resolve.ts +63 -0
  126. package/src/console/startup.ts +209 -0
  127. package/src/console/theme/adapter.ts +45 -0
  128. package/src/console/theme/index.ts +7 -0
  129. package/src/console/theme/palette.ts +68 -0
  130. package/src/console/theme/resolve.ts +39 -0
  131. package/src/console/theme/tokens.ts +71 -0
  132. package/src/entry.ts +55 -0
  133. package/src/guardrails.ts +37 -0
  134. package/src/index.ts +18 -0
  135. package/src/insight/channel.ts +88 -0
  136. package/src/insight/contract.ts +185 -0
  137. package/src/insight/index.ts +110 -0
  138. package/src/insight/recorder.ts +213 -0
  139. package/src/insight/redaction.ts +157 -0
  140. package/src/insight/replay.ts +158 -0
  141. package/src/insight/sampling.ts +70 -0
  142. package/src/insight/serialize.ts +50 -0
  143. package/src/insight/sinks/console.ts +64 -0
  144. package/src/insight/sinks/file.ts +40 -0
  145. package/src/insight/sinks/index.ts +24 -0
  146. package/src/insight/sinks/stream.ts +54 -0
  147. package/src/integrations/sarvam/attach.ts +239 -0
  148. package/src/integrations/sarvam/config.ts +156 -0
  149. package/src/integrations/sarvam/index.ts +25 -0
  150. package/src/integrations/sarvam/sarvam.test.ts +60 -0
  151. package/src/integrations/sarvam/types.ts +27 -0
  152. package/src/integrations/zoho/attach.ts +342 -0
  153. package/src/integrations/zoho/config.ts +125 -0
  154. package/src/integrations/zoho/index.ts +27 -0
  155. package/src/integrations/zoho/types.ts +21 -0
  156. package/src/integrations/zoho/zoho.test.ts +50 -0
  157. package/src/kit/clipboard-image.ts +107 -0
  158. package/src/kit/external-editor.ts +48 -0
  159. package/src/kit/image.ts +59 -0
  160. package/src/kit/index.ts +51 -0
  161. package/src/kit/shell.ts +19 -0
  162. package/src/kit/tool-fetch.ts +85 -0
  163. package/src/launch/catalog.ts +148 -0
  164. package/src/launch/contract.ts +187 -0
  165. package/src/launch/credentials.ts +625 -0
  166. package/src/launch/index.ts +98 -0
  167. package/src/launch/invocation/attachments.ts +179 -0
  168. package/src/launch/invocation/flags.ts +196 -0
  169. package/src/launch/invocation/index.ts +25 -0
  170. package/src/launch/invocation/read.ts +260 -0
  171. package/src/launch/invocation/usage.ts +67 -0
  172. package/src/launch/login.ts +324 -0
  173. package/src/launch/oauth.test.ts +18 -0
  174. package/src/launch/oauth.ts +203 -0
  175. package/src/launch/packages.ts +194 -0
  176. package/src/launch/pickers.ts +189 -0
  177. package/src/runtime-bridge/bridges/_drive.ts +96 -0
  178. package/src/runtime-bridge/bridges/builtins.ts +68 -0
  179. package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
  180. package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
  181. package/src/runtime-bridge/bridges/index.ts +33 -0
  182. package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
  183. package/src/runtime-bridge/broker.ts +227 -0
  184. package/src/runtime-bridge/contract.ts +122 -0
  185. package/src/runtime-bridge/index.ts +79 -0
  186. package/src/runtime-bridge/sink.ts +180 -0
  187. package/src/sessions/contract.ts +81 -0
  188. package/src/sessions/index.ts +13 -0
  189. package/src/sessions/library.ts +229 -0
  190. package/src/settings/contract.ts +114 -0
  191. package/src/settings/index.ts +32 -0
  192. package/src/settings/manager.ts +117 -0
  193. package/src/transcript-export/index.ts +45 -0
  194. package/src/transcript-export/publish.ts +260 -0
  195. package/src/transcript-export/sgr.ts +315 -0
  196. package/src/transcript-export/template.ts +272 -0
  197. package/src/transcript-export/theme-bridge.ts +150 -0
  198. package/src/window-budget/budget/estimate.ts +135 -0
  199. package/src/window-budget/budget/gate.ts +33 -0
  200. package/src/window-budget/budget/index.ts +16 -0
  201. package/src/window-budget/budget/slice.ts +56 -0
  202. package/src/window-budget/condenser.ts +58 -0
  203. package/src/window-budget/contract.ts +184 -0
  204. package/src/window-budget/index.ts +19 -0
  205. package/src/window-budget/microcompact.ts +95 -0
  206. package/src/window-budget/rehydrate.ts +136 -0
  207. package/src/window-budget/summarize/condense.ts +103 -0
  208. package/src/window-budget/summarize/index.ts +14 -0
  209. package/src/window-budget/summarize/prompt.ts +149 -0
  210. package/src/workflow-engine/agent-runner.ts +181 -0
  211. package/src/workflow-engine/display.ts +224 -0
  212. package/src/workflow-engine/engine.ts +294 -0
  213. package/src/workflow-engine/index.ts +23 -0
  214. package/src/workflow-engine/parse.ts +172 -0
  215. package/src/workflow-engine/structured-output.ts +35 -0
  216. package/src/workspace/brand.ts +29 -0
  217. package/src/workspace/index.ts +18 -0
  218. package/src/workspace/locator.ts +103 -0
  219. package/src/workspace/runtime-detect.ts +64 -0
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Built-in capability bridge — ONE seam to the framework's native tools.
3
+ *
4
+ * The framework (`indusagi/agent`, the `actions` surface) already authors its
5
+ * file, search, shell, web, process, and checklist tools as fully-formed
6
+ * `AgentTool` objects and ships a `create<Name>Tool(cwd, options)` factory for
7
+ * each. This module is the single place the deck reaches across that seam: it
8
+ * pairs every native factory with a deck {@link CapabilityCard.build} closure so
9
+ * the whole framework tool set is re-exposed as {@link Capability} rows in one
10
+ * file rather than in a dozen one-line re-export stubs.
11
+ *
12
+ * What this buys the rest of the deck:
13
+ * - {@link BUILTIN_BRIDGE} — a single record keyed by the wire-facing tool
14
+ * name, each value a {@link BridgeBuilder} that mints the live capability for
15
+ * a {@link DeckContext}. The manifest derives its catalog rows from this
16
+ * record; nothing else hand-maintains a parallel list of built-ins.
17
+ * - {@link buildBuiltin} — resolve-and-build one built-in by id, surfacing a
18
+ * typed {@link DeckFault} on an unknown id or a builder throw.
19
+ * - {@link BUILTIN_PROFILES} — the per-built-in profile membership the
20
+ * data-driven provisioner intersects with a requested {@link DeckProfile},
21
+ * so the survey (observe-only) set falls out of one table instead of a
22
+ * second hand-written tool list.
23
+ *
24
+ * Design notes:
25
+ * - The builders thread `ctx.cwd` (and, where a factory takes them, the
26
+ * framework option bags) into the native factory. They never reimplement the
27
+ * tool — the framework owns the read/edit/grep/bash/etc. behavior; the bridge
28
+ * only *binds* it to a working context and re-labels it for the catalog.
29
+ * - The process and checklist tools are stateful framework singletons exposed
30
+ * through their factories; the bridge keeps them framework-backed (wrapping
31
+ * the framework, not any third-party package) while presenting them as plain
32
+ * capabilities to the deck.
33
+ * - All model-facing tool `name` strings are the framework's own wire contract
34
+ * (`read`, `bash`, `grep`, …) and are kept verbatim — the model's prompt and
35
+ * the conductor key off them. Only the deck-side titles/summaries are the
36
+ * deck's own prose.
37
+ */
38
+ import {
39
+ createReadTool,
40
+ createWriteTool,
41
+ createEditTool,
42
+ createLsTool,
43
+ createGrepTool,
44
+ createFindTool,
45
+ createBashTool,
46
+ createProcessTool,
47
+ createWebSearchTool,
48
+ createWebFetchTool,
49
+ createTodoReadTool,
50
+ createTodoWriteTool,
51
+ } from "indusagi/agent";
52
+ import {
53
+ capabilityId,
54
+ deckFault,
55
+ type AnyCapability,
56
+ type Capability,
57
+ type CapabilityId,
58
+ type CardProfiles,
59
+ type DeckContext,
60
+ } from "./contract.js";
61
+
62
+ /**
63
+ * A closure that mints one framework built-in {@link Capability} for a working
64
+ * context. The deck's view of a native factory after the bridge has bound the
65
+ * `cwd` (and any option bag) the factory needs.
66
+ *
67
+ * Returns `AnyCapability` because the heterogeneous built-ins carry different
68
+ * parameter schemas; a typed `AgentTool<TSchema>` widens to this element type,
69
+ * which is exactly what the deck and the conductor consume.
70
+ */
71
+ export type BridgeBuilder = (ctx: DeckContext) => AnyCapability;
72
+
73
+ /**
74
+ * One descriptor in the built-in catalog: the wire-facing id, the deck-side
75
+ * title/summary prose, the profiles the tool participates in, and the
76
+ * {@link BridgeBuilder} that binds the framework factory to a context.
77
+ *
78
+ * The manifest turns each of these into a {@link CapabilityCard}; consumers that
79
+ * want the raw built-in set read {@link BUILTIN_BRIDGE} directly.
80
+ */
81
+ export interface BuiltinDescriptor {
82
+ /** Wire-facing tool name the model invokes (framework contract, verbatim). */
83
+ readonly id: CapabilityId;
84
+ /** Short human-facing title for catalogs and help text. */
85
+ readonly title: string;
86
+ /** One-line description in the deck's own voice. */
87
+ readonly summary: string;
88
+ /** Which deck profiles this built-in belongs to. */
89
+ readonly profiles: CardProfiles;
90
+ /** Bind the framework factory to a working context. */
91
+ readonly build: BridgeBuilder;
92
+ }
93
+
94
+ /**
95
+ * Profile membership for read-only framework built-ins.
96
+ *
97
+ * Every observe-only built-in (read/ls/grep/find/web search & fetch/checklist
98
+ * read) participates in both `authoring` (sub-agent) and `survey` (full
99
+ * read-only) profiles.
100
+ */
101
+ export const READ_ONLY: CardProfiles = ["authoring", "survey"];
102
+
103
+ /**
104
+ * Profile membership for mutating framework built-ins.
105
+ *
106
+ * Mutating built-ins (write/edit/bash/process/checklist write) participate only
107
+ * in the `authoring` profile (sub-agents), not the read-only survey set.
108
+ */
109
+ export const MUTATING: CardProfiles = ["authoring"];
110
+
111
+ /**
112
+ * The ordered list of built-in descriptors — the manifest projects its catalog
113
+ * rows from this, preserving order.
114
+ *
115
+ * Each row pairs a framework-native factory (`create<Name>Tool`) with deck-side
116
+ * prose and profile membership. Order is the model's enumeration order.
117
+ */
118
+ export const BUILTIN_DESCRIPTORS: readonly BuiltinDescriptor[] = [
119
+ {
120
+ id: capabilityId("read"),
121
+ title: "Read file",
122
+ summary:
123
+ "Return the contents of a file at a path, optionally from an offset and capped to a line limit; renders images inline where supported.",
124
+ profiles: READ_ONLY,
125
+ build: (ctx) =>
126
+ createReadTool(ctx.cwd, {
127
+ readState: ctx.framework?.["readState"],
128
+ }),
129
+ },
130
+ {
131
+ id: capabilityId("ls"),
132
+ title: "List directory",
133
+ summary: "Enumerate the entries of a directory, with an optional cap on how many are returned.",
134
+ profiles: READ_ONLY,
135
+ build: (ctx) => createLsTool(ctx.cwd),
136
+ },
137
+ {
138
+ id: capabilityId("grep"),
139
+ title: "Search file contents",
140
+ summary:
141
+ "Scan files for lines matching a pattern, with optional case-insensitivity, literal matching, surrounding context, and a result cap.",
142
+ profiles: READ_ONLY,
143
+ build: (ctx) => createGrepTool(ctx.cwd),
144
+ },
145
+ {
146
+ id: capabilityId("find"),
147
+ title: "Find files by name",
148
+ summary:
149
+ "Locate files and directories whose names match a glob-style pattern beneath a root, capped to a result limit.",
150
+ profiles: READ_ONLY,
151
+ build: (ctx) => createFindTool(ctx.cwd),
152
+ },
153
+ {
154
+ id: capabilityId("websearch"),
155
+ title: "Web search",
156
+ summary:
157
+ "Query the live web for a string and return a ranked set of result snippets, capped to a requested count.",
158
+ profiles: READ_ONLY,
159
+ build: () => createWebSearchTool(),
160
+ },
161
+ {
162
+ id: capabilityId("webfetch"),
163
+ title: "Fetch URL",
164
+ summary:
165
+ "Retrieve a single URL and return its body as text, Markdown, or HTML, with an optional request timeout.",
166
+ profiles: READ_ONLY,
167
+ build: () => createWebFetchTool(),
168
+ },
169
+ {
170
+ id: capabilityId("todoread"),
171
+ title: "Read checklist",
172
+ summary:
173
+ "Read back the session's running task checklist so the agent can review what is pending, active, or done.",
174
+ profiles: READ_ONLY,
175
+ // The framework's default in-memory checklist singleton; a session-aware
176
+ // persistent store is wired by the separate checklist provisioner, not here.
177
+ build: () => createTodoReadTool(),
178
+ },
179
+ {
180
+ id: capabilityId("write"),
181
+ title: "Write file",
182
+ summary: "Create or overwrite a file at a path with the given contents, making parent directories as needed.",
183
+ profiles: MUTATING,
184
+ build: (ctx) =>
185
+ createWriteTool(ctx.cwd, {
186
+ readState: ctx.framework?.["readState"],
187
+ checkpoint: ctx.framework?.["checkpoint"],
188
+ }),
189
+ },
190
+ {
191
+ id: capabilityId("edit"),
192
+ title: "Edit file",
193
+ summary:
194
+ "Apply a find-and-replace edit to a file, swapping an exact span of old text for new text and reporting the resulting diff.",
195
+ profiles: MUTATING,
196
+ build: (ctx) =>
197
+ createEditTool(ctx.cwd, {
198
+ readState: ctx.framework?.["readState"],
199
+ checkpoint: ctx.framework?.["checkpoint"],
200
+ }),
201
+ },
202
+ {
203
+ id: capabilityId("bash"),
204
+ title: "Run shell command",
205
+ summary:
206
+ "Execute a shell command in the working directory, streaming its output and honoring an optional timeout.",
207
+ profiles: MUTATING,
208
+ build: (ctx) => createBashTool(ctx.cwd),
209
+ },
210
+ {
211
+ id: capabilityId("process"),
212
+ title: "Manage background processes",
213
+ summary:
214
+ "Start, list, inspect, feed input to, and stop long-running background commands without blocking the agent on them.",
215
+ profiles: MUTATING,
216
+ build: (ctx) => createProcessTool({ cwd: ctx.cwd }),
217
+ },
218
+ {
219
+ id: capabilityId("todowrite"),
220
+ title: "Update checklist",
221
+ summary: "Replace or amend the session's task checklist, setting each item's text, status, and priority.",
222
+ profiles: MUTATING,
223
+ // Paired with the read singleton above; both share the framework default
224
+ // in-memory store. The checklist provisioner swaps in a persistent store.
225
+ build: () => createTodoWriteTool(),
226
+ },
227
+ ];
228
+
229
+ /**
230
+ * The framework built-ins keyed by their wire-facing {@link CapabilityId} — the
231
+ * single record the manifest and provisioner read from.
232
+ *
233
+ * Derived from {@link BUILTIN_DESCRIPTORS}, so the keyed view and the ordered
234
+ * list never drift: there is exactly one place a built-in is declared.
235
+ */
236
+ export const BUILTIN_BRIDGE: Readonly<Record<string, BuiltinDescriptor>> = Object.freeze(
237
+ Object.fromEntries(BUILTIN_DESCRIPTORS.map((d) => [d.id, d])),
238
+ );
239
+
240
+ /**
241
+ * The wire-facing ids of every framework built-in, in catalog order. Convenient
242
+ * for `--tools` style selection and for the manifest's projection.
243
+ */
244
+ export const BUILTIN_IDS: readonly CapabilityId[] = BUILTIN_DESCRIPTORS.map((d) => d.id);
245
+
246
+ /**
247
+ * The profile membership of every built-in, keyed by id — the slice of the
248
+ * profile table the data-driven provisioner reads to decide which built-ins a
249
+ * requested {@link DeckProfile} includes.
250
+ */
251
+ export const BUILTIN_PROFILES: Readonly<Record<string, CardProfiles>> = Object.freeze(
252
+ Object.fromEntries(BUILTIN_DESCRIPTORS.map((d) => [d.id, d.profiles])),
253
+ );
254
+
255
+ /**
256
+ * The ordered list of built-in descriptors — the manifest projects its catalog
257
+ * rows from this, preserving order.
258
+ */
259
+ export function builtinDescriptors(): readonly BuiltinDescriptor[] {
260
+ return BUILTIN_DESCRIPTORS;
261
+ }
262
+
263
+ /**
264
+ * Resolve a built-in by id and build its live capability for a context.
265
+ *
266
+ * Throws a typed {@link DeckFault}: `unknown_capability` when the id is not a
267
+ * framework built-in, or `build_failed` when the framework factory throws. This
268
+ * is the sanctioned entry point so failure shaping stays uniform across the deck.
269
+ *
270
+ * @param id the wire-facing built-in name to build
271
+ * @param ctx the working context to bind the factory to
272
+ */
273
+ export function buildBuiltin(id: CapabilityId, ctx: DeckContext): Capability {
274
+ const descriptor = BUILTIN_BRIDGE[id];
275
+ if (descriptor === undefined) {
276
+ throw deckFault(
277
+ "unknown_capability",
278
+ `No framework built-in is registered under "${id}".`,
279
+ );
280
+ }
281
+ try {
282
+ return descriptor.build(ctx);
283
+ } catch (cause) {
284
+ throw deckFault(
285
+ "build_failed",
286
+ `Framework built-in "${id}" failed to build.`,
287
+ cause,
288
+ );
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Build every framework built-in eligible for a profile, bound to a context.
294
+ *
295
+ * Walks the bridge table once, keeps each built-in whose profile membership
296
+ * admits the requested profile (`all` admits everything), and binds it. A single
297
+ * data-driven pass replaces a trio of per-profile build functions.
298
+ *
299
+ * @param profile the requested deck profile
300
+ * @param ctx the working context to bind each factory to
301
+ */
302
+ export function buildBuiltinsForProfile(
303
+ profile: "authoring" | "survey" | "all",
304
+ ctx: DeckContext,
305
+ ): AnyCapability[] {
306
+ const built: AnyCapability[] = [];
307
+ for (const descriptor of BUILTIN_DESCRIPTORS) {
308
+ if (profile !== "all" && !descriptor.profiles.includes(profile)) continue;
309
+ built.push(buildBuiltin(descriptor.id, ctx));
310
+ }
311
+ return built;
312
+ }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Background-process capability — start, poll, and stop long-lived child
3
+ * processes that outlive a single tool call.
4
+ *
5
+ * App-novel and framework-agnostic: it wraps Node's `child_process.spawn`
6
+ * directly rather than delegating to any framework process controller, so the
7
+ * lifecycle is owned entirely by this card. A long-running command (a dev
8
+ * server, a watcher, a build in `--watch` mode) is launched detached from the
9
+ * tool turn; the agent later polls its captured output or signals it to stop.
10
+ *
11
+ * One tool folds the lifecycle into an `action` discriminant:
12
+ * - `start` — spawn `command` in a shell, return the assigned handle id.
13
+ * - `poll` — return the buffered stdout/stderr (and live/exited status) for a
14
+ * handle, optionally only the lines appended since the last poll.
15
+ * - `stop` — send SIGTERM (escalating to SIGKILL after a grace window) to a
16
+ * handle's process tree.
17
+ * - `list` — enumerate every handle this capability is tracking.
18
+ *
19
+ * Output is captured into bounded ring buffers so a chatty process cannot grow
20
+ * memory without limit; only the most recent lines are retained. The card
21
+ * produces a {@link Capability} (framework `AgentTool`) the conductor consumes.
22
+ */
23
+ import { spawn, type ChildProcess } from "node:child_process";
24
+ import { Type, type Static } from "@sinclair/typebox";
25
+ import { capabilityId, type Capability, type CapabilityCard, type DeckContext } from "../contract.js";
26
+
27
+ /** Coarse lifecycle state of a tracked background process. */
28
+ export type DaemonState = "running" | "exited" | "signalled";
29
+
30
+ /** A bounded buffer of recent output lines for one stream. */
31
+ interface RingBuffer {
32
+ lines: string[];
33
+ /** Index of the next unread line for incremental polling. */
34
+ cursor: number;
35
+ }
36
+
37
+ /** One tracked background process and its captured state. */
38
+ interface DaemonHandle {
39
+ readonly id: string;
40
+ readonly command: string;
41
+ readonly child: ChildProcess;
42
+ state: DaemonState;
43
+ exitCode: number | null;
44
+ signal: NodeJS.Signals | null;
45
+ readonly startedAt: number;
46
+ readonly stdout: RingBuffer;
47
+ readonly stderr: RingBuffer;
48
+ }
49
+
50
+ /** Cap on how many output lines one ring buffer keeps per stream. */
51
+ const MAX_BUFFERED_LINES = 2_000;
52
+
53
+ /**
54
+ * An in-process table of background children, owned by one built capability.
55
+ *
56
+ * Each `start` spawns a shell-wrapped child, wires its stdout/stderr into
57
+ * bounded buffers, and records terminal state on `exit`. `stop` escalates from
58
+ * SIGTERM to SIGKILL if the process does not exit within a grace window. The
59
+ * table is per-capability, so one session's daemons are isolated from another's.
60
+ */
61
+ export class DaemonTable {
62
+ private readonly handles = new Map<string, DaemonHandle>();
63
+ private nextSeq = 1;
64
+ private readonly cwd: string;
65
+
66
+ constructor(cwd: string) {
67
+ this.cwd = cwd;
68
+ }
69
+
70
+ start(command: string): DaemonHandle {
71
+ const id = `bg-${this.nextSeq++}`;
72
+ const child = spawn(command, {
73
+ cwd: this.cwd,
74
+ shell: true,
75
+ stdio: ["ignore", "pipe", "pipe"],
76
+ detached: false,
77
+ });
78
+ const handle: DaemonHandle = {
79
+ id,
80
+ command,
81
+ child,
82
+ state: "running",
83
+ exitCode: null,
84
+ signal: null,
85
+ startedAt: Date.now(),
86
+ stdout: { lines: [], cursor: 0 },
87
+ stderr: { lines: [], cursor: 0 },
88
+ };
89
+ child.stdout?.setEncoding("utf8");
90
+ child.stderr?.setEncoding("utf8");
91
+ child.stdout?.on("data", (d) => pushLines(handle.stdout, d));
92
+ child.stderr?.on("data", (d) => pushLines(handle.stderr, d));
93
+ child.on("exit", (code, sig) => {
94
+ handle.exitCode = code;
95
+ handle.signal = sig;
96
+ handle.state = sig ? "signalled" : "exited";
97
+ });
98
+ child.on("error", (err) => {
99
+ pushLines(handle.stderr, `spawn error: ${String(err)}`);
100
+ handle.state = "exited";
101
+ });
102
+ this.handles.set(id, handle);
103
+ return handle;
104
+ }
105
+
106
+ get(id: string): DaemonHandle | undefined {
107
+ return this.handles.get(id);
108
+ }
109
+
110
+ list(): DaemonHandle[] {
111
+ return [...this.handles.values()];
112
+ }
113
+
114
+ /**
115
+ * Signal a handle's process to terminate. Sends SIGTERM immediately and a
116
+ * SIGKILL after `graceMs` if the child is still alive. Resolves once the
117
+ * child has exited or the kill has been sent.
118
+ */
119
+ async stop(handle: DaemonHandle, graceMs = 3_000): Promise<void> {
120
+ if (handle.state !== "running") return;
121
+ const child = handle.child;
122
+ child.kill("SIGTERM");
123
+ await new Promise<void>((resolve) => {
124
+ const timer = setTimeout(() => {
125
+ if (handle.state === "running") child.kill("SIGKILL");
126
+ resolve();
127
+ }, graceMs);
128
+ child.once("exit", () => {
129
+ clearTimeout(timer);
130
+ resolve();
131
+ });
132
+ });
133
+ }
134
+ }
135
+
136
+ /** Append chunked stream output to a ring buffer, trimming the oldest lines when full. */
137
+ function pushLines(buf: RingBuffer, chunk: string): void {
138
+ for (const line of chunk.split(/\r?\n/)) {
139
+ if (line.length === 0) continue;
140
+ buf.lines.push(line);
141
+ }
142
+ if (buf.lines.length > MAX_BUFFERED_LINES) {
143
+ const drop = buf.lines.length - MAX_BUFFERED_LINES;
144
+ buf.lines.splice(0, drop);
145
+ buf.cursor = Math.max(0, buf.cursor - drop);
146
+ }
147
+ }
148
+
149
+ /** Drain the buffered output, optionally only since the last read. */
150
+ function drainBuffer(buf: RingBuffer, sinceLast: boolean): string[] {
151
+ if (sinceLast) {
152
+ const fresh = buf.lines.slice(buf.cursor);
153
+ buf.cursor = buf.lines.length;
154
+ return fresh;
155
+ }
156
+ return [...buf.lines];
157
+ }
158
+
159
+ /** Format a handle's status for model-facing prose. */
160
+ function statusLine(h: DaemonHandle): string {
161
+ if (h.state === "running") return "running";
162
+ if (h.state === "signalled") return `signalled (${h.signal ?? "?"})`;
163
+ return `exited (code ${h.exitCode ?? "?"})`;
164
+ }
165
+
166
+ const DaemonParams = Type.Object(
167
+ {
168
+ action: Type.Union(
169
+ [
170
+ Type.Literal("start"),
171
+ Type.Literal("poll"),
172
+ Type.Literal("stop"),
173
+ Type.Literal("list"),
174
+ ],
175
+ {
176
+ description:
177
+ "`start` launches a long-running command; `poll` reads its captured output; `stop` terminates it; `list` shows all tracked processes.",
178
+ },
179
+ ),
180
+ command: Type.Optional(
181
+ Type.String({
182
+ description: "Shell command to launch. Required for `start`.",
183
+ }),
184
+ ),
185
+ id: Type.Optional(
186
+ Type.String({
187
+ description: "Handle returned by `start`. Required for `poll` and `stop`.",
188
+ }),
189
+ ),
190
+ sinceLast: Type.Optional(
191
+ Type.Boolean({
192
+ description:
193
+ "On `poll`, return only output appended since the previous poll of this handle. Defaults to false (return the full retained buffer).",
194
+ }),
195
+ ),
196
+ },
197
+ { additionalProperties: false },
198
+ );
199
+
200
+ /** Statically-inferred parameter type the capability's `execute` receives. */
201
+ export type DaemonParamsType = Static<typeof DaemonParams>;
202
+
203
+ /** Structured detail returned alongside the model-facing content. */
204
+ export interface DaemonDetails {
205
+ readonly action: "start" | "poll" | "stop" | "list";
206
+ readonly ok: boolean;
207
+ readonly id?: string;
208
+ readonly state?: DaemonState;
209
+ readonly exitCode?: number | null;
210
+ readonly processes?: ReadonlyArray<{
211
+ readonly id: string;
212
+ readonly command: string;
213
+ readonly state: DaemonState;
214
+ }>;
215
+ }
216
+
217
+ const DAEMON_DESCRIPTION =
218
+ 'Run and manage long-lived background processes that persist across tool calls \u2014 dev servers, file watchers, anything you start once and observe over time. Use `action:"start"` with a `command` to launch one (you get back a handle `id`), `action:"poll"` with that `id` to read its accumulated output, `action:"stop"` to terminate it, and `action:"list"` to see what is running. Do NOT use this for ordinary one-shot commands that finish on their own \u2014 run those with the shell tool.';
219
+
220
+ /** Standard error result shape for invalid bg-process actions. */
221
+ function errResult(action: DaemonDetails["action"], message: string): {
222
+ content: Array<{ type: "text"; text: string }>;
223
+ details: DaemonDetails;
224
+ isError: true;
225
+ } {
226
+ return {
227
+ content: [{ type: "text", text: message }],
228
+ details: { action, ok: false },
229
+ isError: true,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Build the background-process capability, binding it to a fresh per-session
235
+ * {@link DaemonTable} scoped to the context's working directory.
236
+ *
237
+ * @param ctx the deck context — its `cwd` is where launched processes run.
238
+ */
239
+ export function buildDaemonCapability(ctx: DeckContext): Capability<typeof DaemonParams, DaemonDetails> {
240
+ const table = new DaemonTable(ctx.cwd);
241
+ return {
242
+ name: "bg-process",
243
+ label: "Background process",
244
+ description: DAEMON_DESCRIPTION,
245
+ parameters: DaemonParams,
246
+ async execute(_toolCallId, params) {
247
+ switch (params.action) {
248
+ case "start": {
249
+ if (!params.command) {
250
+ return errResult("start", "`command` is required to start a background process.");
251
+ }
252
+ const h = table.start(params.command);
253
+ return {
254
+ content: [
255
+ {
256
+ type: "text",
257
+ text: `Started background process ${h.id}: ${h.command}\nPoll it with action:"poll", id:"${h.id}".`,
258
+ },
259
+ ],
260
+ details: { action: "start", ok: true, id: h.id, state: h.state },
261
+ };
262
+ }
263
+ case "poll": {
264
+ const h = params.id ? table.get(params.id) : undefined;
265
+ if (!h) return errResult("poll", `No background process with id "${params.id ?? ""}".`);
266
+ const out = drainBuffer(h.stdout, params.sinceLast ?? false);
267
+ const err = drainBuffer(h.stderr, params.sinceLast ?? false);
268
+ const parts = [`Process ${h.id} \u2014 ${statusLine(h)}`];
269
+ if (out.length) parts.push(`--- stdout ---\n${out.join("\n")}`);
270
+ if (err.length) parts.push(`--- stderr ---\n${err.join("\n")}`);
271
+ if (out.length === 0 && err.length === 0) parts.push("(no new output)");
272
+ return {
273
+ content: [{ type: "text", text: parts.join("\n") }],
274
+ details: {
275
+ action: "poll",
276
+ ok: true,
277
+ id: h.id,
278
+ state: h.state,
279
+ exitCode: h.exitCode,
280
+ },
281
+ };
282
+ }
283
+ case "stop": {
284
+ const h = params.id ? table.get(params.id) : undefined;
285
+ if (!h) return errResult("stop", `No background process with id "${params.id ?? ""}".`);
286
+ await table.stop(h);
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: `Stopped background process ${h.id} (${statusLine(h)}).`,
292
+ },
293
+ ],
294
+ details: {
295
+ action: "stop",
296
+ ok: true,
297
+ id: h.id,
298
+ state: h.state,
299
+ exitCode: h.exitCode,
300
+ },
301
+ };
302
+ }
303
+ case "list": {
304
+ const all = table.list();
305
+ const text =
306
+ all.length === 0
307
+ ? "No background processes are being tracked."
308
+ : all.map((h) => `${h.id} [${statusLine(h)}] ${h.command}`).join("\n");
309
+ return {
310
+ content: [{ type: "text", text }],
311
+ details: {
312
+ action: "list",
313
+ ok: true,
314
+ processes: all.map((h) => ({
315
+ id: h.id,
316
+ command: h.command,
317
+ state: h.state,
318
+ })),
319
+ },
320
+ };
321
+ }
322
+ }
323
+ },
324
+ };
325
+ }
326
+
327
+ /** Catalog row for the background-process capability. */
328
+ export const daemonCard: CapabilityCard = {
329
+ id: capabilityId("bg-process"),
330
+ title: "Background process",
331
+ summary: "Start, poll, and stop long-lived child processes that span multiple turns.",
332
+ // Erase the precisely-typed builder to the `Capability` card surface (through
333
+ // `unknown`, as TypeBox tools are invariant in their parameter schema).
334
+ build: (ctx) => buildDaemonCapability(ctx),
335
+ };