agent-nuvira 3.3.12 → 3.3.14

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 (458) hide show
  1. package/README.md +67 -1
  2. package/dist/agents/agents/planner.d.ts.map +1 -1
  3. package/dist/agents/agents/planner.js +9 -0
  4. package/dist/agents/agents/planner.js.map +1 -1
  5. package/dist/agents/agents/reasoner.d.ts.map +1 -1
  6. package/dist/agents/agents/reasoner.js +13 -0
  7. package/dist/agents/agents/reasoner.js.map +1 -1
  8. package/dist/agents/orchestrator.d.ts +31 -0
  9. package/dist/agents/orchestrator.d.ts.map +1 -1
  10. package/dist/agents/orchestrator.js +103 -135
  11. package/dist/agents/orchestrator.js.map +1 -1
  12. package/dist/cli/chat.d.ts +89 -0
  13. package/dist/cli/chat.d.ts.map +1 -1
  14. package/dist/cli/chat.js +798 -39
  15. package/dist/cli/chat.js.map +1 -1
  16. package/dist/cli/cli-program.d.ts.map +1 -1
  17. package/dist/cli/cli-program.js +20 -0
  18. package/dist/cli/cli-program.js.map +1 -1
  19. package/dist/cli/config.d.ts +12 -0
  20. package/dist/cli/config.d.ts.map +1 -1
  21. package/dist/cli/config.js +107 -3
  22. package/dist/cli/config.js.map +1 -1
  23. package/dist/cli/decisions.d.ts +28 -0
  24. package/dist/cli/decisions.d.ts.map +1 -0
  25. package/dist/cli/decisions.js +140 -0
  26. package/dist/cli/decisions.js.map +1 -0
  27. package/dist/cli/doctor.d.ts +6 -0
  28. package/dist/cli/doctor.d.ts.map +1 -1
  29. package/dist/cli/doctor.js +46 -0
  30. package/dist/cli/doctor.js.map +1 -1
  31. package/dist/cli/execute.d.ts.map +1 -1
  32. package/dist/cli/execute.js +6 -0
  33. package/dist/cli/execute.js.map +1 -1
  34. package/dist/cli/failover-runner.d.ts.map +1 -1
  35. package/dist/cli/failover-runner.js +19 -1
  36. package/dist/cli/failover-runner.js.map +1 -1
  37. package/dist/cli/feedback.d.ts.map +1 -1
  38. package/dist/cli/feedback.js +5 -1
  39. package/dist/cli/feedback.js.map +1 -1
  40. package/dist/cli/hooks.d.ts +30 -0
  41. package/dist/cli/hooks.d.ts.map +1 -0
  42. package/dist/cli/hooks.js +224 -0
  43. package/dist/cli/hooks.js.map +1 -0
  44. package/dist/cli/knowledge.d.ts +31 -0
  45. package/dist/cli/knowledge.d.ts.map +1 -0
  46. package/dist/cli/knowledge.js +223 -0
  47. package/dist/cli/knowledge.js.map +1 -0
  48. package/dist/cli/loop-executor.d.ts.map +1 -1
  49. package/dist/cli/loop-executor.js +92 -18
  50. package/dist/cli/loop-executor.js.map +1 -1
  51. package/dist/cli/model.d.ts +20 -0
  52. package/dist/cli/model.d.ts.map +1 -1
  53. package/dist/cli/model.js +294 -14
  54. package/dist/cli/model.js.map +1 -1
  55. package/dist/cli/omniroute-control.d.ts +88 -0
  56. package/dist/cli/omniroute-control.d.ts.map +1 -0
  57. package/dist/cli/omniroute-control.js +179 -0
  58. package/dist/cli/omniroute-control.js.map +1 -0
  59. package/dist/cli/omniroute.d.ts +21 -0
  60. package/dist/cli/omniroute.d.ts.map +1 -0
  61. package/dist/cli/omniroute.js +67 -0
  62. package/dist/cli/omniroute.js.map +1 -0
  63. package/dist/cli/rate.d.ts +78 -0
  64. package/dist/cli/rate.d.ts.map +1 -0
  65. package/dist/cli/rate.js +294 -0
  66. package/dist/cli/rate.js.map +1 -0
  67. package/dist/config/process-env.d.ts.map +1 -1
  68. package/dist/config/process-env.js +10 -0
  69. package/dist/config/process-env.js.map +1 -1
  70. package/dist/config/types.d.ts +8 -0
  71. package/dist/config/types.d.ts.map +1 -1
  72. package/dist/context/history.d.ts +21 -0
  73. package/dist/context/history.d.ts.map +1 -1
  74. package/dist/context/history.js +50 -0
  75. package/dist/context/history.js.map +1 -1
  76. package/dist/enterprise/vault.d.ts.map +1 -1
  77. package/dist/enterprise/vault.js +5 -21
  78. package/dist/enterprise/vault.js.map +1 -1
  79. package/dist/gateway/hook-contract.d.ts +206 -0
  80. package/dist/gateway/hook-contract.d.ts.map +1 -0
  81. package/dist/gateway/hook-contract.js +449 -0
  82. package/dist/gateway/hook-contract.js.map +1 -0
  83. package/dist/gateway/hooks.d.ts.map +1 -1
  84. package/dist/gateway/hooks.js +14 -0
  85. package/dist/gateway/hooks.js.map +1 -1
  86. package/dist/inference/gemini-adapter.d.ts.map +1 -1
  87. package/dist/inference/gemini-adapter.js +4 -0
  88. package/dist/inference/gemini-adapter.js.map +1 -1
  89. package/dist/inference/interface.d.ts +54 -0
  90. package/dist/inference/interface.d.ts.map +1 -1
  91. package/dist/inference/model-id-validation.d.ts +57 -0
  92. package/dist/inference/model-id-validation.d.ts.map +1 -0
  93. package/dist/inference/model-id-validation.js +127 -0
  94. package/dist/inference/model-id-validation.js.map +1 -0
  95. package/dist/inference/native-tools.d.ts +16 -1
  96. package/dist/inference/native-tools.d.ts.map +1 -1
  97. package/dist/inference/native-tools.js +34 -10
  98. package/dist/inference/native-tools.js.map +1 -1
  99. package/dist/inference/openai-compat-adapter.d.ts.map +1 -1
  100. package/dist/inference/openai-compat-adapter.js +12 -1
  101. package/dist/inference/openai-compat-adapter.js.map +1 -1
  102. package/dist/inference/provider-catalog.d.ts +11 -0
  103. package/dist/inference/provider-catalog.d.ts.map +1 -1
  104. package/dist/inference/provider-catalog.js +80 -0
  105. package/dist/inference/provider-catalog.js.map +1 -1
  106. package/dist/inference/route-resolver.d.ts +32 -8
  107. package/dist/inference/route-resolver.d.ts.map +1 -1
  108. package/dist/inference/route-resolver.js +175 -1
  109. package/dist/inference/route-resolver.js.map +1 -1
  110. package/dist/inference/tool-call-utils.d.ts.map +1 -1
  111. package/dist/inference/tool-call-utils.js +10 -8
  112. package/dist/inference/tool-call-utils.js.map +1 -1
  113. package/dist/inference/tools.d.ts +17 -1
  114. package/dist/inference/tools.d.ts.map +1 -1
  115. package/dist/inference/tools.js +69 -19
  116. package/dist/inference/tools.js.map +1 -1
  117. package/dist/learning/acceptance-model.d.ts +247 -0
  118. package/dist/learning/acceptance-model.d.ts.map +1 -0
  119. package/dist/learning/acceptance-model.js +708 -0
  120. package/dist/learning/acceptance-model.js.map +1 -0
  121. package/dist/learning/auto-router.d.ts +121 -24
  122. package/dist/learning/auto-router.d.ts.map +1 -1
  123. package/dist/learning/auto-router.js +374 -96
  124. package/dist/learning/auto-router.js.map +1 -1
  125. package/dist/learning/autonomy-policy.d.ts +12 -16
  126. package/dist/learning/autonomy-policy.d.ts.map +1 -1
  127. package/dist/learning/autonomy-policy.js +50 -48
  128. package/dist/learning/autonomy-policy.js.map +1 -1
  129. package/dist/learning/behavioural-labels.d.ts +86 -0
  130. package/dist/learning/behavioural-labels.d.ts.map +1 -0
  131. package/dist/learning/behavioural-labels.js +166 -0
  132. package/dist/learning/behavioural-labels.js.map +1 -0
  133. package/dist/learning/capability-evidence.d.ts +167 -0
  134. package/dist/learning/capability-evidence.d.ts.map +1 -0
  135. package/dist/learning/capability-evidence.js +210 -0
  136. package/dist/learning/capability-evidence.js.map +1 -0
  137. package/dist/learning/capability-readiness.d.ts +64 -0
  138. package/dist/learning/capability-readiness.d.ts.map +1 -0
  139. package/dist/learning/capability-readiness.js +74 -0
  140. package/dist/learning/capability-readiness.js.map +1 -0
  141. package/dist/learning/capability-types.d.ts +169 -0
  142. package/dist/learning/capability-types.d.ts.map +1 -0
  143. package/dist/learning/capability-types.js +20 -0
  144. package/dist/learning/capability-types.js.map +1 -0
  145. package/dist/learning/catalog-feed.d.ts +89 -0
  146. package/dist/learning/catalog-feed.d.ts.map +1 -0
  147. package/dist/learning/catalog-feed.js +211 -0
  148. package/dist/learning/catalog-feed.js.map +1 -0
  149. package/dist/learning/cli-intent-effects.d.ts +60 -0
  150. package/dist/learning/cli-intent-effects.d.ts.map +1 -0
  151. package/dist/learning/cli-intent-effects.js +153 -0
  152. package/dist/learning/cli-intent-effects.js.map +1 -0
  153. package/dist/learning/command-adaptation.d.ts +41 -0
  154. package/dist/learning/command-adaptation.d.ts.map +1 -0
  155. package/dist/learning/command-adaptation.js +122 -0
  156. package/dist/learning/command-adaptation.js.map +1 -0
  157. package/dist/learning/consent-picture.d.ts +42 -0
  158. package/dist/learning/consent-picture.d.ts.map +1 -0
  159. package/dist/learning/consent-picture.js +53 -0
  160. package/dist/learning/consent-picture.js.map +1 -0
  161. package/dist/learning/context-budget.d.ts +37 -6
  162. package/dist/learning/context-budget.d.ts.map +1 -1
  163. package/dist/learning/context-budget.js +38 -9
  164. package/dist/learning/context-budget.js.map +1 -1
  165. package/dist/learning/decision-log.d.ts +114 -0
  166. package/dist/learning/decision-log.d.ts.map +1 -0
  167. package/dist/learning/decision-log.js +296 -0
  168. package/dist/learning/decision-log.js.map +1 -0
  169. package/dist/learning/deliverable-class.d.ts.map +1 -1
  170. package/dist/learning/deliverable-class.js +19 -0
  171. package/dist/learning/deliverable-class.js.map +1 -1
  172. package/dist/learning/deliverable-corpus.d.ts +117 -0
  173. package/dist/learning/deliverable-corpus.d.ts.map +1 -0
  174. package/dist/learning/deliverable-corpus.js +176 -0
  175. package/dist/learning/deliverable-corpus.js.map +1 -0
  176. package/dist/learning/error-repair.d.ts +45 -2
  177. package/dist/learning/error-repair.d.ts.map +1 -1
  178. package/dist/learning/error-repair.js +95 -0
  179. package/dist/learning/error-repair.js.map +1 -1
  180. package/dist/learning/exhaustion-report.d.ts +136 -0
  181. package/dist/learning/exhaustion-report.d.ts.map +1 -0
  182. package/dist/learning/exhaustion-report.js +239 -0
  183. package/dist/learning/exhaustion-report.js.map +1 -0
  184. package/dist/learning/failure-bookkeeping.d.ts +16 -0
  185. package/dist/learning/failure-bookkeeping.d.ts.map +1 -1
  186. package/dist/learning/failure-bookkeeping.js +51 -5
  187. package/dist/learning/failure-bookkeeping.js.map +1 -1
  188. package/dist/learning/gate-friction.d.ts +39 -0
  189. package/dist/learning/gate-friction.d.ts.map +1 -0
  190. package/dist/learning/gate-friction.js +91 -0
  191. package/dist/learning/gate-friction.js.map +1 -0
  192. package/dist/learning/harness-narration.d.ts +34 -0
  193. package/dist/learning/harness-narration.d.ts.map +1 -0
  194. package/dist/learning/harness-narration.js +58 -0
  195. package/dist/learning/harness-narration.js.map +1 -0
  196. package/dist/learning/hub-skill-catalog.d.ts.map +1 -1
  197. package/dist/learning/hub-skill-catalog.js +1 -2
  198. package/dist/learning/hub-skill-catalog.js.map +1 -1
  199. package/dist/learning/hybrid-router.d.ts +65 -0
  200. package/dist/learning/hybrid-router.d.ts.map +1 -1
  201. package/dist/learning/hybrid-router.js +186 -0
  202. package/dist/learning/hybrid-router.js.map +1 -1
  203. package/dist/learning/knowledge-base.d.ts +350 -0
  204. package/dist/learning/knowledge-base.d.ts.map +1 -0
  205. package/dist/learning/knowledge-base.js +906 -0
  206. package/dist/learning/knowledge-base.js.map +1 -0
  207. package/dist/learning/knowledge-eval.d.ts +171 -0
  208. package/dist/learning/knowledge-eval.d.ts.map +1 -0
  209. package/dist/learning/knowledge-eval.js +196 -0
  210. package/dist/learning/knowledge-eval.js.map +1 -0
  211. package/dist/learning/knowledge-turn.d.ts +92 -0
  212. package/dist/learning/knowledge-turn.d.ts.map +1 -0
  213. package/dist/learning/knowledge-turn.js +190 -0
  214. package/dist/learning/knowledge-turn.js.map +1 -0
  215. package/dist/learning/learned-commands.d.ts +64 -0
  216. package/dist/learning/learned-commands.d.ts.map +1 -0
  217. package/dist/learning/learned-commands.js +108 -0
  218. package/dist/learning/learned-commands.js.map +1 -0
  219. package/dist/learning/lexical-search.d.ts +50 -0
  220. package/dist/learning/lexical-search.d.ts.map +1 -0
  221. package/dist/learning/lexical-search.js +111 -0
  222. package/dist/learning/lexical-search.js.map +1 -0
  223. package/dist/learning/machine-facts.d.ts +71 -0
  224. package/dist/learning/machine-facts.d.ts.map +1 -0
  225. package/dist/learning/machine-facts.js +164 -0
  226. package/dist/learning/machine-facts.js.map +1 -0
  227. package/dist/learning/markdown-outline.d.ts +73 -0
  228. package/dist/learning/markdown-outline.d.ts.map +1 -0
  229. package/dist/learning/markdown-outline.js +136 -0
  230. package/dist/learning/markdown-outline.js.map +1 -0
  231. package/dist/learning/model-first-router.d.ts +33 -1
  232. package/dist/learning/model-first-router.d.ts.map +1 -1
  233. package/dist/learning/model-first-router.js +123 -9
  234. package/dist/learning/model-first-router.js.map +1 -1
  235. package/dist/learning/model-identity.d.ts +80 -0
  236. package/dist/learning/model-identity.d.ts.map +1 -0
  237. package/dist/learning/model-identity.js +115 -0
  238. package/dist/learning/model-identity.js.map +1 -0
  239. package/dist/learning/model-registry.d.ts +141 -2
  240. package/dist/learning/model-registry.d.ts.map +1 -1
  241. package/dist/learning/model-registry.js +324 -38
  242. package/dist/learning/model-registry.js.map +1 -1
  243. package/dist/learning/model-scoring.d.ts.map +1 -1
  244. package/dist/learning/model-scoring.js +8 -1
  245. package/dist/learning/model-scoring.js.map +1 -1
  246. package/dist/learning/model-selection.d.ts +2 -1
  247. package/dist/learning/model-selection.d.ts.map +1 -1
  248. package/dist/learning/model-selection.js +76 -2
  249. package/dist/learning/model-selection.js.map +1 -1
  250. package/dist/learning/outcome-observation.d.ts +49 -0
  251. package/dist/learning/outcome-observation.d.ts.map +1 -0
  252. package/dist/learning/outcome-observation.js +95 -0
  253. package/dist/learning/outcome-observation.js.map +1 -0
  254. package/dist/learning/pair-entitlement.d.ts +184 -0
  255. package/dist/learning/pair-entitlement.d.ts.map +1 -0
  256. package/dist/learning/pair-entitlement.js +214 -0
  257. package/dist/learning/pair-entitlement.js.map +1 -0
  258. package/dist/learning/project-docs.d.ts +83 -0
  259. package/dist/learning/project-docs.d.ts.map +1 -0
  260. package/dist/learning/project-docs.js +132 -0
  261. package/dist/learning/project-docs.js.map +1 -0
  262. package/dist/learning/provider-fallback.d.ts +23 -1
  263. package/dist/learning/provider-fallback.d.ts.map +1 -1
  264. package/dist/learning/provider-fallback.js +24 -0
  265. package/dist/learning/provider-fallback.js.map +1 -1
  266. package/dist/learning/quota-ledger.d.ts +13 -0
  267. package/dist/learning/quota-ledger.d.ts.map +1 -1
  268. package/dist/learning/quota-ledger.js +28 -0
  269. package/dist/learning/quota-ledger.js.map +1 -1
  270. package/dist/learning/reasoning-trace.d.ts +65 -1
  271. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  272. package/dist/learning/reasoning-trace.js +123 -1
  273. package/dist/learning/reasoning-trace.js.map +1 -1
  274. package/dist/learning/requirement-probe.d.ts +55 -0
  275. package/dist/learning/requirement-probe.d.ts.map +1 -0
  276. package/dist/learning/requirement-probe.js +77 -0
  277. package/dist/learning/requirement-probe.js.map +1 -0
  278. package/dist/learning/resilient-call.d.ts +20 -1
  279. package/dist/learning/resilient-call.d.ts.map +1 -1
  280. package/dist/learning/resilient-call.js +42 -4
  281. package/dist/learning/resilient-call.js.map +1 -1
  282. package/dist/learning/response-usability.d.ts +85 -0
  283. package/dist/learning/response-usability.d.ts.map +1 -0
  284. package/dist/learning/response-usability.js +135 -0
  285. package/dist/learning/response-usability.js.map +1 -0
  286. package/dist/learning/router-bandit.d.ts +132 -0
  287. package/dist/learning/router-bandit.d.ts.map +1 -1
  288. package/dist/learning/router-bandit.js +253 -0
  289. package/dist/learning/router-bandit.js.map +1 -1
  290. package/dist/learning/routing-history.d.ts +41 -2
  291. package/dist/learning/routing-history.d.ts.map +1 -1
  292. package/dist/learning/routing-history.js.map +1 -1
  293. package/dist/learning/run-trace.d.ts +21 -0
  294. package/dist/learning/run-trace.d.ts.map +1 -1
  295. package/dist/learning/run-trace.js +38 -0
  296. package/dist/learning/run-trace.js.map +1 -1
  297. package/dist/learning/seeded-benchmark.d.ts +14 -0
  298. package/dist/learning/seeded-benchmark.d.ts.map +1 -1
  299. package/dist/learning/seeded-benchmark.js +43 -4
  300. package/dist/learning/seeded-benchmark.js.map +1 -1
  301. package/dist/learning/session-grant.d.ts +75 -0
  302. package/dist/learning/session-grant.d.ts.map +1 -0
  303. package/dist/learning/session-grant.js +112 -0
  304. package/dist/learning/session-grant.js.map +1 -0
  305. package/dist/learning/task-model-continuity.d.ts +73 -0
  306. package/dist/learning/task-model-continuity.d.ts.map +1 -0
  307. package/dist/learning/task-model-continuity.js +87 -0
  308. package/dist/learning/task-model-continuity.js.map +1 -0
  309. package/dist/learning/turn-feedback.d.ts +93 -0
  310. package/dist/learning/turn-feedback.d.ts.map +1 -0
  311. package/dist/learning/turn-feedback.js +127 -0
  312. package/dist/learning/turn-feedback.js.map +1 -0
  313. package/dist/learning/turn-report.d.ts +104 -2
  314. package/dist/learning/turn-report.d.ts.map +1 -1
  315. package/dist/learning/turn-report.js +161 -6
  316. package/dist/learning/turn-report.js.map +1 -1
  317. package/dist/learning/verdict-routing.d.ts +51 -0
  318. package/dist/learning/verdict-routing.d.ts.map +1 -0
  319. package/dist/learning/verdict-routing.js +122 -0
  320. package/dist/learning/verdict-routing.js.map +1 -0
  321. package/dist/mcp/client.d.ts.map +1 -1
  322. package/dist/mcp/client.js +5 -0
  323. package/dist/mcp/client.js.map +1 -1
  324. package/dist/mcp/manager.d.ts +10 -0
  325. package/dist/mcp/manager.d.ts.map +1 -1
  326. package/dist/mcp/manager.js +20 -0
  327. package/dist/mcp/manager.js.map +1 -1
  328. package/dist/mcp/mcp-discovery.d.ts +34 -0
  329. package/dist/mcp/mcp-discovery.d.ts.map +1 -0
  330. package/dist/mcp/mcp-discovery.js +71 -0
  331. package/dist/mcp/mcp-discovery.js.map +1 -0
  332. package/dist/mcp/mcp-schema-cache.d.ts +17 -0
  333. package/dist/mcp/mcp-schema-cache.d.ts.map +1 -1
  334. package/dist/mcp/mcp-schema-cache.js +47 -3
  335. package/dist/mcp/mcp-schema-cache.js.map +1 -1
  336. package/dist/mcp/types.d.ts +28 -0
  337. package/dist/mcp/types.d.ts.map +1 -1
  338. package/dist/mcp/types.js.map +1 -1
  339. package/dist/security/secret-scan.d.ts +156 -0
  340. package/dist/security/secret-scan.d.ts.map +1 -0
  341. package/dist/security/secret-scan.js +525 -0
  342. package/dist/security/secret-scan.js.map +1 -0
  343. package/dist/tools/ask-user.d.ts.map +1 -1
  344. package/dist/tools/ask-user.js +4 -0
  345. package/dist/tools/ask-user.js.map +1 -1
  346. package/dist/tools/capability-registry.d.ts +150 -0
  347. package/dist/tools/capability-registry.d.ts.map +1 -0
  348. package/dist/tools/capability-registry.js +400 -0
  349. package/dist/tools/capability-registry.js.map +1 -0
  350. package/dist/tools/child-agent-runtime.d.ts.map +1 -1
  351. package/dist/tools/child-agent-runtime.js +19 -5
  352. package/dist/tools/child-agent-runtime.js.map +1 -1
  353. package/dist/tools/coding-tools.d.ts +17 -5
  354. package/dist/tools/coding-tools.d.ts.map +1 -1
  355. package/dist/tools/coding-tools.js +80 -11
  356. package/dist/tools/coding-tools.js.map +1 -1
  357. package/dist/tools/command-memo.d.ts +116 -0
  358. package/dist/tools/command-memo.d.ts.map +1 -0
  359. package/dist/tools/command-memo.js +202 -0
  360. package/dist/tools/command-memo.js.map +1 -0
  361. package/dist/tools/git-tool.d.ts.map +1 -1
  362. package/dist/tools/git-tool.js +13 -3
  363. package/dist/tools/git-tool.js.map +1 -1
  364. package/dist/tools/harness-directive.d.ts +42 -0
  365. package/dist/tools/harness-directive.d.ts.map +1 -0
  366. package/dist/tools/harness-directive.js +45 -0
  367. package/dist/tools/harness-directive.js.map +1 -0
  368. package/dist/tools/loop-skill-hint.d.ts +74 -174
  369. package/dist/tools/loop-skill-hint.d.ts.map +1 -1
  370. package/dist/tools/loop-skill-hint.js +110 -536
  371. package/dist/tools/loop-skill-hint.js.map +1 -1
  372. package/dist/tools/mcp-client-tool.d.ts +2 -0
  373. package/dist/tools/mcp-client-tool.d.ts.map +1 -1
  374. package/dist/tools/mcp-client-tool.js +3 -0
  375. package/dist/tools/mcp-client-tool.js.map +1 -1
  376. package/dist/tools/modality/shared.d.ts +1 -4
  377. package/dist/tools/modality/shared.d.ts.map +1 -1
  378. package/dist/tools/modality/shared.js +7 -22
  379. package/dist/tools/modality/shared.js.map +1 -1
  380. package/dist/tools/plan-store.d.ts +44 -4
  381. package/dist/tools/plan-store.d.ts.map +1 -1
  382. package/dist/tools/plan-store.js +93 -9
  383. package/dist/tools/plan-store.js.map +1 -1
  384. package/dist/tools/registry.d.ts +76 -3
  385. package/dist/tools/registry.d.ts.map +1 -1
  386. package/dist/tools/registry.js +623 -15
  387. package/dist/tools/registry.js.map +1 -1
  388. package/dist/tools/remediation-ladder.d.ts +91 -0
  389. package/dist/tools/remediation-ladder.d.ts.map +1 -0
  390. package/dist/tools/remediation-ladder.js +316 -0
  391. package/dist/tools/remediation-ladder.js.map +1 -0
  392. package/dist/tools/run-cli.d.ts.map +1 -1
  393. package/dist/tools/run-cli.js +29 -4
  394. package/dist/tools/run-cli.js.map +1 -1
  395. package/dist/tools/run-terminal.d.ts +11 -0
  396. package/dist/tools/run-terminal.d.ts.map +1 -1
  397. package/dist/tools/run-terminal.js +289 -10
  398. package/dist/tools/run-terminal.js.map +1 -1
  399. package/dist/tools/skills-hub.d.ts +7 -0
  400. package/dist/tools/skills-hub.d.ts.map +1 -1
  401. package/dist/tools/skills-hub.js +11 -2
  402. package/dist/tools/skills-hub.js.map +1 -1
  403. package/dist/tools/step-artifact.d.ts +35 -0
  404. package/dist/tools/step-artifact.d.ts.map +1 -0
  405. package/dist/tools/step-artifact.js +154 -0
  406. package/dist/tools/step-artifact.js.map +1 -0
  407. package/dist/tools/tool-loop.d.ts +327 -13
  408. package/dist/tools/tool-loop.d.ts.map +1 -1
  409. package/dist/tools/tool-loop.js +891 -66
  410. package/dist/tools/tool-loop.js.map +1 -1
  411. package/dist/tools/toolsets.js +4 -4
  412. package/dist/tools/toolsets.js.map +1 -1
  413. package/dist/tools/worktree.d.ts +11 -2
  414. package/dist/tools/worktree.d.ts.map +1 -1
  415. package/dist/tools/worktree.js +11 -2
  416. package/dist/tools/worktree.js.map +1 -1
  417. package/dist/utils/atomic-store.d.ts +53 -0
  418. package/dist/utils/atomic-store.d.ts.map +1 -0
  419. package/dist/utils/atomic-store.js +178 -0
  420. package/dist/utils/atomic-store.js.map +1 -0
  421. package/dist/utils/binary-probe.d.ts +25 -0
  422. package/dist/utils/binary-probe.d.ts.map +1 -0
  423. package/dist/utils/binary-probe.js +48 -0
  424. package/dist/utils/binary-probe.js.map +1 -0
  425. package/dist/utils/workspace-path.d.ts +50 -0
  426. package/dist/utils/workspace-path.d.ts.map +1 -0
  427. package/dist/utils/workspace-path.js +86 -0
  428. package/dist/utils/workspace-path.js.map +1 -0
  429. package/dist/web-dashboard/chat-console.d.ts +24 -0
  430. package/dist/web-dashboard/chat-console.d.ts.map +1 -1
  431. package/dist/web-dashboard/chat-console.js +38 -0
  432. package/dist/web-dashboard/chat-console.js.map +1 -1
  433. package/dist/web-dashboard/project-context.d.ts +27 -2
  434. package/dist/web-dashboard/project-context.d.ts.map +1 -1
  435. package/dist/web-dashboard/project-context.js +32 -2
  436. package/dist/web-dashboard/project-context.js.map +1 -1
  437. package/dist/web-dashboard/server.d.ts +40 -0
  438. package/dist/web-dashboard/server.d.ts.map +1 -1
  439. package/dist/web-dashboard/server.js +733 -15
  440. package/dist/web-dashboard/server.js.map +1 -1
  441. package/dist/web-dashboard/src/types.d.ts +236 -1
  442. package/dist/web-dashboard/src/types.d.ts.map +1 -1
  443. package/dist/web-dashboard/workspace-guard.d.ts +18 -2
  444. package/dist/web-dashboard/workspace-guard.d.ts.map +1 -1
  445. package/dist/web-dashboard/workspace-guard.js +94 -12
  446. package/dist/web-dashboard/workspace-guard.js.map +1 -1
  447. package/dist/web-dashboard/workspace-resolution.d.ts +87 -0
  448. package/dist/web-dashboard/workspace-resolution.d.ts.map +1 -0
  449. package/dist/web-dashboard/workspace-resolution.js +105 -0
  450. package/dist/web-dashboard/workspace-resolution.js.map +1 -0
  451. package/package.json +1 -1
  452. package/src/web-dashboard/public/assets/index-tK8Y2qHB.css +1 -0
  453. package/src/web-dashboard/public/assets/index-zjbdJnO-.js +210 -0
  454. package/src/web-dashboard/public/assets/index-zjbdJnO-.js.map +1 -0
  455. package/src/web-dashboard/public/index.html +2 -2
  456. package/src/web-dashboard/public/assets/index-BrZcYhy6.js +0 -207
  457. package/src/web-dashboard/public/assets/index-BrZcYhy6.js.map +0 -1
  458. package/src/web-dashboard/public/assets/index-DsWczTa6.css +0 -1
@@ -27,8 +27,18 @@ import { envBuff } from '../config/paths.js';
27
27
  import { z, toJSONSchema } from 'zod';
28
28
  import { ACTION_BY_INTENT } from '../nlu/actions.js';
29
29
  import { detectPermissionSeeking, IRREVERSIBLE_ACTION_RE } from '../learning/autonomy-policy.js';
30
+ // The explicit, session-scoped "allow all <category> for this session" grant —
31
+ // offered at the confirmation a state-changing tool demanded (see the
32
+ // `ask_user` runner) and consulted by the write/terminal gates.
33
+ import { grantSession, sessionGrantLabel, sessionGrantNotice, } from '../learning/session-grant.js';
34
+ // Cluster G — a folder the user names in an ask_user reply IS the workspace
35
+ // (see the adoption block in the `ask_user` runner). The rule is the same,
36
+ // conservative one the dashboard server applies to a message.
37
+ import { directoryFromMessage } from '../utils/workspace-path.js';
30
38
  import { createFindingTool } from './finding-tool.js';
39
+ import { declareToolCapability } from './capability-registry.js';
31
40
  import { recordArtifact } from './artifact-append.js';
41
+ import { checkStepArtifacts } from './step-artifact.js';
32
42
  // ─── Tool definitions ───────────────────────────────────────────────────────
33
43
  // ─── Shared auto-delivery helper ─────────────────────────────────────────
34
44
  // Tools that produce artifact files (generate_image, speak, video_generate)
@@ -93,6 +103,19 @@ const askUserSchema = z.object({
93
103
  .describe('2–4 answer choices the user can pick from'),
94
104
  multi_select: z.boolean().default(false).describe('Allow multiple selections (checkbox)'),
95
105
  });
106
+ /**
107
+ * The extra choice a TOOL-DEMANDED confirmation ask offers: allow this whole
108
+ * category of state-changing action for the rest of the session. Deliberately
109
+ * one fixed label, so the grant is a choice the user picks — never a phrase
110
+ * the harness tries to recognise in free text.
111
+ */
112
+ const SESSION_GRANT_CHOICE_LABEL = 'Allow this for the whole session';
113
+ /** Did the user's answer (single-select or multi-select) name the grant choice? */
114
+ function answerIsSessionGrant(answer) {
115
+ return Array.isArray(answer)
116
+ ? answer.includes(SESSION_GRANT_CHOICE_LABEL)
117
+ : answer === SESSION_GRANT_CHOICE_LABEL;
118
+ }
96
119
  /** E3c — publish tool args: bump type + safety flags (irreversible action). */
97
120
  export const publishToolSchema = z.object({
98
121
  goal: z.string().optional().describe('Optional release goal (e.g. "Release v1.2.0")'),
@@ -306,8 +329,12 @@ const editFileSchema = z.object({
306
329
  });
307
330
  /** P0.3 — write_file tool args: create/overwrite a file (confirmed). */
308
331
  const writeFileSchema = z.object({
309
- path: z.string().describe('Path to write, relative to the workspace root (parent directories are created as needed). Overwrites existing content.'),
310
- content: z.string().describe('The FULL new file content (replaces any existing content).'),
332
+ path: z.string().describe('Path to write, relative to the workspace root (parent directories are created as needed). Overwrites existing content unless mode is "append".'),
333
+ content: z.string().describe('The new file content. With mode "overwrite" (default) this REPLACES the file; with mode "append" it is added to the end. Keep any SINGLE call small enough to emit (well under ~150 lines): a payload too large for one model output arrives empty and cannot be sent at all.'),
334
+ mode: z
335
+ .enum(['overwrite', 'append'])
336
+ .default('overwrite')
337
+ .describe('Use "append" to build a large document or file in SECTIONS across several calls: the first call creates it (mode "overwrite" or "append" — same thing when the file does not exist yet), then every further section appends. This is the supported way to deliver something bigger than one model output; a single oversized call cannot be delivered.'),
311
338
  confirm: z.boolean().default(false).describe('Set true ONLY after the user explicitly confirmed this write via ask_user. CREATING a new file the request asked for needs no confirmation — it is applied directly. Refused without it only when it would REPLACE content that already exists.'),
312
339
  });
313
340
  /** P0.4 — run_terminal tool args: verify commands + gated shell execution. */
@@ -315,6 +342,7 @@ const runTerminalSchema = z.object({
315
342
  command: z.string().min(1).describe('The shell command to run in the workspace — e.g. "npx vitest run tests/foo.test.ts", "npx tsc --noEmit", "npm run build", "git diff --stat". Read-only verify commands run directly. Recoverable workspace commands (dependency installs, mkdir/touch/cp/mv, git add, npm init) also run directly when the request authorized the work; every other state-changing command needs confirm:true after the user approves via ask_user. Destructive/system commands are denied outright.'),
316
343
  confirm: z.boolean().default(false).describe('Set true ONLY after the user explicitly confirmed a command you initiated. Required for state-changing commands outside the recoverable workspace set (network fetches, arbitrary code, global installs, anything that leaves the machine).'),
317
344
  timeout_ms: z.number().int().min(1000).max(300000).optional().describe('Timeout in ms (default 120000).'),
345
+ verb: z.string().optional().describe('What this command is FOR (e.g. "install java"). When it SUCCEEDS, it is remembered for this verb on this machine, so the next tool_search resolve can offer it instead of re-deriving it. Optional.'),
318
346
  });
319
347
  /** The C2 requirementState check as a reusable tool. */
320
348
  const verifyRequirementSchema = z.object({
@@ -347,6 +375,11 @@ const registry = new Map();
347
375
  */
348
376
  export function registerTool(tool) {
349
377
  registry.set(tool.name, tool);
378
+ // Record the tool's OWN capability declaration so the name-only lookups
379
+ // (`effectClassOfTool`, `grantCategoryOfTool`) and the descriptor answer from
380
+ // it instead of a hand-maintained map. A tool that declares nothing is left
381
+ // out — absence IS the conservative default.
382
+ declareToolCapability(tool.name, tool.capability);
350
383
  }
351
384
  /** Look up a tool by name. */
352
385
  export function getTool(name) {
@@ -434,6 +467,7 @@ registerTool({
434
467
  // same pipeline core + the publish workflow, so any ask auto-runs.
435
468
  registerTool({
436
469
  name: 'document',
470
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
437
471
  description: 'Write documentation for the project (README, API docs, guides) through the agent pipeline',
438
472
  category: 'pipeline',
439
473
  inputSchema: taskGoalArgs,
@@ -442,6 +476,7 @@ registerTool({
442
476
  });
443
477
  registerTool({
444
478
  name: 'website',
479
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
445
480
  description: 'Create or build a website / web app end-to-end through the agent pipeline',
446
481
  category: 'pipeline',
447
482
  inputSchema: taskGoalArgs,
@@ -450,6 +485,7 @@ registerTool({
450
485
  });
451
486
  registerTool({
452
487
  name: 'analyze',
488
+ capability: { effectClass: 'read' },
453
489
  description: 'Analyze the project (state, architecture, comparison against another project) and produce findings',
454
490
  category: 'pipeline',
455
491
  inputSchema: taskGoalArgs,
@@ -466,6 +502,7 @@ registerTool({
466
502
  });
467
503
  registerTool({
468
504
  name: 'publish',
505
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external', requires: { credentials: [{ anyOf: ['NPM_TOKEN', 'GITHUB_TOKEN'], note: 'an npm automation token, or a GitHub token for a GitHub release' }] } },
469
506
  description: 'Publish a release to npm/GitHub (version bump, changelog, build, publish). IRREVERSIBLE — confirm bump type and target with ask_user first.',
470
507
  category: 'workflow',
471
508
  inputSchema: publishToolSchema,
@@ -484,6 +521,7 @@ registerTool({
484
521
  });
485
522
  registerTool({
486
523
  name: 'ask_user',
524
+ capability: { effectClass: 'read' },
487
525
  description: 'Ask the user a clarifying question with 2–4 choices (multi-select optional). Every surface also offers the user a free-text "Other" answer, so a reply may be their own words instead of one of your choices. Use when a request is ambiguous or missing information.',
488
526
  category: 'experience',
489
527
  inputSchema: askUserSchema,
@@ -537,6 +575,14 @@ registerTool({
537
575
  // question reaches the user once.
538
576
  const pending = ctx.pendingConfirmation;
539
577
  if (!pending &&
578
+ // Cluster G — an UNSCOPED turn is asking for a required INPUT, not for
579
+ // permission, and the two can look identical to this heuristic ("which
580
+ // folder should I create the app in?" reads as permission-seeking).
581
+ // Suppressing it deadlocks the turn: the user is never asked, so no
582
+ // folder is ever named, so every retry fails for the same reason — the
583
+ // "keeps refusing" loop. A missing workspace is not a decision the model
584
+ // may take on the user's behalf.
585
+ !ctx.workspaceUnscoped &&
540
586
  (ctx.writesAuthorized?.authorized === true || Boolean(ctx.envelope)) &&
541
587
  detectPermissionSeeking(question) &&
542
588
  !IRREVERSIBLE_ACTION_RE.test(question)) {
@@ -564,18 +610,131 @@ registerTool({
564
610
  // One confirmation refusal buys exactly ONE ask — clear it before rendering.
565
611
  if (pending)
566
612
  ctx.pendingConfirmation = undefined;
613
+ // ── Session-grant offer (Copilot-style "allow all for this session") ─────
614
+ // Only when the ask is the CONFIRMATION a state-changing tool demanded
615
+ // (`pending` is set) is there a category to offer. It is ONE extra choice,
616
+ // so the user's explicit go-ahead is RECEIVED, never inferred — an answer
617
+ // that does not name it leaves the ask-every-time default untouched, and an
618
+ // unattended default (returned below) is an assumption, not a grant.
619
+ const grantCategory = pending
620
+ ? (pending.category ??
621
+ (pending.tool === 'run_terminal'
622
+ ? 'terminal'
623
+ : pending.tool === 'run_cli'
624
+ ? null // run_cli states its own category; absent = a grant cannot help
625
+ : 'write'))
626
+ : null;
627
+ const offeredChoices = grantCategory && choices.length < 4
628
+ ? [
629
+ ...choices,
630
+ {
631
+ label: SESSION_GRANT_CHOICE_LABEL,
632
+ description: `Allow all ${sessionGrantLabel(grantCategory)} for this session — no more asks until the conversation ends.`,
633
+ },
634
+ ]
635
+ : choices;
636
+ // Deferred so the registry stays import-light — the writing half of the
637
+ // decision log is only needed when an ask actually reaches a person.
638
+ const { recordDecision, decisionsRecordingEnabled } = await import('../learning/decision-log.js');
567
639
  const render = ctx.askUser || (await import('./ask-user.js')).renderAskUser;
568
- const answer = await render(question, choices, multi_select);
640
+ const answer = await render(question, offeredChoices, multi_select);
569
641
  const picked = Array.isArray(answer.answer) ? answer.answer.join(', ') : answer.answer;
642
+ // E1 — an UNATTENDED answer is an ASSUMPTION, not the user's word. Recorded
643
+ // as one (never as a shown ask with an answer, which is what made a silent
644
+ // default indistinguishable from a real reply), emitted so the turn can
645
+ // disclose it, and worded to the model as an assumption so its answer cannot
646
+ // claim the user chose it.
647
+ if (answer.unattended) {
648
+ ctx.runTrace?.recordAssumption(question, picked);
649
+ ctx.emit?.('autonomy:assumed-default', { question, assumed: picked }, 'tool-loop');
650
+ return (`No user was reachable to answer, so "${picked}" was ASSUMED — it is NOT their answer. ` +
651
+ (answer.custom ??
652
+ `In your written answer, tell the user the question you would have asked and the assumption ` +
653
+ `you made (e.g. "I assumed ${picked} — tell me if you would prefer something else"), then continue with it.`) +
654
+ ' Do not record it as a user decision or re-ask the same question.');
655
+ }
656
+ // ── Grant it, if the user picked the session choice ──────────────────────
657
+ // The grant is stored against the session (the same key the intent envelope
658
+ // uses), so it outlives this turn and dies with the conversation. The write
659
+ // happens HERE; the RETURN that tells the model to finish the refused call
660
+ // is issued after the decision is recorded, so the grant itself is a
661
+ // recorded, revisable decision like any other must-ask.
662
+ const granted = Boolean(grantCategory) && answerIsSessionGrant(answer.answer);
663
+ const grant = granted && grantCategory
664
+ ? grantSession(ctx.planStore, [grantCategory], 'the user allowed it for the session')
665
+ : null;
666
+ if (granted && grantCategory) {
667
+ ctx.emit?.('autonomy:session-grant', { category: grantCategory, question }, 'tool-loop');
668
+ }
570
669
  // Recorded SHOWN and with the answer, so the trace can hand both back when
571
670
  // the agent tries to ask it a second time (and so a self-report can say what
572
671
  // the user actually replied, instead of "asked 4×, no answer recorded").
573
672
  ctx.runTrace?.recordAsk(question, true, picked);
673
+ // Bundle 36 — a MUST-ASK decision is also a PROJECT artifact: recorded so the
674
+ // user can go back through it later and revise it (see
675
+ // `learning/decision-log.ts`). Only a genuine, SHOWN ask reaches here (an
676
+ // unattended default is an assumption and returned above), and the write is
677
+ // best-effort — recording a decision must never break the turn. Deferred
678
+ // import to keep the registry import-light.
679
+ if (decisionsRecordingEnabled() && ctx.cwd) {
680
+ try {
681
+ const answerText = answer.custom
682
+ ? picked
683
+ ? `${picked} (${answer.custom})`
684
+ : answer.custom
685
+ : picked;
686
+ recordDecision({
687
+ question,
688
+ answer: answerText,
689
+ choices: offeredChoices.map((c) => c.label),
690
+ source: 'ask_user',
691
+ dir: ctx.cwd,
692
+ });
693
+ }
694
+ catch {
695
+ // Best-effort — never break a turn on a decision-log write.
696
+ }
697
+ }
698
+ // The grant was a real decision — recorded above — so now hand the model the
699
+ // instruction that turns it into work: finish the ONE refused call, then stop
700
+ // asking about this category.
701
+ if (granted && grantCategory) {
702
+ return (`User allowed all ${sessionGrantLabel(grantCategory)} for this session. ` +
703
+ `Retry the refused ${grantCategory === 'terminal' ? 'run_terminal command' : 'write'} ONCE with confirm:true to complete it. ` +
704
+ 'Every later action in this category proceeds without asking — do not ask about it again. ' +
705
+ (grant ? sessionGrantNotice(grant) : '') +
706
+ 'Irreversible and off-machine actions still ask, and the permission ends with the conversation.');
707
+ }
708
+ // ── Cluster G: the reply may be the FOLDER, and then it is the workspace ──
709
+ // An unscoped turn asks "which folder should I use?". The user answers by
710
+ // typing one (the free-text "Other" field, or a label they edited) — and
711
+ // until now that answer went no further than the model's context: the write
712
+ // tool would refuse again for the same reason and the user would be asked
713
+ // the same question a second time. That repeat is the reported "agent keep
714
+ // refusing even after i attach the folder".
715
+ //
716
+ // So a directory in the answer IS adopted: `ctx.cwd` moves to it and the
717
+ // unscoped flag clears, exactly as `clone_repo` moves `ctx.cwd` after it
718
+ // clones. `directoryFromMessage` is the same conservative rule the server
719
+ // applies to a message (absolute, existing, a DIRECTORY), so a reply that
720
+ // merely mentions a file path cannot relocate the turn.
721
+ const namedFolder = ctx.workspaceUnscoped
722
+ ? directoryFromMessage(`${picked}\n${answer.custom ?? ''}`)
723
+ : undefined;
724
+ if (namedFolder) {
725
+ ctx.cwd = namedFolder;
726
+ ctx.workspaceUnscoped = false;
727
+ ctx.emit?.('workspace:adopted', { path: namedFolder, source: 'ask_user' }, 'tool-loop');
728
+ return (`User answered: ${picked}${answer.custom ? ` (custom: ${answer.custom})` : ''}\n` +
729
+ `Workspace adopted: '${namedFolder}'. This chat now works INSIDE that folder — retry the ` +
730
+ 'write with a path inside it (the previous refusal no longer applies).');
731
+ }
574
732
  return `User answered: ${picked}${answer.custom ? ` (custom: ${answer.custom})` : ''}`;
575
733
  },
576
734
  });
577
735
  registerTool({
578
736
  name: 'suggest_followups',
737
+ capability: { effectClass: 'read' },
579
738
  description: 'Suggest clickable followup prompts the user can click to send. Use this tool AFTER completing the task — call it last, after your written answer (never before or instead of it). Aim for ~3 suggestions; skip only when there is no sensible next step (e.g. the user said goodbye).',
580
739
  category: 'experience',
581
740
  inputSchema: suggestFollowupsSchema,
@@ -594,6 +753,7 @@ registerTool({
594
753
  // LLM needed — runs entirely on file system access.
595
754
  registerTool({
596
755
  name: 'code_search',
756
+ capability: { effectClass: 'read' },
597
757
  description: 'Search the project for a regex/literal pattern (ripgrep-fast), returning matching file:line:column entries. Use for context gathering: finding definitions, usages, and where things live.',
598
758
  category: 'workflow',
599
759
  inputSchema: codeSearchSchema,
@@ -626,6 +786,7 @@ registerTool({
626
786
  // Backed by `src/tools/coding-tools.ts` (pure fs — no new dependencies).
627
787
  registerTool({
628
788
  name: 'read_file',
789
+ capability: { effectClass: 'read' },
629
790
  description: 'Read files with line numbers. Pass `path` for one file (offset/limit for large files) OR `paths` to read several in one call — batching is one step instead of many. Each file gets its own line count and a continuation offset when truncated. Use to open the actual contents of files code_search or glob located — never guess what a file contains.',
630
791
  category: 'workflow',
631
792
  inputSchema: readFileSchema,
@@ -634,6 +795,7 @@ registerTool({
634
795
  });
635
796
  registerTool({
636
797
  name: 'list_dir',
798
+ capability: { effectClass: 'read' },
637
799
  description: 'List a directory in the workspace (subdirectories first, sorted). Use to explore project structure — what is in this folder, where does this component live.',
638
800
  category: 'workflow',
639
801
  inputSchema: listDirSchema,
@@ -642,6 +804,7 @@ registerTool({
642
804
  });
643
805
  registerTool({
644
806
  name: 'glob',
807
+ capability: { effectClass: 'read' },
645
808
  description: 'Find files by glob pattern relative to the workspace (e.g. "src/**\/*.ts", "tests/*.test.ts"). Use to locate files by shape when code_search content search does not fit.',
646
809
  category: 'workflow',
647
810
  inputSchema: globSchema,
@@ -650,6 +813,7 @@ registerTool({
650
813
  });
651
814
  registerTool({
652
815
  name: 'edit_file',
816
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
653
817
  description: 'Surgical exact-text edit to a file (find old_string, replace with new_string — like str_replace). Make ONE call for ALL edits to a file via `replacements[]` (they apply atomically — all-or-nothing, never a partial edit). Optionally `dry_run` to preview a unified diff without writing. State-changing: confirm with the user via ask_user first, then retry with confirm:true. Use after read_file so the match is exact.',
654
818
  category: 'workflow',
655
819
  inputSchema: editFileSchema,
@@ -658,7 +822,8 @@ registerTool({
658
822
  });
659
823
  registerTool({
660
824
  name: 'write_file',
661
- description: 'Create a new file or overwrite one with full content (parent directories are created). State-changing: confirm with the user via ask_user first, then retry with confirm:true.',
825
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a create is undone by deleting the file; an overwrite is a git revert" },
826
+ description: 'Create a new file, overwrite one, or APPEND a section to one (parent directories are created). Use mode "append" to deliver a large document or file in sections across several calls — a payload too large for one model output cannot be sent at all, so split it. State-changing: confirm with the user via ask_user first, then retry with confirm:true.',
662
827
  category: 'workflow',
663
828
  inputSchema: writeFileSchema,
664
829
  endsAgentStep: false,
@@ -666,6 +831,7 @@ registerTool({
666
831
  });
667
832
  registerTool({
668
833
  name: 'run_terminal',
834
+ capability: { effectClass: 'local-state', grantCategory: 'terminal' },
669
835
  description: 'Run a shell command in the workspace and see its REAL output — typecheck, a single test file, a build, git diff/status. Use to verify code by actual invocation: run, read the failure, edit, re-run. Verify-class commands (tests/typecheck/build/git-readonly) run directly; state-changing commands need confirm:true after the user approves via ask_user. Destructive/system commands (sudo, git push, rm -rf at dangerous targets, ...) are denied outright.',
670
836
  category: 'workflow',
671
837
  inputSchema: runTerminalSchema,
@@ -689,14 +855,51 @@ registerTool({
689
855
  const { action, goal, steps, id, status, note } = planTodoSchema.parse(args);
690
856
  const { defaultPlanStore } = await import('./plan-store.js');
691
857
  const store = ctx.planStore ?? defaultPlanStore();
858
+ // C4 — a re-declaration of the SAME goal carries the progress of the steps it
859
+ // re-declares (see PlanStore.create). Disclosed rather than silent: a model
860
+ // that re-declared the plan to RESET it must be told that the harness kept a
861
+ // step's status, or it will keep working around a step it believes is pending.
862
+ let carriedNote = '';
692
863
  if (action === 'create') {
693
864
  if (!goal || !steps || steps.length === 0) {
694
865
  return 'Error: plan_todo create needs goal + steps (id + description each).';
695
866
  }
867
+ const before = store.snapshot?.() ?? null;
696
868
  store.create(goal, steps);
869
+ const after = store.snapshot?.() ?? null;
870
+ const carried = (after?.steps ?? []).filter((s) => s.status !== 'pending');
871
+ if (before?.goal === after?.goal && carried.length > 0) {
872
+ carriedNote =
873
+ `\n♻️ Carried ${carried.length} step(s) already progressed from this goal's previous plan: ` +
874
+ `${carried.map((s) => `${s.id} (${s.status})`).join(', ')}. ` +
875
+ `Leave them as they are, or call update with status "pending" if you really mean to redo them.`;
876
+ }
697
877
  }
698
878
  else if (id && status) {
699
- store.update(id, status, note);
879
+ // A1 — never accept an update that matched nothing. Silently returning
880
+ // the unchanged plan reads as success to the model (and as tracking to
881
+ // the user) while the counter stays frozen; observed live on the `cal`
882
+ // Android run, where the checklist sat at `1/7` across four successful
883
+ // steps. Resolve the reference first and, when it names no step, say so
884
+ // with the ids that DO exist so the model can re-send it and move on.
885
+ const resolved = store.resolveStepId ? store.resolveStepId(id) : id;
886
+ if (!resolved) {
887
+ const valid = (store.snapshot?.()?.steps ?? []).map((s) => s.id);
888
+ return `Error: plan_todo found no step "${id}" — valid step ids: ${valid.length > 0 ? valid.join(', ') : '(none — declare the plan with action "create" first)'}. Re-send this update with one of those ids.`;
889
+ }
890
+ // A6 — a step that NAMES an artifact is done only once that artifact
891
+ // exists. The plan can otherwise advance past undelivered work (the `cal`
892
+ // run kept going after its APK step never produced an APK). Checked
893
+ // against the workspace; a step with no file-like token is unaffected.
894
+ if (status === 'done' && ctx.cwd) {
895
+ const step = store.snapshot?.()?.steps.find((s) => s.id === resolved);
896
+ const { missing } = checkStepArtifacts(step?.description ?? '', ctx.cwd);
897
+ if (missing.length > 0) {
898
+ return (`Error: step "${resolved}" names artifact(s) that do not exist yet: ${missing.join(', ')}. ` +
899
+ `Create them in ${ctx.cwd}, then mark the step done — a step is not done while its artifact is missing.`);
900
+ }
901
+ }
902
+ store.update(resolved, status, note);
700
903
  }
701
904
  else {
702
905
  return 'Error: plan_todo update needs id + status (pending|running|done|blocked).';
@@ -709,7 +912,7 @@ registerTool({
709
912
  ctx.emit?.('plan:changed', snapshot);
710
913
  }
711
914
  }
712
- return store.toText ? store.toText() : `Plan updated (${action}).`;
915
+ return (store.toText ? store.toText() : `Plan updated (${action}).`) + carriedNote;
713
916
  },
714
917
  });
715
918
  // ─── WS1 (#23) finding tool — a claim, and the verdict the gate computed ────
@@ -729,6 +932,7 @@ registerTool(createFindingTool());
729
932
  // `src/tools/web-research.ts` (mocked-fetch tests, no network).
730
933
  registerTool({
731
934
  name: 'web_search',
935
+ capability: { effectClass: 'read' },
732
936
  description: 'Search the web for a query, returning title/url/snippet hits. Uses the first available backend: a configured Brave/Serper/Tavily/Google-CSE key, a self-hosted SearXNG, or the keyless DuckDuckGo fallback. Set the backend with `tools.toolsets.web.provider` or an API-key env var. Use to ground answers in current information.',
733
937
  category: 'workflow',
734
938
  inputSchema: webSearchSchema,
@@ -751,6 +955,7 @@ registerTool({
751
955
  });
752
956
  registerTool({
753
957
  name: 'read_page',
958
+ capability: { effectClass: 'read' },
754
959
  description: 'Fetch a URL and return its readable text (Jina Reader free tier when JINA_API_KEY is set, else a plain fetch). Use to read a full page found by web_search.',
755
960
  category: 'workflow',
756
961
  inputSchema: readPageSchema,
@@ -768,6 +973,7 @@ registerTool({
768
973
  // ─── I2–I5 modality packs (browser / image / voice / vision) ────────────────
769
974
  registerTool({
770
975
  name: 'browser',
976
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
771
977
  description: 'Automate a real browser (open/click/type/extract/screenshot). Requires the optional playwright package (npm i playwright && npx playwright install chromium). Use for tasks that need a live page (login flows, scraping dynamic sites).',
772
978
  category: 'workflow',
773
979
  inputSchema: browserSchema,
@@ -785,6 +991,7 @@ registerTool({
785
991
  });
786
992
  registerTool({
787
993
  name: 'generate_image',
994
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
788
995
  description: 'Generate an image from a text prompt using a bring-your-own-key backend: Google Gemini/Imagen (GEMINI_API_KEY), OpenAI DALL-E/gpt-image (OPENAI_API_KEY), Stability AI (STABILITY_API_KEY), a local ComfyUI/Stable Diffusion (BUFF_IMAGE_API_URL), or the free keyless Pollinations.ai fallback. The first backend whose key is configured wins (override with `modality.image.provider`), and a failing backend falls back to Pollinations. The image is saved to the sandbox images/ dir and the path is returned. When running in a gateway context (WhatsApp/Telegram/etc.), the image is automatically sent back to the originating channel.',
789
996
  category: 'workflow',
790
997
  inputSchema: imageGenSchema,
@@ -821,6 +1028,7 @@ registerTool({
821
1028
  });
822
1029
  registerTool({
823
1030
  name: 'speak',
1031
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
824
1032
  description: 'Synthesize speech for text to an audio file (edge-tts or Piper, both free and local). Requires edge-tts or piper on PATH. Returns the audio file path. When running in a gateway context, the audio is automatically sent back to the originating channel.',
825
1033
  category: 'workflow',
826
1034
  inputSchema: speakSchema,
@@ -844,6 +1052,7 @@ registerTool({
844
1052
  });
845
1053
  registerTool({
846
1054
  name: 'transcribe',
1055
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
847
1056
  description: 'Transcribe an audio file to text (whisper.cpp or faster-whisper, local and free). Requires whisper on PATH. Returns the transcript.',
848
1057
  category: 'workflow',
849
1058
  inputSchema: transcribeSchema,
@@ -857,6 +1066,7 @@ registerTool({
857
1066
  });
858
1067
  registerTool({
859
1068
  name: 'describe_image',
1069
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
860
1070
  description: 'Describe an image (local Ollama llava/llama3.2-vision, or free Gemini vision when BUFF_GEMINI_API_KEY is set). Use for screenshots, diagrams, or any image the user references.',
861
1071
  category: 'workflow',
862
1072
  inputSchema: describeImageSchema,
@@ -873,6 +1083,7 @@ registerTool({
873
1083
  });
874
1084
  registerTool({
875
1085
  name: 'gateway_send',
1086
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
876
1087
  description: 'Send a message (text or image) to a DIFFERENT channel or contact through the gateway (WhatsApp by contact name or number, Telegram, Slack, Discord, email, or any registered alias). Do NOT use this tool to reply to the CURRENT conversation — your text response is automatically sent back to the originating channel. Only call this when the user asks you to deliver a result to SOMEONE ELSE — e.g. "send the poem to Alex on whatsapp" (while you are chatting with Divya). For images, set image_path to the file path (e.g. from generate_image output) — the image is sent with text as caption. AUTHORIZATION: on a gateway turn the sender must hold outbound send authority for that platform (dashboard → Agent Hub → Permissions → Send authority); the result string tells you plainly when a send was refused and why — report that to the user honestly and NEVER claim a message was delivered unless this tool returned its \'✅ sent\' result.',
877
1088
  category: 'workflow',
878
1089
  inputSchema: gatewaySendSchema,
@@ -881,6 +1092,7 @@ registerTool({
881
1092
  });
882
1093
  registerTool({
883
1094
  name: 'run_cli',
1095
+ capability: { effectClass: 'local-state', grantCategory: 'terminal' },
884
1096
  description: 'Resolve a plain-English request into the exact nuvira CLI command and execute it (start/stop the dashboard or gateway, check status, add a verified sender or send-by-name contact, configure a platform like telegram/whatsapp, run evals, show stats, manage memory/cache, etc.). Use when the user describes a system/tooling task in plain English instead of typing the command — e.g. "stop the dashboard", "add Rahul to whatsapp", "enable telegram support", "run the eval suite". Ambiguous asks and destructive actions are gated: the tool returns what to confirm, then call ask_user and retry with confirm:true when the user agreed.',
885
1097
  category: 'workflow',
886
1098
  inputSchema: runCliSchema,
@@ -889,6 +1101,7 @@ registerTool({
889
1101
  });
890
1102
  registerTool({
891
1103
  name: 'verify_requirement',
1104
+ capability: { effectClass: 'read' },
892
1105
  description: 'Verify whether a user request is complete enough to act on (requirementState: complete vs needs-clarification), returning the missing information.',
893
1106
  category: 'experience',
894
1107
  inputSchema: verifyRequirementSchema,
@@ -921,6 +1134,7 @@ registerTool({
921
1134
  // exec (no shell) make injection structurally impossible.
922
1135
  registerTool({
923
1136
  name: 'clone_repo',
1137
+ capability: { effectClass: 'external', grantCategory: 'external', reversibleHow: 'delete the cloned directory' },
924
1138
  description: 'Clone a git repository (depth-1 shallow) into an ephemeral cache and scope the conversation to it — read_file / list_dir / glob / code_search / run_terminal then operate on the CLONE, so you can assess another project without touching the user\'s workspace. Use when the ask is about a repo that is not the attached project.',
925
1139
  category: 'workflow',
926
1140
  inputSchema: cloneRepoSchema,
@@ -936,6 +1150,7 @@ registerTool({
936
1150
  // while reset --hard/clean remain structurally unexpressible AND deny-guarded.
937
1151
  registerTool({
938
1152
  name: 'git',
1153
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external', reversibleHow: 'a local commit is reversible; a push is only as reversible as the remote' },
939
1154
  description: 'Structured git operations: status/log (read-only), diff (unified output + a diff card in the GUI), commit (GATED — confirm:true only after the user approved via ask_user; optional files = the accepted subset to stage+commit), and push (GATED — sends a branch to a remote; runs when the user’s own request named the push, otherwise ask via ask_user first). Use for the whole flow: diff → ask_user (accept/reject files) → commit → push.',
940
1155
  category: 'workflow',
941
1156
  inputSchema: gitToolSchema,
@@ -1107,6 +1322,7 @@ registerTool({
1107
1322
  // Browser Supervisor tool
1108
1323
  registerTool({
1109
1324
  name: 'browser_supervisor',
1325
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
1110
1326
  description: 'Browser CDP supervisor: monitor health, detect dialogs, track frames.',
1111
1327
  category: 'workflow',
1112
1328
  inputSchema: z.object({
@@ -1128,6 +1344,7 @@ registerTool({
1128
1344
  // Browser Dialog tool
1129
1345
  registerTool({
1130
1346
  name: 'browser_dialog',
1347
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
1131
1348
  description: 'Handle native browser dialogs: accept, dismiss, or enter values.',
1132
1349
  category: 'workflow',
1133
1350
  inputSchema: z.object({
@@ -1155,6 +1372,7 @@ registerTool({
1155
1372
  // Camofox tool
1156
1373
  registerTool({
1157
1374
  name: 'camofox',
1375
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
1158
1376
  description: 'Camofox anti-detection browser: open pages, screenshots, interact via a11y refs.',
1159
1377
  category: 'workflow',
1160
1378
  inputSchema: z.object({
@@ -1182,6 +1400,7 @@ registerTool({
1182
1400
  // Docker tool
1183
1401
  registerTool({
1184
1402
  name: 'docker',
1403
+ capability: { effectClass: 'local-state', grantCategory: 'terminal' },
1185
1404
  description: 'Docker management: containers, images, compose, volumes, networks.',
1186
1405
  category: 'workflow',
1187
1406
  inputSchema: z.object({
@@ -1296,6 +1515,7 @@ registerTool({
1296
1515
  // Kanban tool
1297
1516
  registerTool({
1298
1517
  name: 'kanban',
1518
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
1299
1519
  description: 'Kanban board: create boards, manage cards, track priorities.',
1300
1520
  category: 'workflow',
1301
1521
  inputSchema: z.object({
@@ -1414,6 +1634,7 @@ registerTool({
1414
1634
  // Blueprint tool
1415
1635
  registerTool({
1416
1636
  name: 'blueprint',
1637
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
1417
1638
  description: 'Project blueprints: create project templates with file structure.',
1418
1639
  category: 'workflow',
1419
1640
  inputSchema: z.object({
@@ -1435,6 +1656,7 @@ registerTool({
1435
1656
  // File ops tool
1436
1657
  registerTool({
1437
1658
  name: 'file_ops',
1659
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
1438
1660
  description: 'Advanced file operations: state tracking, safe writes, extraction, preview.',
1439
1661
  category: 'workflow',
1440
1662
  inputSchema: z.object({
@@ -1645,6 +1867,7 @@ registerTool({
1645
1867
  // tool-loop.ts trusts). See TOOL_TRUTHFULNESS_TRACKER.md findings #3 and #9.
1646
1868
  registerTool({
1647
1869
  name: 'messaging',
1870
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
1648
1871
  description: 'NOT CONNECTED — this tool has no delivery adapter and every send/react refuses with code "not_configured". '
1649
1872
  + 'Do NOT use it to deliver anything and never report a delivery from it. Use gateway_send instead: it sends '
1650
1873
  + 'through the configured gateway (WhatsApp by contact name or number, Telegram, Slack, Discord, email) and '
@@ -1676,6 +1899,7 @@ registerTool({
1676
1899
  // Vision tool — comprehensive image analysis, OCR, UI element detection
1677
1900
  registerTool({
1678
1901
  name: 'vision',
1902
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
1679
1903
  description: 'Analyze an image and transcribe its text (OCR) through the configured vision backend (a local Ollama '
1680
1904
  + 'vision model or a Gemini key); `analyze` also reports real pixel dimensions. detect-elements, '
1681
1905
  + 'find-element and compare have NO backend and return ok:false with code "not_configured" — an empty '
@@ -1757,6 +1981,7 @@ registerTool({
1757
1981
  // Process registry tool
1758
1982
  registerTool({
1759
1983
  name: 'process_registry',
1984
+ capability: { effectClass: 'read' },
1760
1985
  description: 'Track all running processes with metadata and cleanup.',
1761
1986
  category: 'workflow',
1762
1987
  inputSchema: z.object({
@@ -1782,6 +2007,7 @@ registerTool({
1782
2007
  // Code execution tool
1783
2008
  registerTool({
1784
2009
  name: 'code_execution',
2010
+ capability: { effectClass: 'local-state', grantCategory: 'terminal' },
1785
2011
  description: 'Execute code in sandboxed environment with timeout and resource limits.',
1786
2012
  category: 'workflow',
1787
2013
  inputSchema: z.object({
@@ -1839,6 +2065,7 @@ registerTool({
1839
2065
  // Threat patterns tool
1840
2066
  registerTool({
1841
2067
  name: 'threat_patterns',
2068
+ capability: { effectClass: 'read' },
1842
2069
  description: 'Detect threat patterns: shell execution, external HTTP, obfuscation.',
1843
2070
  category: 'workflow',
1844
2071
  inputSchema: z.object({
@@ -1853,6 +2080,7 @@ registerTool({
1853
2080
  // URL safety tool
1854
2081
  registerTool({
1855
2082
  name: 'url_safety',
2083
+ capability: { effectClass: 'read' },
1856
2084
  description: 'Check URL safety: suspicious TLDs, patterns, length.',
1857
2085
  category: 'workflow',
1858
2086
  inputSchema: z.object({
@@ -1867,6 +2095,7 @@ registerTool({
1867
2095
  // Path security tool
1868
2096
  registerTool({
1869
2097
  name: 'path_security',
2098
+ capability: { effectClass: 'read' },
1870
2099
  description: 'Check path security: traversal, null bytes, directory escape.',
1871
2100
  category: 'workflow',
1872
2101
  inputSchema: z.object({
@@ -1882,6 +2111,7 @@ registerTool({
1882
2111
  // Security score tool
1883
2112
  registerTool({
1884
2113
  name: 'security_score',
2114
+ capability: { effectClass: 'read' },
1885
2115
  description: 'Calculate security score for a file: code injection, secrets, threats.',
1886
2116
  category: 'workflow',
1887
2117
  inputSchema: z.object({
@@ -1893,6 +2123,54 @@ registerTool({
1893
2123
  return JSON.stringify(m.getSecurityScorer().score(filePath));
1894
2124
  }),
1895
2125
  });
2126
+ // Secret scan tool — LOCAL, dependency-free, on-demand.
2127
+ //
2128
+ // `security_score`/`ast_audit` scan ONE named file and report a score. This is
2129
+ // the other question an agent that writes and commits code actually has to
2130
+ // answer before it commits: "is anything secret-shaped anywhere in here?". It
2131
+ // walks the tree with bounded coverage, needs no `gitleaks`/`ggshield` binary
2132
+ // and no network, and reports masked values only — see src/security/secret-scan.ts.
2133
+ registerTool({
2134
+ name: 'secret_scan',
2135
+ capability: { effectClass: 'read' },
2136
+ description: 'Scan the workspace for secret-shaped strings (API keys, tokens, private keys, credentials in URLs) before committing or publishing. Fully local — no network and no external scanner binary — and every reported value is masked. Returns findings with path:line and severity, plus a summary. Set history:true to scan git HISTORY as well (every committed version of every file) — a key deleted in a later commit is STILL committed and must be rotated. A clean result means "nothing matched a known shape", NOT "no secrets": report it as a lint, never as a guarantee.',
2137
+ category: 'workflow',
2138
+ inputSchema: z.object({
2139
+ path: z.string().optional().describe('Directory to scan (relative to the workspace, or absolute). Default: the workspace root.'),
2140
+ subdir: z.string().optional().describe('Narrow the scan to one subtree, e.g. "src" or "docs".'),
2141
+ maxFiles: z.number().optional().describe('Cap on files visited (default 5000) — a huge repo does not hang the turn.'),
2142
+ history: z.boolean().optional().describe('Also scan git history (reachable blobs) for secrets ever committed. Default false. Use before publishing or when auditing an existing repo.'),
2143
+ maxBlobs: z.number().optional().describe('With history:true, cap on distinct historical blobs read (default 2000).'),
2144
+ }),
2145
+ endsAgentStep: false,
2146
+ run: async (args, ctx) => {
2147
+ const { join } = await import('node:path');
2148
+ const m = await import('../security/secret-scan.js');
2149
+ const { path: target, subdir, maxFiles, history, maxBlobs } = args;
2150
+ const cwd = ctx?.cwd ?? process.cwd();
2151
+ const root = target
2152
+ ? (target.startsWith('/') || /^[A-Za-z]:[\\/]/.test(target) ? target : join(cwd, target))
2153
+ : cwd;
2154
+ if (history) {
2155
+ // History only: the tree scan runs first (fast, no git), then history is
2156
+ // appended as its own section so a reader can tell the two apart.
2157
+ const tree = m.scanDirectory(root, {
2158
+ ...(subdir ? { subdir } : {}),
2159
+ ...(maxFiles ? { maxFiles } : {}),
2160
+ });
2161
+ const hist = m.scanGitHistory(root, {
2162
+ ...(subdir ? { subdir } : {}),
2163
+ ...(maxBlobs ? { maxBlobs } : {}),
2164
+ });
2165
+ return [m.formatSecretScan(tree), '', '── git history ──', m.formatSecretHistoryScan(hist)].join('\n');
2166
+ }
2167
+ const result = m.scanDirectory(root, {
2168
+ ...(subdir ? { subdir } : {}),
2169
+ ...(maxFiles ? { maxFiles } : {}),
2170
+ });
2171
+ return m.formatSecretScan(result);
2172
+ },
2173
+ });
1896
2174
  // ─── Batch 5: Infrastructure ──────────────────────────────────────────────
1897
2175
  // Tool search tool — the tiered-exposure discovery surface. Two actions:
1898
2176
  // - search (default): fuzzy-find tools by query (the pre-existing behavior).
@@ -1904,21 +2182,28 @@ registerTool({
1904
2182
  // ~85% of per-turn schema tokens saved, zero capability lost.
1905
2183
  registerTool({
1906
2184
  name: 'tool_search',
1907
- description: 'Discover and load tools. Actions: "search" fuzzy-finds tools by query; ' +
2185
+ capability: { effectClass: 'read' },
2186
+ description: 'Discover and load capabilities. Actions: "search" finds what the agent can DO by query and returns each hit as a CAPABILITY — its effect (read / local-write / local-state / external / destructive), whether it is reversible and how, any credentials or binaries it needs, whether a session grant can cover it, and a `check` saying whether those needs are actually MET on this machine (run this FIRST for an install / publish / deploy / push task, so a missing executable is known before the work starts rather than halfway through); ' +
2187
+ '"readiness" is the same probe without a search — call it with a query to pre-flight a described task, or with no query to see what the curated install / publish / deploy / push verbs are missing here; ' +
2188
+ 'Tools on any MCP server connected this session are included as capabilities too (kind "mcp", with an `invoke` saying how to call them). Installed SKILLS are capabilities too (kind "skill") — call this to find the skill that fits a task, then load it with the skill tool; ' +
1908
2189
  '"load" activates a whole toolset (media, browser, channels, docker, ' +
1909
2190
  'productivity, publish, core-pipeline, …) for THIS turn — call it before ' +
1910
2191
  'using any tool outside the always-available core set. "load" returns ' +
1911
- 'the toolset\'s tool names, which become callable immediately.',
2192
+ 'the toolset\'s tool names, which become callable immediately; ' +
2193
+ '"resolve" checks a command or a verb against THIS machine (is the executable present, which package managers exist here, and any command already learned for it) — run it BEFORE an OS-specific command instead of guessing the platform; "record" stores a command you derived so it is known next time (the table grows by use); "list-commands" and "forget" manage that store.',
1912
2194
  category: 'workflow',
1913
2195
  inputSchema: z.object({
1914
- action: z.enum(['search', 'load']).optional().describe('Defaults to "search".'),
1915
- query: z.string().optional().describe('Search query (action=search)'),
2196
+ action: z.enum(['search', 'load', 'readiness', 'resolve', 'record', 'forget', 'list-commands']).optional().describe('Defaults to "search".'),
2197
+ query: z.string().optional().describe('Search query (action=search|readiness; omit for readiness to check the install/publish/deploy/push verbs)'),
1916
2198
  limit: z.number().optional().describe('Max results (action=search)'),
1917
2199
  toolset: z.string().optional().describe('Toolset name to load (action=load) — e.g. "media", "browser", "channels"'),
2200
+ command: z.string().optional().describe('A command to check against this machine (action=resolve) or to store (action=record)'),
2201
+ verb: z.string().optional().describe('What the command is FOR (action=resolve|record|forget) — e.g. "install java", or a capability ref'),
2202
+ note: z.string().optional().describe('Optional caveat to store beside a learned command (action=record)'),
1918
2203
  }),
1919
2204
  endsAgentStep: false,
1920
2205
  run: async (args, ctx) => {
1921
- const { action = 'search', query, limit, toolset } = args;
2206
+ const { action = 'search', query, limit, toolset, command, verb, note } = args;
1922
2207
  // ── load: activate a toolset for this turn (tiered exposure) ──
1923
2208
  if (action === 'load') {
1924
2209
  if (!toolset) {
@@ -1961,13 +2246,181 @@ registerTool({
1961
2246
  : 'Toolset recorded, but this context has no live schema loader (loop not in tiered mode) — tools remain governed by the standard enablement gate.',
1962
2247
  });
1963
2248
  }
1964
- // ── search: the pre-existing fuzzy find (unchanged behavior) ──
2249
+ // ── resolve / record / forget / list-commands: the long-tail OS command
2250
+ // loop. The MODEL derives a command; the harness checks it against the
2251
+ // machine and remembers it. No enumeration, no phrase list — presence is
2252
+ // probed (binaryOnPath) and the learned store is keyed by (verb, os).
2253
+ if (action === 'resolve' || action === 'record' || action === 'forget' || action === 'list-commands') {
2254
+ const { detectMachineFacts } = await import('../learning/machine-facts.js');
2255
+ const facts = detectMachineFacts();
2256
+ const learnedMod = await import('../learning/learned-commands.js');
2257
+ const { leadingBinary, suggestInstallCommand } = await import('../learning/command-adaptation.js');
2258
+ const { binaryOnPath } = await import('../utils/binary-probe.js');
2259
+ if (action === 'list-commands') {
2260
+ return JSON.stringify({ os: facts.os, commands: learnedMod.listLearnedCommands() });
2261
+ }
2262
+ if (action === 'forget') {
2263
+ return JSON.stringify({ os: facts.os, verb: verb ?? '', forgotten: learnedMod.forgetLearnedCommand(verb ?? '') });
2264
+ }
2265
+ if (action === 'record') {
2266
+ const bin = command ? leadingBinary(command) : null;
2267
+ const entry = learnedMod.recordLearnedCommand({
2268
+ verb: verb ?? '',
2269
+ command: command ?? '',
2270
+ ...(bin ? { binary: bin } : {}),
2271
+ ...(note ? { note } : {}),
2272
+ });
2273
+ return JSON.stringify({
2274
+ os: facts.os,
2275
+ recorded: entry,
2276
+ ...(entry
2277
+ ? { note: 'Recorded. It will be offered the next time this verb is resolved on this OS.' }
2278
+ : { error: 'record needs a verb and a command.' }),
2279
+ });
2280
+ }
2281
+ // resolve
2282
+ const bin = command ? leadingBinary(command) : null;
2283
+ const binPresent = bin ? binaryOnPath(bin) : null;
2284
+ const learned = verb ? learnedMod.learnedCommandFor(verb) : null;
2285
+ // When the executable is missing, prefer a manager that is ACTUALLY PRESENT
2286
+ // here — the suggestion is buildable, not a manager the machine lacks.
2287
+ const suggestedInstall = bin && binPresent === false ? suggestInstallCommand(facts, bin) : null;
2288
+ return JSON.stringify({
2289
+ os: facts.os,
2290
+ osName: facts.osName,
2291
+ arch: facts.arch,
2292
+ shell: facts.shellName,
2293
+ packageManagers: facts.packageManagers,
2294
+ ...(command ? { proposed: { command, binary: bin, present: binPresent } } : {}),
2295
+ ...(suggestedInstall
2296
+ ? {
2297
+ suggestedInstall: {
2298
+ manager: suggestedInstall.manager,
2299
+ command: suggestedInstall.command,
2300
+ note: 'The package name can differ from the executable name — adjust if the install fails.',
2301
+ },
2302
+ }
2303
+ : {}),
2304
+ ...(learned
2305
+ ? { learned: { command: learned.command, binary: learned.binary, source: learned.source, learnedAt: learned.learnedAt } }
2306
+ : {}),
2307
+ advice: command
2308
+ ? binPresent
2309
+ ? `\`${bin}\` is on this machine — the command can run.`
2310
+ : bin
2311
+ ? `\`${bin}\` is NOT on this machine.` +
2312
+ (suggestedInstall
2313
+ ? ` Install it with \`${suggestedInstall.command}\` (${suggestedInstall.manager} is present here), or choose another present manager: ${facts.packageManagers.join(', ')}.`
2314
+ : ` No package manager that can install it is present; use what is on the machine.`)
2315
+ : 'Could not identify an executable in this command; check it with a small probe run.'
2316
+ : learned
2317
+ ? `A command for "${verb}" on this OS was already derived — use it, or record a better one.`
2318
+ : `No command is known for "${verb}" on this OS yet. Derive one from the machine facts, run it, then record it.`,
2319
+ });
2320
+ }
2321
+ // ── search: fuzzy find, ENRICHED with capability descriptors ──
2322
+ // The tools returned are the same as before; each is also returned as a
2323
+ // CAPABILITY (effect class + reversibility + requirements + grant), and the
2324
+ // curated high-level ACTIONS (install / publish / deploy / push …) ride
2325
+ // alongside, so the model can discover what it can do — not just which tool
2326
+ // names exist. Discovery only: nothing here decides what runs.
1965
2327
  const m = await import('./infra-tools.js');
1966
2328
  const engine = m.getToolSearchEngine();
1967
- // Index from registry
1968
2329
  const { listTools } = await import('./registry.js');
1969
- engine.index(listTools().map((t) => ({ name: t.name, description: t.description })));
1970
- return JSON.stringify(engine.search(query || '', limit || 10));
2330
+ const tools = listTools();
2331
+ engine.index(tools.map((t) => ({ name: t.name, description: t.description })));
2332
+ const { capabilityIndex, searchCapabilities } = await import('./capability-registry.js');
2333
+ const index = await capabilityIndex(tools.map((t) => ({ name: t.name, description: t.description, category: t.category })));
2334
+ const { describeGap, probeRequirements } = await import('../learning/requirement-probe.js');
2335
+ // A command the MODEL derived for THIS OS (learned) rides on the hit, so the
2336
+ // per-OS table grows by use rather than by hand.
2337
+ const { detectMachineFacts: detectFacts } = await import('../learning/machine-facts.js');
2338
+ const { learnedCommandFor } = await import('../learning/learned-commands.js');
2339
+ const currentOs = detectFacts().os;
2340
+ const hits = searchCapabilities(index, query || '', limit || 10);
2341
+ // PRE-FLIGHT. Each hit's declared requirements are probed HERE, at discovery
2342
+ // time, so a missing executable is known BEFORE the run starts rather than
2343
+ // after it has written half the work. A binary gap is a hard blocker; a
2344
+ // credential gap is advisory and never blocks — see requirement-probe.ts.
2345
+ const toCapability = (h) => {
2346
+ const requires = h.capability.requires;
2347
+ const hasRequires = Object.keys(requires).length > 0;
2348
+ const readiness = hasRequires ? probeRequirements(requires) : undefined;
2349
+ const learnedOnThisOs = learnedCommandFor(h.capability.ref, currentOs);
2350
+ return {
2351
+ id: h.capability.id,
2352
+ kind: h.capability.kind,
2353
+ ref: h.capability.ref,
2354
+ name: h.capability.name,
2355
+ does: h.capability.oneLiner,
2356
+ effect: h.capability.effectClass,
2357
+ reversible: h.capability.reversible,
2358
+ ...(h.capability.reversibleHow ? { undo: h.capability.reversibleHow } : {}),
2359
+ ...(h.capability.grantCategory ? { grantable: h.capability.grantCategory } : {}),
2360
+ ...(hasRequires ? { requires } : {}),
2361
+ // A foreign (MCP) hit is not called by its own name — say how to reach it.
2362
+ ...(h.capability.invoke ? { invoke: h.capability.invoke } : {}),
2363
+ // HOW TO DO IT HERE vs. EVERYWHERE. `onThisMachine` is the entry for the
2364
+ // OS the agent is actually running on, resolved from the same per-OS map the
2365
+ // rest is declared in, so the model never has to infer which package manager
2366
+ // exists from the platform name. `platforms` carries the others, present only
2367
+ // where the OS genuinely determines the command.
2368
+ ...(h.capability.platforms
2369
+ ? {
2370
+ onThisMachine: h.capability.platforms[process.platform],
2371
+ platforms: h.capability.platforms,
2372
+ }
2373
+ : {}),
2374
+ // A command the MODEL derived for this OS rides ALONGSIDE the declared
2375
+ // one: the table grows by use, and the model can see both and choose.
2376
+ ...(learnedOnThisOs
2377
+ ? {
2378
+ learnedOnThisMachine: {
2379
+ command: learnedOnThisOs.command,
2380
+ ...(learnedOnThisOs.binary ? { binary: learnedOnThisOs.binary } : {}),
2381
+ source: learnedOnThisOs.source,
2382
+ },
2383
+ }
2384
+ : {}),
2385
+ ...(readiness
2386
+ ? {
2387
+ check: {
2388
+ ready: readiness.ready,
2389
+ ...(readiness.gaps.length > 0 ? { gaps: readiness.gaps.map(describeGap) } : {}),
2390
+ ...(readiness.ask.length > 0 ? { ask: readiness.ask } : {}),
2391
+ },
2392
+ }
2393
+ : {}),
2394
+ matched: h.matched,
2395
+ };
2396
+ };
2397
+ // ── readiness: the same hits, reported as a PRE-FLIGHT rather than a find ──
2398
+ if (action === 'readiness') {
2399
+ // With no query, check the curated ACTION verbs — the capabilities that
2400
+ // declare requirements (install / publish / deploy / push / credentials).
2401
+ // With no query, check the curated verbs that declare needs. A read-only
2402
+ // action with no requirements (this-machine facts) is not a pre-flight
2403
+ // subject, so it is excluded rather than reported with an empty check.
2404
+ const selected = query
2405
+ ? hits
2406
+ : index
2407
+ .filter((c) => c.kind === 'action' && Object.keys(c.requires).length > 0)
2408
+ .map((c) => ({ capability: c, matched: [] }));
2409
+ const caps = selected.map(toCapability);
2410
+ const blocked = caps.filter((c) => c.check && !c.check.ready);
2411
+ return JSON.stringify({
2412
+ checked: caps.length,
2413
+ blocked: blocked.length,
2414
+ summary: blocked.length === 0
2415
+ ? 'No missing executables among these capabilities. Any credential notes are advisory — a credential may live in the vault rather than the environment.'
2416
+ : `${blocked.length} of ${caps.length} need an executable that is not on PATH; see each check.gaps.`,
2417
+ capabilities: caps,
2418
+ });
2419
+ }
2420
+ return JSON.stringify({
2421
+ tools: engine.search(query || '', limit || 10),
2422
+ capabilities: hits.map(toCapability),
2423
+ });
1971
2424
  },
1972
2425
  });
1973
2426
  // Budget config tool
@@ -1996,6 +2449,7 @@ registerTool({
1996
2449
  // Fuzzy match tool
1997
2450
  registerTool({
1998
2451
  name: 'fuzzy_match',
2452
+ capability: { effectClass: 'read' },
1999
2453
  description: 'Fuzzy string matching with Levenshtein distance.',
2000
2454
  category: 'workflow',
2001
2455
  inputSchema: z.object({
@@ -2029,6 +2483,7 @@ registerTool({
2029
2483
  // OSV check tool
2030
2484
  registerTool({
2031
2485
  name: 'osv_check',
2486
+ capability: { effectClass: 'read' },
2032
2487
  description: 'Check for known vulnerabilities in npm packages via OSV API.',
2033
2488
  category: 'workflow',
2034
2489
  inputSchema: z.object({
@@ -2058,6 +2513,7 @@ registerTool({
2058
2513
  // Image source tool
2059
2514
  registerTool({
2060
2515
  name: 'image_source',
2516
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2061
2517
  description: 'Detect image source: format, size, is screenshot.',
2062
2518
  category: 'workflow',
2063
2519
  inputSchema: z.object({
@@ -2073,6 +2529,7 @@ registerTool({
2073
2529
  // Discord tool
2074
2530
  registerTool({
2075
2531
  name: 'discord',
2532
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2076
2533
  description: 'Discord server management: list servers, channels, members, messages, roles. Send messages.',
2077
2534
  category: 'workflow',
2078
2535
  inputSchema: z.object({
@@ -2100,6 +2557,7 @@ registerTool({
2100
2557
  // Home Assistant tool
2101
2558
  registerTool({
2102
2559
  name: 'homeassistant',
2560
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2103
2561
  description: 'Home Assistant smart home: list entities, get state, call services (turn_on/off, set_temperature).',
2104
2562
  category: 'workflow',
2105
2563
  inputSchema: z.object({
@@ -2128,6 +2586,7 @@ registerTool({
2128
2586
  // Microsoft Graph tool
2129
2587
  registerTool({
2130
2588
  name: 'microsoft_graph',
2589
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2131
2590
  description: 'Microsoft Graph API: email, calendar, OneDrive, contacts.',
2132
2591
  category: 'workflow',
2133
2592
  inputSchema: z.object({
@@ -2178,6 +2637,7 @@ registerTool({
2178
2637
  // Feishu document tool
2179
2638
  registerTool({
2180
2639
  name: 'feishu_doc',
2640
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2181
2641
  description: 'Feishu document management: create, read, update, delete documents.',
2182
2642
  category: 'workflow',
2183
2643
  inputSchema: z.object({
@@ -2202,6 +2662,7 @@ registerTool({
2202
2662
  // Feishu drive tool
2203
2663
  registerTool({
2204
2664
  name: 'feishu_drive',
2665
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2205
2666
  description: 'Feishu drive management: list files, create folders, move, copy, search.',
2206
2667
  category: 'workflow',
2207
2668
  inputSchema: z.object({
@@ -2231,6 +2692,7 @@ registerTool({
2231
2692
  // Video generation tool
2232
2693
  registerTool({
2233
2694
  name: 'video_generate',
2695
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2234
2696
  description: 'Generate videos from text prompts or images. Supports FAL/BFL providers. When running in a gateway context and a video URL is returned, the video is automatically downloaded and sent back to the originating channel.',
2235
2697
  category: 'workflow',
2236
2698
  inputSchema: z.object({
@@ -2339,6 +2801,7 @@ registerTool({
2339
2801
  // NeuTTS synthesis tool
2340
2802
  registerTool({
2341
2803
  name: 'neutts_synth',
2804
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2342
2805
  description: 'High-quality text-to-speech synthesis via NeuTTS.',
2343
2806
  category: 'workflow',
2344
2807
  inputSchema: z.object({
@@ -2362,6 +2825,7 @@ registerTool({
2362
2825
  // Terminal tool — multi-environment execution
2363
2826
  registerTool({
2364
2827
  name: 'terminal',
2828
+ capability: { effectClass: 'local-state', grantCategory: 'terminal' },
2365
2829
  description: 'Execute commands in local, Docker, Modal, SSH environments with background support.',
2366
2830
  category: 'workflow',
2367
2831
  inputSchema: z.object({
@@ -2416,6 +2880,7 @@ registerTool({
2416
2880
  // Send message tool — cross-channel messaging
2417
2881
  registerTool({
2418
2882
  name: 'send_message',
2883
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2419
2884
  description: 'Send messages to Telegram, Discord, Slack, WhatsApp, Email. Supports media attachments (images, videos, audio, documents) on WhatsApp.',
2420
2885
  category: 'workflow',
2421
2886
  inputSchema: z.object({
@@ -2677,6 +3142,7 @@ registerTool({
2677
3142
  // MCP Tool — Dynamic MCP server connection and tool invocation
2678
3143
  registerTool({
2679
3144
  name: 'mcp_tool',
3145
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2680
3146
  description: 'Connect to external MCP servers and invoke their tools — stdio, HTTP, or SSE transport.',
2681
3147
  category: 'workflow',
2682
3148
  inputSchema: z.object({
@@ -2704,6 +3170,7 @@ registerTool({
2704
3170
  // Computer Use — Desktop automation via cua-driver
2705
3171
  registerTool({
2706
3172
  name: 'computer_use',
3173
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2707
3174
  description: 'Desktop control via cua-driver — screenshots, mouse, keyboard, scroll without stealing focus.',
2708
3175
  category: 'workflow',
2709
3176
  inputSchema: z.object({
@@ -2792,6 +3259,7 @@ registerTool({
2792
3259
  // OpenRouter Client — Multi-LLM routing
2793
3260
  registerTool({
2794
3261
  name: 'openrouter_client',
3262
+ capability: { effectClass: 'external', grantCategory: 'external' },
2795
3263
  description: 'Route LLM calls to multiple providers via OpenRouter — cost optimization and fallback.',
2796
3264
  category: 'workflow',
2797
3265
  inputSchema: z.object({
@@ -2816,6 +3284,7 @@ registerTool({
2816
3284
  // TTS Streaming — Provider-agnostic streaming TTS
2817
3285
  registerTool({
2818
3286
  name: 'tts_streaming',
3287
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2819
3288
  description: 'Convert text to speech using various providers — streaming and batch modes.',
2820
3289
  category: 'workflow',
2821
3290
  inputSchema: z.object({
@@ -2894,6 +3363,7 @@ registerTool({
2894
3363
  // that cannot, so the model can distinguish "read it" from "could not read it".
2895
3364
  registerTool({
2896
3365
  name: 'read_extract',
3366
+ capability: { effectClass: 'read' },
2897
3367
  description: 'Extract text from a document. WORKS for PDF, DOCX, XLSX, PPTX, HTML, CSV/TSV, JSON, XML, YAML and '
2898
3368
  + 'plain text/Markdown; PDF and spreadsheet table rows are preserved so values stay paired with their '
2899
3369
  + 'columns. Returns success:false with a typed code when it cannot read the file — "no_data" means a '
@@ -2914,6 +3384,141 @@ registerTool({
2914
3384
  return mgr.extract(filePath, { ocr: Boolean(ocr) }).then((r) => JSON.stringify(r));
2915
3385
  }),
2916
3386
  });
3387
+ // Knowledge — tag-scoped, vectorized retrieval over the user's own documents
3388
+ //
3389
+ // WHY A TOOL. The user's flow is "bring documents, tag them, ask questions scoped
3390
+ // to that tag." Retrieval must be something the AGENT can drive mid-task without
3391
+ // a CLI round-trip, and it must pay the read+embed cost ONCE per document rather
3392
+ // than re-reading the file into context every turn. This tool is that surface:
3393
+ // `add` ingests under a tag (documents are extracted, chunked, embedded once),
3394
+ // `query` returns the tag's top-k chunks with their source files so the model can
3395
+ // answer the data half from the user's data and the generic half from its own
3396
+ // knowledge / web-research.
3397
+ registerTool({
3398
+ name: 'knowledge',
3399
+ description: 'Work with the USER\'S OWN tagged documents. `add` ingests files/folders under a tag (extract → '
3400
+ + 'chunk → embed, done once); `query` retrieves the tag\'s most relevant passages, labeled with their '
3401
+ + 'source file and section, so the data part of an answer comes from the user\'s data and the general '
3402
+ + 'part from model knowledge or web-research. `toc` lists a tag\'s documents and their headings; `read` '
3403
+ + 'returns a whole document or ONE named section VERBATIM — use it when you have to implement or quote '
3404
+ + 'what a document says, rather than summarizing it from similar passages. `sync` brings a tag up to '
3405
+ + 'date with its folders (unchanged files are not re-embedded; files that vanished stop being served). '
3406
+ + 'Actions: add | query | toc | read | sync | list | forget | stats. Tags are normalized '
3407
+ + '(spaces/capitals/underscores become lowercase-hyphen), e.g. "Dheeraj health report" → '
3408
+ + '"dheeraj-health-report".',
3409
+ category: 'workflow',
3410
+ inputSchema: z.object({
3411
+ action: z.enum(['add', 'query', 'toc', 'read', 'sync', 'list', 'forget', 'stats']).describe('Action'),
3412
+ tag: z.string().optional().describe('Tag/hashtag scoping the data (e.g. "dheeraj-health-report")'),
3413
+ paths: z.array(z.string()).optional().describe('Files or folders to ingest (action: add | sync)'),
3414
+ question: z.string().optional().describe('Question to answer from the tagged data (action: query)'),
3415
+ doc: z.string().optional().describe('Document to read or list: doc id, full path, or basename (action: read | toc)'),
3416
+ section: z.string().optional().describe('Section heading to read verbatim (action: read) — omit for the whole document'),
3417
+ topK: z.number().optional().describe('Chunks to retrieve (action: query, default 6)'),
3418
+ }),
3419
+ endsAgentStep: false,
3420
+ run: (args) => import('../learning/knowledge-base.js').then(async (m) => {
3421
+ const { action, tag, paths, question, doc, section, topK } = args;
3422
+ switch (action) {
3423
+ case 'add': {
3424
+ if (!tag)
3425
+ return 'tag is required for action: add';
3426
+ if (!paths || paths.length === 0)
3427
+ return 'paths is required for action: add';
3428
+ const result = await m.ingestKnowledge(tag, paths);
3429
+ const skipped = result.skipped.length
3430
+ ? `\nSkipped: ${result.skipped.map((s) => `${s.path} (${s.reason})`).join('; ')}`
3431
+ : '';
3432
+ // Unchanged documents are named rather than silently folded in: "0 chunks"
3433
+ // and "nothing needed re-embedding" are different answers, and only one
3434
+ // of them means the ingest did nothing.
3435
+ const unchanged = result.unchanged > 0 ? `, ${result.unchanged} unchanged (not re-embedded)` : '';
3436
+ return `✅ Ingested ${result.files} document(s) / ${result.chunks} chunk(s)${unchanged} under tag '${result.tag}'.`
3437
+ + `\nQuery it with action: query, tag: '${result.tag}'.${skipped}`;
3438
+ }
3439
+ case 'query': {
3440
+ if (!tag)
3441
+ return 'tag is required for action: query';
3442
+ if (!question)
3443
+ return 'question is required for action: query';
3444
+ const hits = await m.queryKnowledge(tag, question, { topK });
3445
+ return m.formatKnowledgeContext(tag, hits);
3446
+ }
3447
+ case 'toc': {
3448
+ if (!tag)
3449
+ return 'tag is required for action: toc';
3450
+ const docs = m.listKnowledgeSections(tag, doc);
3451
+ if (docs.length === 0) {
3452
+ return doc
3453
+ ? `No document '${doc}' under tag '${tag}'.`
3454
+ : `No documents under tag '${tag}' — ingest some with action: add.`;
3455
+ }
3456
+ return docs
3457
+ .map((d) => {
3458
+ const head = `📄 ${d.title} (${d.chunks} chunk(s)) — ${d.path}`;
3459
+ const sections = d.sections.length
3460
+ ? d.sections
3461
+ .map((s) => `\n ${' '.repeat(Math.max(0, s.level - 1))}§ ${s.headingPath ? `${s.headingPath} > ` : ''}${s.title}`)
3462
+ .join('')
3463
+ : '\n (no headings — read the whole document)';
3464
+ return head + sections;
3465
+ })
3466
+ .join('\n');
3467
+ }
3468
+ case 'read': {
3469
+ if (!tag)
3470
+ return 'tag is required for action: read';
3471
+ if (!doc)
3472
+ return 'doc is required for action: read (a doc id, full path, or basename — see action: toc)';
3473
+ const read = m.readKnowledgeDocument(tag, doc, section);
3474
+ if (!read.ok)
3475
+ return read.reason;
3476
+ const header = `📖 ${read.path}${read.headingPath ? ` — §${read.headingPath}` : ''}`;
3477
+ const note = read.truncated ? `\n\n[... truncated at ${m.MAX_READ_CHARS} characters ...]` : '';
3478
+ return `${header}\n\n${read.text}${note}`;
3479
+ }
3480
+ case 'sync': {
3481
+ if (!tag)
3482
+ return 'tag is required for action: sync';
3483
+ if (!paths || paths.length === 0)
3484
+ return 'paths is required for action: sync';
3485
+ const synced = await m.syncKnowledgeTag(tag, paths, { onChunkSkipped: () => { } });
3486
+ const removed = synced.removed.length
3487
+ ? `\nRemoved (no longer present): ${synced.removed.map((p) => `${p}`).join('; ')}`
3488
+ : '';
3489
+ const skipped = synced.skipped.length
3490
+ ? `\nSkipped: ${synced.skipped.map((s) => `${s.path} (${s.reason})`).join('; ')}`
3491
+ : '';
3492
+ return `🔄 Synced '${synced.tag}': ${synced.added} added, ${synced.changed} changed, `
3493
+ + `${synced.unchanged} unchanged, ${synced.chunks} chunk(s) written.${removed}${skipped}`;
3494
+ }
3495
+ case 'list': {
3496
+ const tags = m.listKnowledgeTags();
3497
+ if (tags.length === 0)
3498
+ return 'No knowledge tags yet. Ingest documents with action: add.';
3499
+ return tags
3500
+ .map((t) => ` • ${t.tag} — ${t.chunkCount} chunk(s) across ${t.documents.length} document(s)`)
3501
+ .join('\n');
3502
+ }
3503
+ case 'forget': {
3504
+ if (!tag)
3505
+ return 'tag is required for action: forget';
3506
+ const removed = await m.forgetKnowledgeTag(tag);
3507
+ return removed ? `🗑️ Removed knowledge tag '${m.normalizeKnowledgeTag(tag)}'.` : `No knowledge tag '${tag}' found.`;
3508
+ }
3509
+ case 'stats': {
3510
+ if (!tag)
3511
+ return 'tag is required for action: stats';
3512
+ const entry = m.getKnowledgeTag(tag);
3513
+ if (!entry)
3514
+ return `No knowledge tag '${tag}' found.`;
3515
+ return JSON.stringify(entry);
3516
+ }
3517
+ default:
3518
+ return `Unknown action '${action}'`;
3519
+ }
3520
+ }),
3521
+ });
2917
3522
  // Credential Files — Credential file management
2918
3523
  registerTool({
2919
3524
  name: 'credential_files',
@@ -2964,6 +3569,7 @@ registerTool({
2964
3569
  });
2965
3570
  registerTool({
2966
3571
  name: 'search_memory',
3572
+ capability: { effectClass: 'read' },
2967
3573
  description: 'Search memories by content, type, or tags. Searches BOTH memory stores: entries added with add_memory (keyword/tag) and facts learned from earlier sessions (semantic), so a memory recorded by another path is still found.',
2968
3574
  category: 'workflow',
2969
3575
  inputSchema: z.object({
@@ -3025,6 +3631,7 @@ registerTool({
3025
3631
  });
3026
3632
  registerTool({
3027
3633
  name: 'list_memories',
3634
+ capability: { effectClass: 'read' },
3028
3635
  description: 'List all memory entries with optional filters.',
3029
3636
  category: 'workflow',
3030
3637
  inputSchema: z.object({
@@ -3041,6 +3648,7 @@ registerTool({
3041
3648
  });
3042
3649
  registerTool({
3043
3650
  name: 'memory_stats',
3651
+ capability: { effectClass: 'read' },
3044
3652
  description: 'Get memory statistics.',
3045
3653
  category: 'workflow',
3046
3654
  inputSchema: z.object({}),