@cohortapp/agent-sdk 2.16.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 (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  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/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +29 -2
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. 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
+ };