@cohortapp/agent-sdk 2.17.0 → 2.18.4

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 (526) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture.mjs +8 -6
  23. package/lib/collective/global-config.mjs +63 -1
  24. package/lib/collective/presence.mjs +142 -5
  25. package/lib/comms/send-gate.mjs +559 -1
  26. package/lib/diagnostics/alerts.mjs +49 -0
  27. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  28. package/lib/engine/agents/definitions.mjs +343 -0
  29. package/lib/engine/agents/persist.mjs +275 -0
  30. package/lib/engine/agents/runtime.mjs +748 -0
  31. package/lib/engine/agents/usage.mjs +95 -0
  32. package/lib/engine/auth-status.mjs +139 -0
  33. package/lib/engine/budget.mjs +194 -0
  34. package/lib/engine/cli.mjs +1204 -0
  35. package/lib/engine/commands/index.mjs +269 -0
  36. package/lib/engine/context/budget.mjs +219 -0
  37. package/lib/engine/context/cache.mjs +125 -0
  38. package/lib/engine/context/child-env.mjs +215 -0
  39. package/lib/engine/context/compaction.mjs +342 -0
  40. package/lib/engine/context/images.mjs +90 -0
  41. package/lib/engine/context/instructions.mjs +327 -0
  42. package/lib/engine/context/lazy-instructions.mjs +169 -0
  43. package/lib/engine/context/manager.mjs +182 -0
  44. package/lib/engine/context/real-path.mjs +91 -0
  45. package/lib/engine/context/secret-values.mjs +163 -0
  46. package/lib/engine/context/settings.mjs +274 -0
  47. package/lib/engine/context/stream-input.mjs +159 -0
  48. package/lib/engine/guard.mjs +152 -0
  49. package/lib/engine/hooks.mjs +713 -0
  50. package/lib/engine/loop.mjs +560 -0
  51. package/lib/engine/mcp/client.mjs +254 -0
  52. package/lib/engine/mcp/config.mjs +301 -0
  53. package/lib/engine/mcp/http.mjs +201 -0
  54. package/lib/engine/mcp/index.mjs +146 -0
  55. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  56. package/lib/engine/mcp/naming.mjs +66 -0
  57. package/lib/engine/mcp/resources.mjs +89 -0
  58. package/lib/engine/mcp/results.mjs +133 -0
  59. package/lib/engine/mcp/stdio.mjs +137 -0
  60. package/lib/engine/mcp/supervisor.mjs +116 -0
  61. package/lib/engine/messages.mjs +104 -0
  62. package/lib/engine/output/json.mjs +164 -0
  63. package/lib/engine/output/stream-json.mjs +266 -0
  64. package/lib/engine/permissions.mjs +845 -0
  65. package/lib/engine/process-identity.mjs +164 -0
  66. package/lib/engine/process-tree.mjs +551 -0
  67. package/lib/engine/prompt.mjs +60 -0
  68. package/lib/engine/session/store.mjs +299 -0
  69. package/lib/engine/session-runtime/args.mjs +97 -0
  70. package/lib/engine/session-runtime/host.mjs +143 -0
  71. package/lib/engine/session-runtime/inbox.mjs +122 -0
  72. package/lib/engine/session-runtime/notifications.mjs +129 -0
  73. package/lib/engine/session-runtime/registry.mjs +328 -0
  74. package/lib/engine/session-runtime/runner.mjs +344 -0
  75. package/lib/engine/session-runtime/socket.mjs +212 -0
  76. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  77. package/lib/engine/skills/index.mjs +321 -0
  78. package/lib/engine/tools/bash-background.mjs +533 -0
  79. package/lib/engine/tools/bash.mjs +216 -0
  80. package/lib/engine/tools/edit.mjs +97 -0
  81. package/lib/engine/tools/glob.mjs +81 -0
  82. package/lib/engine/tools/grep.mjs +224 -0
  83. package/lib/engine/tools/index.mjs +84 -0
  84. package/lib/engine/tools/list-agents.mjs +32 -0
  85. package/lib/engine/tools/ls.mjs +127 -0
  86. package/lib/engine/tools/monitor.mjs +82 -0
  87. package/lib/engine/tools/notebook-edit.mjs +218 -0
  88. package/lib/engine/tools/read.mjs +103 -0
  89. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  90. package/lib/engine/tools/schema.mjs +144 -0
  91. package/lib/engine/tools/send-message.mjs +77 -0
  92. package/lib/engine/tools/session.mjs +70 -0
  93. package/lib/engine/tools/todo.mjs +144 -0
  94. package/lib/engine/tools/toolsearch.mjs +217 -0
  95. package/lib/engine/tools/walk.mjs +193 -0
  96. package/lib/engine/tools/web-switch.mjs +31 -0
  97. package/lib/engine/tools/webfetch-html.mjs +387 -0
  98. package/lib/engine/tools/webfetch-net.mjs +340 -0
  99. package/lib/engine/tools/webfetch.mjs +198 -0
  100. package/lib/engine/tools/websearch.mjs +91 -0
  101. package/lib/engine/tools/workflow.mjs +95 -0
  102. package/lib/engine/tools/write.mjs +76 -0
  103. package/lib/engine/tui/line-editor.mjs +137 -0
  104. package/lib/engine/tui/render.mjs +86 -0
  105. package/lib/engine/tui/tui.mjs +274 -0
  106. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  107. package/lib/engine/wire/effort.mjs +36 -0
  108. package/lib/engine/wire/errors.mjs +496 -0
  109. package/lib/engine/wire/http.mjs +441 -0
  110. package/lib/engine/wire/index.mjs +76 -0
  111. package/lib/engine/wire/openai-chat.mjs +332 -0
  112. package/lib/engine/wire/prompt-cache.mjs +79 -0
  113. package/lib/engine/wire/search.mjs +140 -0
  114. package/lib/engine/wire/sse.mjs +114 -0
  115. package/lib/engine/wire/stall.mjs +349 -0
  116. package/lib/engine/wire/token-provider.mjs +175 -0
  117. package/lib/engine/wire/usage.mjs +192 -0
  118. package/lib/engine/workflow/host.mjs +524 -0
  119. package/lib/engine/workflow/journal.mjs +188 -0
  120. package/lib/engine/workflow/json-schema.mjs +171 -0
  121. package/lib/engine/workflow/meta.mjs +329 -0
  122. package/lib/engine/workflow/notifications.mjs +52 -0
  123. package/lib/engine/workflow/runtime.mjs +447 -0
  124. package/lib/engine/workflow/sandbox.mjs +534 -0
  125. package/lib/engine/workflow/worker.mjs +141 -0
  126. package/lib/engine/workflow/worktree.mjs +74 -0
  127. package/lib/execution/disposition.mjs +1 -1
  128. package/lib/execution/intake.mjs +10 -0
  129. package/lib/execution/surface-policy.mjs +15 -0
  130. package/lib/learning/curator.mjs +8 -6
  131. package/lib/learning/reflect.mjs +8 -6
  132. package/lib/model-router/catalog/cohort.yaml +137 -0
  133. package/lib/model-router/catalog.mjs +118 -1
  134. package/lib/model-router/failover.mjs +67 -16
  135. package/lib/model-router/llm-task.mjs +39 -3
  136. package/lib/model-router/resolve.mjs +89 -3
  137. package/lib/model-router/spawn.mjs +46 -47
  138. package/lib/model-router/taxonomy.mjs +126 -4
  139. package/lib/org/cost-sync.mjs +141 -11
  140. package/lib/org/inbound/broadcast.mjs +289 -0
  141. package/lib/org/inbound/collective.mjs +375 -0
  142. package/lib/org/inbound/directedness.mjs +96 -8
  143. package/lib/org/inbound/facts.mjs +78 -2
  144. package/lib/org/inbound/project.mjs +22 -0
  145. package/lib/org/inbound/surfaces.mjs +14 -0
  146. package/lib/org/llm-token.mjs +879 -0
  147. package/lib/org/mesh.mjs +61 -0
  148. package/lib/org/messaging.mjs +3 -1
  149. package/lib/org/protocol.checksum +1 -1
  150. package/lib/org/protocol.mjs +15 -0
  151. package/lib/org/quota.mjs +520 -0
  152. package/lib/org/tool-surface.mjs +104 -16
  153. package/lib/org/ui-parity.mjs +16 -1
  154. package/lib/org/work-ledger.mjs +37 -6
  155. package/lib/rate-guard.mjs +114 -1
  156. package/lib/resource-governor.mjs +41 -6
  157. package/lib/runtime/adapter.mjs +823 -0
  158. package/lib/runtime/child-env.mjs +191 -0
  159. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  160. package/lib/runtime/seat-engine.mjs +162 -0
  161. package/lib/session/ask-ledger.mjs +271 -0
  162. package/lib/session/current-work.mjs +676 -0
  163. package/lib/session/feed-core.mjs +40 -3
  164. package/lib/session/launch-args.mjs +56 -4
  165. package/lib/session/status-summary.mjs +26 -9
  166. package/lib/session/upgrade-notice.mjs +42 -0
  167. package/lib/setup/claude-probe.mjs +117 -13
  168. package/lib/setup/enrich.mjs +13 -10
  169. package/lib/setup/sections/model.mjs +39 -13
  170. package/lib/telemetry/collect.mjs +208 -9
  171. package/lib/upgrade/ignored-drift.mjs +105 -0
  172. package/lib/voice/post-call-brief.mjs +30 -17
  173. package/package.json +13 -3
  174. package/plugins/maestro-skills/skills/board-work.md +5 -0
  175. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  176. package/plugins/maestro-skills/skills/main-session.md +18 -7
  177. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  178. package/scripts/ci/run-tests.mjs +47 -19
  179. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  180. package/scripts/collective/hook-runner.mjs +29 -2
  181. package/scripts/continuous-monitor.sh +13 -0
  182. package/scripts/cost/track-claude-usage.mjs +15 -0
  183. package/scripts/daemon/agent-daemon.mjs +408 -20
  184. package/scripts/daemon/assurance.mjs +48 -12
  185. package/scripts/daemon/cadence-consumer.mjs +218 -68
  186. package/scripts/daemon/cadence-handlers.mjs +73 -4
  187. package/scripts/daemon/classifier.mjs +75 -26
  188. package/scripts/daemon/context-compiler.mjs +51 -37
  189. package/scripts/daemon/deliver.mjs +30 -1
  190. package/scripts/daemon/dispatcher.mjs +595 -149
  191. package/scripts/daemon/health.mjs +14 -1
  192. package/scripts/daemon/maestro-daemon.mjs +11 -0
  193. package/scripts/daemon/prompt-builder.mjs +24 -0
  194. package/scripts/daemon/responder.mjs +246 -79
  195. package/scripts/daemon/sdk-version.mjs +98 -16
  196. package/scripts/eval/probe-gateway.mjs +635 -0
  197. package/scripts/eval/replay/extract.mjs +270 -0
  198. package/scripts/eval/replay/grade.mjs +260 -0
  199. package/scripts/eval/replay/lib/config.mjs +50 -0
  200. package/scripts/eval/replay/lib/effects.mjs +65 -0
  201. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  202. package/scripts/eval/replay/lib/judge.mjs +72 -0
  203. package/scripts/eval/replay/lib/redact.mjs +136 -0
  204. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  205. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  206. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  207. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  208. package/scripts/eval/replay/report.mjs +185 -0
  209. package/scripts/eval/replay/run.mjs +404 -0
  210. package/scripts/fleet/rollout.mjs +1094 -0
  211. package/scripts/hooks/pre-send-audit.sh +36 -245
  212. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  213. package/scripts/hooks/validate-state-yaml.sh +190 -0
  214. package/scripts/huddle/huddle-llm.mjs +361 -0
  215. package/scripts/huddle/huddle-server.mjs +46 -121
  216. package/scripts/local-triggers/autoupdate.sh +448 -78
  217. package/scripts/local-triggers/run-trigger.sh +13 -0
  218. package/scripts/maintenance/pin-integrity.mjs +364 -0
  219. package/scripts/poll-slack-events.sh +41 -9
  220. package/scripts/poller/slack-socket-mode.mjs +28 -3
  221. package/scripts/session/supervisor.mjs +80 -13
  222. package/scripts/spawn-session.sh +13 -0
  223. package/bin/maestro.test.mjs +0 -1574
  224. package/lib/action-executor.test.mjs +0 -871
  225. package/lib/archetype.test.mjs +0 -132
  226. package/lib/assurance/plan-note.test.mjs +0 -234
  227. package/lib/assurance/room-budget.test.mjs +0 -486
  228. package/lib/assurance/tier.test.mjs +0 -174
  229. package/lib/autonomy.test.mjs +0 -66
  230. package/lib/backlog.test.mjs +0 -302
  231. package/lib/backup/policy.test.mjs +0 -305
  232. package/lib/budget-escalate.test.mjs +0 -232
  233. package/lib/budget-guard.envelope.test.mjs +0 -476
  234. package/lib/budget-guard.test.mjs +0 -427
  235. package/lib/cadence-bus-requeue.test.mjs +0 -83
  236. package/lib/cadence-bus-schedule.test.mjs +0 -194
  237. package/lib/cadence-bus.test.mjs +0 -720
  238. package/lib/cadences.test.mjs +0 -230
  239. package/lib/capability/inventory.test.mjs +0 -232
  240. package/lib/capability.test.mjs +0 -78
  241. package/lib/channels/base-adapter.test.mjs +0 -590
  242. package/lib/channels/channels.test.mjs +0 -371
  243. package/lib/channels/contract.test.mjs +0 -162
  244. package/lib/channels/inbox-item.test.mjs +0 -368
  245. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  246. package/lib/channels/pairing.test.mjs +0 -270
  247. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  248. package/lib/channels/slack-adapter.test.mjs +0 -212
  249. package/lib/channels/telegram-adapter.test.mjs +0 -306
  250. package/lib/channels/voice/adapter.test.mjs +0 -278
  251. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  252. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  253. package/lib/charter.test.mjs +0 -89
  254. package/lib/claude-bin.test.mjs +0 -131
  255. package/lib/cli/board.test.mjs +0 -227
  256. package/lib/cli/design.test.mjs +0 -270
  257. package/lib/cli/doctor-checks.test.mjs +0 -336
  258. package/lib/cli/global-setup-extras.test.mjs +0 -462
  259. package/lib/cli/inbox.test.mjs +0 -230
  260. package/lib/cli/session-ack.test.mjs +0 -63
  261. package/lib/cli/session.test.mjs +0 -613
  262. package/lib/collective/capture.test.mjs +0 -121
  263. package/lib/collective/cards.test.mjs +0 -114
  264. package/lib/collective/config.test.mjs +0 -123
  265. package/lib/collective/global-config.test.mjs +0 -220
  266. package/lib/collective/global-skills.test.mjs +0 -126
  267. package/lib/collective/presence.test.mjs +0 -95
  268. package/lib/collective/recall.test.mjs +0 -116
  269. package/lib/collective/vendor-skills.test.mjs +0 -306
  270. package/lib/comms/send-gate.test.mjs +0 -770
  271. package/lib/comms.test.mjs +0 -41
  272. package/lib/context/budget.test.mjs +0 -252
  273. package/lib/context/history-scope.test.mjs +0 -79
  274. package/lib/cost/ledger-row.test.mjs +0 -183
  275. package/lib/design/design-md.test.mjs +0 -318
  276. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  277. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  278. package/lib/design/fixtures/foundation.json +0 -133
  279. package/lib/design/refresh-gate.test.mjs +0 -144
  280. package/lib/design/write.test.mjs +0 -241
  281. package/lib/diagnostics/alerts.test.mjs +0 -318
  282. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  283. package/lib/diagnostics/counters.test.mjs +0 -206
  284. package/lib/diagnostics/events.test.mjs +0 -290
  285. package/lib/diagnostics/otel.test.mjs +0 -196
  286. package/lib/diagnostics/trace.test.mjs +0 -251
  287. package/lib/env-compat.test.mjs +0 -104
  288. package/lib/execution/disposition.test.mjs +0 -553
  289. package/lib/execution/drive.test.mjs +0 -270
  290. package/lib/execution/effects.test.mjs +0 -344
  291. package/lib/execution/intake.test.mjs +0 -389
  292. package/lib/execution/journal.test.mjs +0 -261
  293. package/lib/execution/match.test.mjs +0 -235
  294. package/lib/execution/pipeline.test.mjs +0 -392
  295. package/lib/execution/route.test.mjs +0 -186
  296. package/lib/execution/surface-policy.test.mjs +0 -162
  297. package/lib/fs-atomic.test.mjs +0 -72
  298. package/lib/fs-ownership.test.mjs +0 -158
  299. package/lib/goals/admission.test.mjs +0 -164
  300. package/lib/goals/classify.test.mjs +0 -167
  301. package/lib/goals/collaborate.test.mjs +0 -336
  302. package/lib/goals/gaps.test.mjs +0 -284
  303. package/lib/goals/loop.test.mjs +0 -845
  304. package/lib/hooks/bus.test.mjs +0 -387
  305. package/lib/identity/persona.test.mjs +0 -142
  306. package/lib/kpi-sensors.test.mjs +0 -278
  307. package/lib/kpi.test.mjs +0 -244
  308. package/lib/learning/config.test.mjs +0 -75
  309. package/lib/learning/counters.test.mjs +0 -69
  310. package/lib/learning/curator-consolidate.test.mjs +0 -238
  311. package/lib/learning/curator.test.mjs +0 -106
  312. package/lib/learning/reflect.test.mjs +0 -0
  313. package/lib/learning/session-index.test.mjs +0 -125
  314. package/lib/learning/skill-writer.test.mjs +0 -210
  315. package/lib/mandate/audit.test.mjs +0 -195
  316. package/lib/mandate/contract.test.mjs +0 -185
  317. package/lib/mandate/derive.test.mjs +0 -274
  318. package/lib/mandate/model.test.mjs +0 -164
  319. package/lib/mandate/refresh.test.mjs +0 -389
  320. package/lib/mcp/server.test.mjs +0 -426
  321. package/lib/model-router/auth-profiles.test.mjs +0 -580
  322. package/lib/model-router/catalog.test.mjs +0 -385
  323. package/lib/model-router/economics.test.mjs +0 -438
  324. package/lib/model-router/failover.test.mjs +0 -439
  325. package/lib/model-router/health.test.mjs +0 -338
  326. package/lib/model-router/integration-coverage.test.mjs +0 -831
  327. package/lib/model-router/integration.test.mjs +0 -564
  328. package/lib/model-router/ledger.test.mjs +0 -415
  329. package/lib/model-router/llm-task.test.mjs +0 -392
  330. package/lib/model-router/org-credentials.test.mjs +0 -265
  331. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  332. package/lib/model-router/reconcile.test.mjs +0 -316
  333. package/lib/model-router/repair.test.mjs +0 -180
  334. package/lib/model-router/spawn.test.mjs +0 -446
  335. package/lib/model-router/taxonomy.test.mjs +0 -410
  336. package/lib/model-router.test.mjs +0 -1207
  337. package/lib/org/activity.test.mjs +0 -134
  338. package/lib/org/approvals.test.mjs +0 -216
  339. package/lib/org/awareness.test.mjs +0 -159
  340. package/lib/org/board-mine-cache.test.mjs +0 -53
  341. package/lib/org/board.test.mjs +0 -187
  342. package/lib/org/bootstrap-context.test.mjs +0 -153
  343. package/lib/org/client.test.mjs +0 -1206
  344. package/lib/org/cohort-client.test.mjs +0 -126
  345. package/lib/org/cost-sync.test.mjs +0 -153
  346. package/lib/org/doctor.test.mjs +0 -346
  347. package/lib/org/engagement-ledger.test.mjs +0 -112
  348. package/lib/org/engagement.test.mjs +0 -739
  349. package/lib/org/handoff.test.mjs +0 -269
  350. package/lib/org/inbound/directedness.test.mjs +0 -668
  351. package/lib/org/inbound/facts.test.mjs +0 -471
  352. package/lib/org/inbound/hydrate.test.mjs +0 -908
  353. package/lib/org/inbound/index.test.mjs +0 -429
  354. package/lib/org/inbound/project.test.mjs +0 -287
  355. package/lib/org/integration-tools.test.mjs +0 -160
  356. package/lib/org/keys.test.mjs +0 -92
  357. package/lib/org/knowledge.test.mjs +0 -326
  358. package/lib/org/leases.test.mjs +0 -235
  359. package/lib/org/mesh-directives.test.mjs +0 -110
  360. package/lib/org/mesh-integration.test.mjs +0 -127
  361. package/lib/org/mesh.test.mjs +0 -400
  362. package/lib/org/messaging.test.mjs +0 -471
  363. package/lib/org/param-contract.test.mjs +0 -477
  364. package/lib/org/policy.test.mjs +0 -237
  365. package/lib/org/protocol.checksum.test.mjs +0 -90
  366. package/lib/org/protocol.test.mjs +0 -323
  367. package/lib/org/push.test.mjs +0 -792
  368. package/lib/org/registry.test.mjs +0 -100
  369. package/lib/org/resource-tools.test.mjs +0 -361
  370. package/lib/org/tool-access.test.mjs +0 -144
  371. package/lib/org/tool-surface-integration.test.mjs +0 -120
  372. package/lib/org/tool-surface.test.mjs +0 -1268
  373. package/lib/org/typing.test.mjs +0 -291
  374. package/lib/org/ui-parity.test.mjs +0 -560
  375. package/lib/org/verify.test.mjs +0 -194
  376. package/lib/org/work-ledger.test.mjs +0 -273
  377. package/lib/plan/adoption-e2e.test.mjs +0 -366
  378. package/lib/plan/budget-enforcement.test.mjs +0 -400
  379. package/lib/plan/compile.test.mjs +0 -382
  380. package/lib/plan/emit.test.mjs +0 -269
  381. package/lib/plan/explain.test.mjs +0 -188
  382. package/lib/prompts/parallelism.test.mjs +0 -177
  383. package/lib/rag/rag.test.mjs +0 -505
  384. package/lib/rate-guard.test.mjs +0 -272
  385. package/lib/reactive-gate.test.mjs +0 -57
  386. package/lib/render.test.mjs +0 -68
  387. package/lib/resource-governor.test.mjs +0 -488
  388. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  389. package/lib/scheduling/jitter.test.mjs +0 -140
  390. package/lib/secrets/broker.test.mjs +0 -280
  391. package/lib/secrets/providers.test.mjs +0 -274
  392. package/lib/security/audit-engine.test.mjs +0 -424
  393. package/lib/security/coerce-args.test.mjs +0 -281
  394. package/lib/security/dangerous-tools.test.mjs +0 -68
  395. package/lib/security/external-content.test.mjs +0 -84
  396. package/lib/security/redact.test.mjs +0 -441
  397. package/lib/security/secret-equal.test.mjs +0 -55
  398. package/lib/session/config.test.mjs +0 -92
  399. package/lib/session/feed-core.test.mjs +0 -198
  400. package/lib/session/first-run.test.mjs +0 -121
  401. package/lib/session/frontdoor.test.mjs +0 -205
  402. package/lib/session/handoffs.test.mjs +0 -183
  403. package/lib/session/identity.test.mjs +0 -180
  404. package/lib/session/inbox-claims.test.mjs +0 -286
  405. package/lib/session/launch-args.test.mjs +0 -157
  406. package/lib/session/liveness.test.mjs +0 -100
  407. package/lib/session/status-summary.test.mjs +0 -118
  408. package/lib/session-permissions.test.mjs +0 -120
  409. package/lib/setup/claude-probe.test.mjs +0 -187
  410. package/lib/setup/completeness.test.mjs +0 -110
  411. package/lib/setup/context-pack.test.mjs +0 -89
  412. package/lib/setup/enrich.test.mjs +0 -115
  413. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  414. package/lib/setup/integration.test.mjs +0 -162
  415. package/lib/setup/io.test.mjs +0 -77
  416. package/lib/setup/runner.test.mjs +0 -132
  417. package/lib/setup/sections/identity.test.mjs +0 -234
  418. package/lib/setup/sections/inventory.test.mjs +0 -198
  419. package/lib/setup/sections/learning.test.mjs +0 -81
  420. package/lib/setup/sections/mandate.test.mjs +0 -388
  421. package/lib/setup/sections/messaging.test.mjs +0 -127
  422. package/lib/setup/sections/model.test.mjs +0 -240
  423. package/lib/setup/sections/org.test.mjs +0 -346
  424. package/lib/setup/sections/orgmail.test.mjs +0 -118
  425. package/lib/setup/sections/recovery.test.mjs +0 -98
  426. package/lib/setup/sections/subagents.test.mjs +0 -429
  427. package/lib/setup/sections/verify.test.mjs +0 -175
  428. package/lib/setup/sot.test.mjs +0 -81
  429. package/lib/setup/state.test.mjs +0 -115
  430. package/lib/singleton.test.mjs +0 -151
  431. package/lib/subagents/cli.test.mjs +0 -389
  432. package/lib/subagents/client.test.mjs +0 -309
  433. package/lib/subagents/gap.test.mjs +0 -234
  434. package/lib/subagents/lock.test.mjs +0 -248
  435. package/lib/subagents/manifest.test.mjs +0 -175
  436. package/lib/subagents/refs.test.mjs +0 -204
  437. package/lib/subagents/resolve.test.mjs +0 -422
  438. package/lib/subagents/schema.test.mjs +0 -328
  439. package/lib/telemetry/alerts.test.mjs +0 -109
  440. package/lib/telemetry/collect.test.mjs +0 -1274
  441. package/lib/tool-definitions-integration.test.mjs +0 -83
  442. package/lib/tool-definitions.test.mjs +0 -437
  443. package/lib/upgrade/global-refresh.test.mjs +0 -65
  444. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  445. package/lib/upgrade/post-steps.test.mjs +0 -200
  446. package/lib/upgrade/verify.test.mjs +0 -164
  447. package/lib/util/fetch-timeout.test.mjs +0 -202
  448. package/lib/util/reconnect.test.mjs +0 -369
  449. package/lib/util/unhandled.test.mjs +0 -216
  450. package/lib/voice/outbound.test.mjs +0 -69
  451. package/lib/voice/session-rotation.test.mjs +0 -114
  452. package/lib/voice/stt.test.mjs +0 -226
  453. package/lib/voice/voice.test.mjs +0 -990
  454. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  455. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  456. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  457. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  458. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  459. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  460. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  461. package/scripts/ci/check.test.mjs +0 -194
  462. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  463. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  464. package/scripts/collective/hook-runner.test.mjs +0 -173
  465. package/scripts/cost/fleet-digest.test.mjs +0 -207
  466. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  467. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  468. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  469. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  470. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  471. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  472. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  473. package/scripts/daemon/assurance.test.mjs +0 -1791
  474. package/scripts/daemon/board-mirror.test.mjs +0 -165
  475. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  476. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  477. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  478. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  479. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  480. package/scripts/daemon/classifier.test.mjs +0 -266
  481. package/scripts/daemon/classify-kind.test.mjs +0 -40
  482. package/scripts/daemon/context-compiler.test.mjs +0 -406
  483. package/scripts/daemon/deliver.test.mjs +0 -564
  484. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  485. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  486. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  487. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  488. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  489. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  490. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  491. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  492. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  493. package/scripts/daemon/integration.test.mjs +0 -149
  494. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  495. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  496. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  497. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  498. package/scripts/daemon/responder-cost.test.mjs +0 -68
  499. package/scripts/daemon/responder-history.test.mjs +0 -221
  500. package/scripts/daemon/sdk-version.test.mjs +0 -31
  501. package/scripts/daemon/session-lock.test.mjs +0 -252
  502. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  503. package/scripts/daemon/typing-registry.test.mjs +0 -102
  504. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  505. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  506. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  507. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  508. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  509. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  510. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  511. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  512. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  513. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  514. package/scripts/poller-launchd/install.test.mjs +0 -243
  515. package/scripts/restore-from-backup.test.mjs +0 -181
  516. package/scripts/session/feed.test.mjs +0 -196
  517. package/scripts/session/supervisor-sh.test.mjs +0 -218
  518. package/scripts/session/supervisor.test.mjs +0 -482
  519. package/scripts/setup/configure-macos.test.mjs +0 -306
  520. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  521. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  522. package/scripts/setup/generate-capability.test.mjs +0 -134
  523. package/scripts/setup/init-agent.test.mjs +0 -370
  524. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  525. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  526. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -0,0 +1,216 @@
1
+ /**
2
+ * lib/engine/tools/bash.mjs — the Bash tool.
3
+ *
4
+ * Input contract: `{ command, timeout?, description? }` (timeout in ms).
5
+ * Runs `bash -c <command>` in the run's working directory, non-interactively
6
+ * (stdin closed), in its own process group so a timeout or abort kills the
7
+ * whole tree, not just the shell.
8
+ *
9
+ * The child environment is SCRUBBED (`scrubToolEnv`) of anything shaped like a
10
+ * model or platform credential: every `*_API_KEY`, `*_AUTH_TOKEN`,
11
+ * `*_OAUTH_TOKEN`, and the COHORT_/NEOLITH_/MAESTRO_ tokens, including the
12
+ * engine's own gateway token. A command the model writes must never be able to
13
+ * read the seat token that pays for the model, or an org-leased provider key.
14
+ * Since W5-B (CF-22) the common secret shapes are removed too
15
+ * (`EXTENDED_SECRET_ENV`: GITHUB_TOKEN, AWS_SECRET_ACCESS_KEY, `*_SECRET`,
16
+ * `*_PASSWORD`, `*_PRIVATE_KEY`, …), unless the run's `envPassthrough` names
17
+ * them (`ctx.envPassthrough`). Since CF-118 a variable whose VALUE looks like a
18
+ * credential is removed too, whatever its name (`DATABASE_URL` with a password,
19
+ * a vendor token, a JWT, a long random or hex run: context/secret-values.mjs),
20
+ * under the same passthrough.
21
+ *
22
+ * Deferred to W2 (documented in docs/engine/README.md): background execution,
23
+ * a persistent working directory across calls, and permission rules. Until the
24
+ * permission engine lands, the caller decides whether Bash is exposed at all
25
+ * (`cohort run --tools`).
26
+ *
27
+ * @module lib/engine/tools/bash
28
+ */
29
+
30
+ import { spawn } from "node:child_process";
31
+ import { createProcessTracker, newProcessTag, PROCESS_TAG_ENV } from "../process-tree.mjs";
32
+ import { credentialValueKind } from "../context/secret-values.mjs";
33
+
34
+ export const DEFAULT_TIMEOUT_MS = 120_000;
35
+ export const MAX_TIMEOUT_MS = 600_000;
36
+ export const MAX_OUTPUT_CHARS = 30_000;
37
+
38
+ /**
39
+ * Credential SHAPES, not a list of names. Org provider keys are leased into the
40
+ * spawn env at startup and never written to disk, so this scrub is the only
41
+ * thing between a model-written command and them; an allow-list of names goes
42
+ * stale the day the model-router catalogue gains a provider (it already had:
43
+ * DEEPSEEK_, MOONSHOT_, DASHSCOPE_ were missed). `tools.test.mjs` pins every
44
+ * `auth_env` in `lib/model-router/catalog/*.yaml` against these patterns.
45
+ */
46
+ export const SECRET_ENV = Object.freeze([
47
+ /(^|_)API_KEY$/i, // every provider key, incl. *_ADMIN_API_KEY
48
+ /(^|_)AUTH_TOKEN$/i, // ANTHROPIC_AUTH_TOKEN and kin
49
+ /(^|_)OAUTH_TOKEN$/i, // CLAUDE_CODE_OAUTH_TOKEN
50
+ /^(COHORT|NEOLITH|MAESTRO)_([A-Z0-9]+_)*TOKEN$/i, // COHORT_TOKEN, COHORT_API_TOKEN, COHORT_LLM_TOKEN, NEOLITH_*, MAESTRO_AGENT_TOKEN
51
+ /^DEEPINFRA_TOKEN$/i,
52
+ /^(COHORT|NEOLITH)_LLM_TOKEN_HELPER$/i, // the command that mints the engine's own gateway token
53
+ ]);
54
+
55
+ /**
56
+ * CF-22 (W5-B): common secret shapes beyond model and platform credentials —
57
+ * code-host and cloud tokens, `*_SECRET`, `*_PASSWORD`, `*_PRIVATE_KEY` and
58
+ * named bearer tokens. Scrubbed from the Bash tool, hook commands and MCP
59
+ * children like {@link SECRET_ENV}, except that an explicit passthrough list
60
+ * (settings `cohort.envPassthrough`, COHORT_ENGINE_ENV_PASSTHROUGH) may name
61
+ * one of THESE to let it through. A name that also matches SECRET_ENV is never
62
+ * passed by that list.
63
+ */
64
+ export const EXTENDED_SECRET_ENV = Object.freeze([
65
+ /^(GITHUB|GH|GITLAB|NPM|HF|HUGGING_FACE_HUB|CIRCLE|VERCEL|NETLIFY|RAILWAY)_TOKEN$/i,
66
+ /^AWS_(SECRET_ACCESS_KEY|SESSION_TOKEN)$/i,
67
+ /(^|_)SECRET$/i, // CLIENT_SECRET, SLACK_SIGNING_SECRET, RELAY_SHARED_SECRET
68
+ /(^|_)SECRET_(ACCESS_)?KEY$/i,
69
+ /(^|_)(PASSWORD|PASSWD|PASSPHRASE)$/i,
70
+ /(^|_)PRIVATE_KEY$/i,
71
+ /(^|_)(ACCESS|REFRESH|SESSION|BOT|USER|APP|PERSONAL_ACCESS)_TOKEN$/i,
72
+ ]);
73
+
74
+ /** @param {string} name */
75
+ export function isExtendedSecretName(name) {
76
+ return EXTENDED_SECRET_ENV.some((re) => re.test(name));
77
+ }
78
+
79
+ /**
80
+ * The environment of a model-written command: no credential of either list,
81
+ * and no variable whose VALUE looks like a credential (CF-118,
82
+ * context/secret-values.mjs: a URL with a password, a vendor token, a JWT, a
83
+ * private key, a long random or hex run); `passthrough` names extended-shape or
84
+ * value-shaped variables to keep. A SECRET_ENV name is never kept.
85
+ * @param {NodeJS.ProcessEnv} env
86
+ * @param {{passthrough?:Iterable<string>}} [o]
87
+ * @returns {Record<string,string>}
88
+ */
89
+ export function scrubToolEnv(env, { passthrough = [] } = {}) {
90
+ const pass = new Set(passthrough);
91
+ const out = {};
92
+ for (const [k, v] of Object.entries(env || {})) {
93
+ if (v === undefined) continue;
94
+ if (SECRET_ENV.some((re) => re.test(k))) continue;
95
+ if (!pass.has(k) && (isExtendedSecretName(k) || credentialValueKind(v, { name: k }) !== null)) continue;
96
+ out[k] = v;
97
+ }
98
+ return out;
99
+ }
100
+
101
+ /**
102
+ * Keep the head and tail of oversized output (errors usually live at the end).
103
+ * @param {string} s @param {number} [max]
104
+ */
105
+ export function truncateOutput(s, max = MAX_OUTPUT_CHARS) {
106
+ if (s.length <= max) return s;
107
+ const half = Math.floor(max / 2);
108
+ return `${s.slice(0, half)}\n\n… [${s.length - max} characters omitted] …\n\n${s.slice(-half)}`;
109
+ }
110
+
111
+ export const bashTool = {
112
+ name: "Bash",
113
+ readOnly: false,
114
+ description:
115
+ "Run a shell command with bash in the working directory and return its combined stdout and stderr. " +
116
+ "Commands run non-interactively with no stdin. " +
117
+ `\`timeout\` is in milliseconds (default ${DEFAULT_TIMEOUT_MS}, maximum ${MAX_TIMEOUT_MS}). ` +
118
+ "Give a short `description` of what the command does. Prefer Read, Glob, Grep and Edit over shell equivalents.",
119
+ input_schema: {
120
+ type: "object",
121
+ properties: {
122
+ command: { type: "string", description: "The command to run." },
123
+ timeout: { type: "integer", minimum: 1, maximum: MAX_TIMEOUT_MS, description: "Timeout in milliseconds." },
124
+ description: { type: "string", description: "What the command does, in a few words." },
125
+ },
126
+ required: ["command"],
127
+ },
128
+
129
+ /**
130
+ * @param {{command:string, timeout?:number}} input
131
+ * @param {import('./index.mjs').ToolContext} ctx
132
+ */
133
+ run(input, ctx) {
134
+ const timeoutMs = Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
135
+ return new Promise((resolve) => {
136
+ if (ctx.signal?.aborted) return resolve({ ok: false, error: "Aborted before the command started." });
137
+ // CF-48: a timeout or abort also stops what the command started outside its group (setsid).
138
+ const tag = newProcessTag();
139
+ const rootId = `bash_fg_${tag}`;
140
+ // CF-119: sampled like a background shell (tree and tag, 100 ms, 500 ms, every 2 s, read without
141
+ // holding the event loop; a call that ends sooner costs nothing), and captured again at the kill.
142
+ const tracker = createProcessTracker();
143
+ let child;
144
+ try {
145
+ child = spawn("/bin/bash", ["-c", input.command], {
146
+ cwd: ctx.cwd,
147
+ env: { ...scrubToolEnv(ctx.env, { passthrough: ctx.envPassthrough ?? [] }), [PROCESS_TAG_ENV]: tag },
148
+ stdio: ["ignore", "pipe", "pipe"],
149
+ detached: true,
150
+ });
151
+ } catch (e) {
152
+ return resolve({ ok: false, error: `Could not start bash: ${e instanceof Error ? e.message : e}` });
153
+ }
154
+ if (child.pid) tracker.track({ id: rootId, pid: child.pid, tag });
155
+ let output = "";
156
+ let over = false;
157
+ const onData = (d) => {
158
+ if (output.length > MAX_OUTPUT_CHARS * 4) {
159
+ over = true;
160
+ return;
161
+ }
162
+ output += d.toString("utf8");
163
+ };
164
+ child.stdout.on("data", onData);
165
+ child.stderr.on("data", onData);
166
+
167
+ let reason = null;
168
+ const killTree = (why) => {
169
+ if (reason) return;
170
+ reason = why;
171
+ try {
172
+ tracker.capture([rootId], { tags: true });
173
+ tracker.signal([rootId], "SIGTERM");
174
+ } catch {
175
+ /* the group signal still goes out */
176
+ }
177
+ try {
178
+ process.kill(-child.pid, "SIGTERM");
179
+ } catch {
180
+ /* the group already exited */
181
+ }
182
+ setTimeout(() => {
183
+ try {
184
+ process.kill(-child.pid, "SIGKILL");
185
+ } catch {
186
+ /* exited on SIGTERM, as hoped */
187
+ }
188
+ tracker.signal([rootId], "SIGKILL");
189
+ tracker.dispose();
190
+ }, 2000).unref();
191
+ };
192
+ const timer = setTimeout(() => killTree("timeout"), timeoutMs);
193
+ const onAbort = () => killTree("aborted");
194
+ ctx.signal?.addEventListener("abort", onAbort, { once: true });
195
+
196
+ const finish = (code, signal) => {
197
+ clearTimeout(timer);
198
+ ctx.signal?.removeEventListener("abort", onAbort);
199
+ tracker.rootExited(rootId);
200
+ if (!reason) tracker.dispose(); // a kill disposes after its SIGKILL pass
201
+ const body = truncateOutput(output) + (over ? "\n… [output limit reached; later output dropped]" : "");
202
+ if (reason === "timeout") return resolve({ ok: false, error: `${body}\nCommand timed out after ${timeoutMs} ms.`.trim() });
203
+ if (reason === "aborted") return resolve({ ok: false, error: `${body}\nCommand aborted.`.trim() });
204
+ if (code === 0) return resolve({ ok: true, content: body === "" ? "(no output)" : body });
205
+ const status = code === null ? `killed by ${signal}` : `exit code ${code}`;
206
+ resolve({ ok: false, error: `${body}\nCommand failed: ${status}.`.trim() });
207
+ };
208
+ child.on("error", (e) => {
209
+ clearTimeout(timer);
210
+ tracker.dispose();
211
+ resolve({ ok: false, error: `bash error: ${e.message}` });
212
+ });
213
+ child.on("close", finish);
214
+ });
215
+ },
216
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * lib/engine/tools/edit.mjs — the Edit tool.
3
+ *
4
+ * Input contract: `{ file_path, old_string, new_string, replace_all? }`.
5
+ * Exact string replacement. `old_string` must occur exactly once unless
6
+ * `replace_all` is set — an ambiguous edit is refused with the occurrence
7
+ * count, so the model widens the context instead of editing the wrong site.
8
+ * An empty `old_string` on a file that does not exist creates it.
9
+ *
10
+ * The replacement is a pure function (`applyEdit`); the tool is its edge.
11
+ *
12
+ * @module lib/engine/tools/edit
13
+ */
14
+
15
+ import { readFileSync, statSync } from "node:fs";
16
+ import path from "node:path";
17
+ import { writeFileAtomic } from "../../fs-atomic.mjs";
18
+ import { checkFresh } from "./write.mjs";
19
+
20
+ /**
21
+ * @param {string} text
22
+ * @param {string} oldString
23
+ * @param {string} newString
24
+ * @param {boolean} replaceAll
25
+ * @returns {{ ok:true, text:string, count:number } | { ok:false, error:string }}
26
+ */
27
+ export function applyEdit(text, oldString, newString, replaceAll) {
28
+ if (oldString === newString) return { ok: false, error: "old_string and new_string are identical; nothing to change." };
29
+ if (oldString === "") return { ok: false, error: "old_string is empty; give the exact text to replace." };
30
+ const parts = text.split(oldString);
31
+ const count = parts.length - 1;
32
+ if (count === 0) return { ok: false, error: "old_string was not found in the file. Read the file and copy the text exactly." };
33
+ if (count > 1 && !replaceAll) {
34
+ return {
35
+ ok: false,
36
+ error: `old_string occurs ${count} times. Include more surrounding text to make it unique, or set replace_all.`,
37
+ };
38
+ }
39
+ // split/join, not String.replace: a `$&` in new_string must stay literal.
40
+ return { ok: true, text: parts.join(newString), count };
41
+ }
42
+
43
+ export const editTool = {
44
+ name: "Edit",
45
+ readOnly: false,
46
+ description:
47
+ "Replace an exact string in a file. `file_path` must be absolute and the file must have been Read first. " +
48
+ "`old_string` must match the file exactly (including whitespace) and occur once, unless `replace_all` is true. " +
49
+ "With an empty `old_string`, creates a new file containing `new_string`.",
50
+ input_schema: {
51
+ type: "object",
52
+ properties: {
53
+ file_path: { type: "string", description: "Absolute path of the file to change." },
54
+ old_string: { type: "string", description: "The exact text to replace." },
55
+ new_string: { type: "string", description: "The replacement text." },
56
+ replace_all: { type: "boolean", description: "Replace every occurrence instead of exactly one." },
57
+ },
58
+ required: ["file_path", "old_string", "new_string"],
59
+ },
60
+
61
+ /**
62
+ * @param {{file_path:string, old_string:string, new_string:string, replace_all?:boolean}} input
63
+ * @param {import('./index.mjs').ToolContext} ctx
64
+ */
65
+ async run(input, ctx) {
66
+ const file = input.file_path;
67
+ if (!path.isAbsolute(file)) return { ok: false, error: `file_path must be absolute, got "${file}"` };
68
+ let st = null;
69
+ try {
70
+ st = statSync(file);
71
+ } catch {
72
+ st = null; // absent: only the create form (empty old_string) is valid
73
+ }
74
+ if (!st) {
75
+ if (input.old_string !== "") return { ok: false, error: `File does not exist: ${file}` };
76
+ try {
77
+ writeFileAtomic(file, input.new_string);
78
+ } catch (e) {
79
+ return { ok: false, error: `Could not create ${file}: ${e instanceof Error ? e.message : e}` };
80
+ }
81
+ ctx.fileState.set(file, { mtimeMs: statSync(file).mtimeMs });
82
+ return { ok: true, content: `Created ${file}.` };
83
+ }
84
+ if (st.isDirectory()) return { ok: false, error: `${file} is a directory.` };
85
+ const fresh = checkFresh(file, st, ctx.fileState);
86
+ if (!fresh.ok) return fresh;
87
+ const r = applyEdit(readFileSync(file, "utf8"), input.old_string, input.new_string, Boolean(input.replace_all));
88
+ if (!r.ok) return r;
89
+ try {
90
+ writeFileAtomic(file, r.text, { mode: st.mode & 0o7777 });
91
+ } catch (e) {
92
+ return { ok: false, error: `Could not write ${file}: ${e instanceof Error ? e.message : e}` };
93
+ }
94
+ ctx.fileState.set(file, { mtimeMs: statSync(file).mtimeMs });
95
+ return { ok: true, content: `Edited ${file} (${r.count} replacement${r.count === 1 ? "" : "s"}).` };
96
+ },
97
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * lib/engine/tools/glob.mjs — the Glob tool.
3
+ *
4
+ * Input contract: `{ pattern, path? }`. Returns matching file paths (absolute),
5
+ * newest first, capped. Candidate files come from ripgrep when available
6
+ * (honours .gitignore) or the pure-JS walk; matching is always `matchGlob`, so
7
+ * both paths agree on semantics.
8
+ *
9
+ * @module lib/engine/tools/glob
10
+ */
11
+
12
+ import { statSync } from "node:fs";
13
+ import path from "node:path";
14
+ import { hasRipgrep, runRipgrep, walkFiles, matchGlob, sortByMtime, toPosix, RG_EXCLUDES } from "./walk.mjs";
15
+
16
+ export const MAX_GLOB_RESULTS = 100;
17
+
18
+ /**
19
+ * Resolve the search root for Glob/Grep: absolute, or relative to cwd.
20
+ * @param {string|undefined} p @param {string} cwd
21
+ */
22
+ export function resolveRoot(p, cwd) {
23
+ if (!p) return cwd;
24
+ return path.isAbsolute(p) ? p : path.resolve(cwd, p);
25
+ }
26
+
27
+ /**
28
+ * List candidate files under root (absolute paths).
29
+ * @param {string} root
30
+ * @param {import('./index.mjs').ToolContext} ctx
31
+ * @returns {Promise<{ok:true, files:string[]} | {ok:false, error:string}>}
32
+ */
33
+ export async function listFiles(root, ctx) {
34
+ if (ctx.useRipgrep ?? hasRipgrep(ctx.env)) {
35
+ const r = await runRipgrep(["--files", "--no-config", "--no-messages", ...RG_EXCLUDES, "--", root], { cwd: ctx.cwd, signal: ctx.signal });
36
+ if (r.ok) return { ok: true, files: r.stdout.split("\n").filter(Boolean) };
37
+ // ripgrep present but failing: the walk still answers.
38
+ }
39
+ return { ok: true, files: walkFiles(root, { signal: ctx.signal }) };
40
+ }
41
+
42
+ export const globTool = {
43
+ name: "Glob",
44
+ readOnly: true,
45
+ description:
46
+ "Find files by name pattern, such as `**/*.mjs` or `src/**/*.{ts,tsx}`. Returns absolute paths, most recently modified first. " +
47
+ "A pattern without a slash matches file names at any depth. `path` is the directory to search (default: the working directory). " +
48
+ "Use Grep to search file contents.",
49
+ input_schema: {
50
+ type: "object",
51
+ properties: {
52
+ pattern: { type: "string", description: "Glob pattern to match file paths against." },
53
+ path: { type: "string", description: "Directory to search in. Defaults to the working directory." },
54
+ },
55
+ required: ["pattern"],
56
+ },
57
+
58
+ /**
59
+ * @param {{pattern:string, path?:string}} input
60
+ * @param {import('./index.mjs').ToolContext} ctx
61
+ */
62
+ async run(input, ctx) {
63
+ const root = resolveRoot(input.path, ctx.cwd);
64
+ try {
65
+ if (!statSync(root).isDirectory()) return { ok: false, error: `${root} is not a directory.` };
66
+ } catch {
67
+ return { ok: false, error: `Directory does not exist: ${root}` };
68
+ }
69
+ const listed = await listFiles(root, ctx);
70
+ if (!listed.ok) return listed;
71
+ const matches = listed.files.filter((f) => matchGlob(toPosix(path.relative(root, f)), input.pattern));
72
+ if (matches.length === 0) return { ok: true, content: "No files found." };
73
+ const sorted = sortByMtime(matches);
74
+ const shown = sorted.slice(0, MAX_GLOB_RESULTS);
75
+ const more = sorted.length - shown.length;
76
+ return {
77
+ ok: true,
78
+ content: shown.join("\n") + (more > 0 ? `\n(${more} more results not shown; narrow the pattern or path)` : ""),
79
+ };
80
+ },
81
+ };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * lib/engine/tools/grep.mjs — the Grep tool.
3
+ *
4
+ * Input contract: `{ pattern, path?, glob?, type?, output_mode?, "-i"?, "-n"?,
5
+ * "-A"?, "-B"?, "-C"?, multiline?, head_limit? }`. `output_mode` is
6
+ * `files_with_matches` (default: paths, newest first), `content` (matching
7
+ * lines, `path:line:text`) or `count` (`path:count`).
8
+ *
9
+ * Ripgrep when available; otherwise a pure-JS search with JavaScript regex
10
+ * syntax (close to, not identical to, ripgrep's). `glob` is always decided by
11
+ * `matchGlob` in both modes: ripgrep's own `--glob` anchors slash patterns
12
+ * differently (`/src/*.mjs` matches for rg, not for us), so in ripgrep mode
13
+ * the candidates come from `rg --files` and are filtered here before the
14
+ * search. `type` uses ripgrep's type table in ripgrep mode and
15
+ * `TYPE_EXTENSIONS` in the fallback.
16
+ *
17
+ * @module lib/engine/tools/grep
18
+ */
19
+
20
+ import { readFileSync, statSync } from "node:fs";
21
+ import path from "node:path";
22
+ import { hasRipgrep, runRipgrep, walkFiles, matchGlob, sortByMtime, toPosix, RG_EXCLUDES } from "./walk.mjs";
23
+ import { resolveRoot } from "./glob.mjs";
24
+
25
+ export const DEFAULT_HEAD_LIMIT = 250;
26
+ const MAX_SEARCH_BYTES = 2 * 1024 * 1024;
27
+ /** Explicit file arguments per ripgrep invocation, well under ARG_MAX. */
28
+ const RG_FILES_PER_CALL = 500;
29
+
30
+ /** A small file-type table for the fallback; ripgrep uses its own. */
31
+ export const TYPE_EXTENSIONS = {
32
+ js: [".js", ".mjs", ".cjs", ".jsx"],
33
+ ts: [".ts", ".tsx", ".mts", ".cts"],
34
+ py: [".py"],
35
+ go: [".go"],
36
+ rust: [".rs"],
37
+ java: [".java"],
38
+ md: [".md", ".markdown"],
39
+ json: [".json"],
40
+ yaml: [".yaml", ".yml"],
41
+ css: [".css", ".scss"],
42
+ html: [".html", ".htm"],
43
+ sh: [".sh", ".bash", ".zsh"],
44
+ sql: [".sql"],
45
+ };
46
+
47
+ /**
48
+ * Pure: search one file's text and render in the requested mode.
49
+ * @param {string} file display path
50
+ * @param {string} text
51
+ * @param {RegExp} re must carry the `g` flag
52
+ * @param {{ mode: 'content'|'count'|'files_with_matches', lineNumbers: boolean, before: number, after: number, multiline: boolean }} o
53
+ * @returns {string[]} output lines (empty when no match)
54
+ */
55
+ export function searchText(file, text, re, o) {
56
+ if (o.multiline) {
57
+ re.lastIndex = 0;
58
+ const hits = [...text.matchAll(re)];
59
+ if (hits.length === 0) return [];
60
+ if (o.mode === "files_with_matches") return [file];
61
+ if (o.mode === "count") return [`${file}:${hits.length}`];
62
+ return hits.map((m) => {
63
+ const line = text.slice(0, m.index).split("\n").length;
64
+ return o.lineNumbers ? `${file}:${line}:${m[0]}` : `${file}:${m[0]}`;
65
+ });
66
+ }
67
+ const lines = text.split(/\r?\n/);
68
+ const matched = [];
69
+ const single = new RegExp(re.source, re.flags.replace("g", ""));
70
+ lines.forEach((l, i) => {
71
+ if (single.test(l)) matched.push(i);
72
+ });
73
+ if (matched.length === 0) return [];
74
+ if (o.mode === "files_with_matches") return [file];
75
+ if (o.mode === "count") return [`${file}:${matched.length}`];
76
+ const include = new Set();
77
+ for (const i of matched) for (let j = Math.max(0, i - o.before); j <= Math.min(lines.length - 1, i + o.after); j++) include.add(j);
78
+ const hit = new Set(matched);
79
+ const out = [];
80
+ let prev = -2;
81
+ for (const j of [...include].sort((a, b) => a - b)) {
82
+ if (prev >= 0 && j > prev + 1) out.push("--");
83
+ const sep = hit.has(j) ? ":" : "-";
84
+ out.push(o.lineNumbers ? `${file}${sep}${j + 1}${sep}${lines[j]}` : `${file}${sep}${lines[j]}`);
85
+ prev = j;
86
+ }
87
+ return out;
88
+ }
89
+
90
+ export const grepTool = {
91
+ name: "Grep",
92
+ readOnly: true,
93
+ description:
94
+ "Search file contents with a regular expression. `output_mode` is `files_with_matches` (default; file paths, newest first), " +
95
+ "`content` (matching lines as path:line:text) or `count` (matches per file). " +
96
+ "Narrow the search with `path`, `glob` (e.g. `*.mjs`) or `type` (e.g. `js`, `py`). " +
97
+ "`-i` ignores case; `-A`/`-B`/`-C` add context lines in content mode; `multiline` lets a pattern span lines; " +
98
+ `\`head_limit\` caps output lines (default ${DEFAULT_HEAD_LIMIT}).`,
99
+ input_schema: {
100
+ type: "object",
101
+ properties: {
102
+ pattern: { type: "string", description: "Regular expression to search for." },
103
+ path: { type: "string", description: "File or directory to search. Defaults to the working directory." },
104
+ glob: { type: "string", description: "Only search files whose names match this glob." },
105
+ type: { type: "string", description: "Only search files of this type (js, ts, py, go, rust, md, json, …)." },
106
+ output_mode: { type: "string", enum: ["content", "files_with_matches", "count"], description: "What to return." },
107
+ "-i": { type: "boolean", description: "Case-insensitive search." },
108
+ "-n": { type: "boolean", description: "Show line numbers in content mode (default true)." },
109
+ "-A": { type: "integer", minimum: 0, description: "Lines of context after each match (content mode)." },
110
+ "-B": { type: "integer", minimum: 0, description: "Lines of context before each match (content mode)." },
111
+ "-C": { type: "integer", minimum: 0, description: "Lines of context around each match (content mode)." },
112
+ multiline: { type: "boolean", description: "Allow the pattern to match across lines." },
113
+ head_limit: { type: "integer", minimum: 1, description: "Maximum number of output lines." },
114
+ },
115
+ required: ["pattern"],
116
+ },
117
+
118
+ /**
119
+ * @param {any} input
120
+ * @param {import('./index.mjs').ToolContext} ctx
121
+ */
122
+ async run(input, ctx) {
123
+ const root = resolveRoot(input.path, ctx.cwd);
124
+ let rootStat;
125
+ try {
126
+ rootStat = statSync(root);
127
+ } catch {
128
+ return { ok: false, error: `Path does not exist: ${root}` };
129
+ }
130
+ const mode = input.output_mode || "files_with_matches";
131
+ const o = {
132
+ mode,
133
+ lineNumbers: input["-n"] ?? true,
134
+ before: input["-B"] ?? input["-C"] ?? 0,
135
+ after: input["-A"] ?? input["-C"] ?? 0,
136
+ multiline: Boolean(input.multiline),
137
+ };
138
+ const headLimit = input.head_limit ?? DEFAULT_HEAD_LIMIT;
139
+ if (input.type && !TYPE_EXTENSIONS[input.type] && !(ctx.useRipgrep ?? hasRipgrep(ctx.env))) {
140
+ return { ok: false, error: `Unknown type "${input.type}". Known: ${Object.keys(TYPE_EXTENSIONS).join(", ")}.` };
141
+ }
142
+
143
+ /** Path a glob is matched against: relative to a directory root, else the basename. */
144
+ const globPath = (f) => toPosix(rootStat.isDirectory() ? path.relative(root, f) : path.basename(f));
145
+
146
+ let lines;
147
+ if (ctx.useRipgrep ?? hasRipgrep(ctx.env)) {
148
+ const args = ["--no-config", "--no-messages", "--color", "never", ...RG_EXCLUDES];
149
+ if (mode === "files_with_matches") args.push("-l");
150
+ else if (mode === "count") args.push("-c", "--with-filename");
151
+ else {
152
+ args.push("--with-filename", "--no-heading");
153
+ if (o.lineNumbers) args.push("-n");
154
+ if (o.before) args.push("-B", String(o.before));
155
+ if (o.after) args.push("-A", String(o.after));
156
+ }
157
+ if (input["-i"]) args.push("-i");
158
+ if (o.multiline) args.push("-U", "--multiline-dotall");
159
+ if (input.type) args.push("--type", input.type);
160
+ args.push("-e", input.pattern, "--");
161
+
162
+ /** @type {string[][]} */
163
+ let targetChunks = [[root]];
164
+ if (input.glob) {
165
+ // ripgrep searches explicitly named files regardless of --type/--glob,
166
+ // so the type filter goes on the listing, and the glob is ours.
167
+ let candidates = [root];
168
+ if (rootStat.isDirectory()) {
169
+ const listArgs = ["--files", "--no-config", "--no-messages", ...RG_EXCLUDES];
170
+ if (input.type) listArgs.push("--type", input.type);
171
+ const listed = await runRipgrep([...listArgs, "--", root], { cwd: ctx.cwd, signal: ctx.signal });
172
+ if (!listed.ok) return { ok: false, error: listed.error };
173
+ candidates = listed.stdout.split("\n").filter(Boolean);
174
+ }
175
+ const selected = candidates.filter((f) => matchGlob(globPath(f), input.glob));
176
+ targetChunks = [];
177
+ for (let i = 0; i < selected.length; i += RG_FILES_PER_CALL) targetChunks.push(selected.slice(i, i + RG_FILES_PER_CALL));
178
+ }
179
+ lines = [];
180
+ for (const chunk of targetChunks) {
181
+ if (ctx.signal?.aborted) break;
182
+ const r = await runRipgrep([...args, ...chunk], { cwd: ctx.cwd, signal: ctx.signal });
183
+ if (!r.ok) return { ok: false, error: r.error };
184
+ lines.push(...r.stdout.split("\n").filter((l) => l !== ""));
185
+ if (lines.length > headLimit * 4) break;
186
+ }
187
+ if (mode === "files_with_matches") lines = sortByMtime(lines);
188
+ } else {
189
+ let re;
190
+ try {
191
+ re = new RegExp(input.pattern, `g${input["-i"] ? "i" : ""}${o.multiline ? "s" : ""}`);
192
+ } catch (e) {
193
+ return { ok: false, error: `Invalid regular expression: ${e instanceof Error ? e.message : e}` };
194
+ }
195
+ const files = rootStat.isDirectory() ? walkFiles(root, { signal: ctx.signal }) : [root];
196
+ const exts = input.type ? TYPE_EXTENSIONS[input.type] : null;
197
+ const selected = files.filter((f) => {
198
+ if (exts && !exts.includes(path.extname(f))) return false;
199
+ if (input.glob && !matchGlob(globPath(f), input.glob)) return false;
200
+ return true;
201
+ });
202
+ lines = [];
203
+ const ordered = mode === "files_with_matches" ? sortByMtime(selected) : selected.sort();
204
+ for (const f of ordered) {
205
+ if (ctx.signal?.aborted) break;
206
+ let buf;
207
+ try {
208
+ if (statSync(f).size > MAX_SEARCH_BYTES) continue;
209
+ buf = readFileSync(f);
210
+ } catch {
211
+ continue; // unreadable or vanished file: skip, as ripgrep does with --no-messages
212
+ }
213
+ if (buf.subarray(0, 8192).includes(0)) continue; // binary
214
+ lines.push(...searchText(f, buf.toString("utf8"), re, o));
215
+ if (lines.length > headLimit * 4) break;
216
+ }
217
+ }
218
+
219
+ if (lines.length === 0) return { ok: true, content: "No matches found." };
220
+ const shown = lines.slice(0, headLimit);
221
+ const more = lines.length - shown.length;
222
+ return { ok: true, content: shown.join("\n") + (more > 0 ? `\n(output truncated at ${headLimit} lines)` : "") };
223
+ },
224
+ };