agent-nuvira 3.3.13 → 3.3.15

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 (398) 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/orchestrator.d.ts +31 -0
  6. package/dist/agents/orchestrator.d.ts.map +1 -1
  7. package/dist/agents/orchestrator.js +74 -126
  8. package/dist/agents/orchestrator.js.map +1 -1
  9. package/dist/cli/chat.d.ts +89 -0
  10. package/dist/cli/chat.d.ts.map +1 -1
  11. package/dist/cli/chat.js +765 -33
  12. package/dist/cli/chat.js.map +1 -1
  13. package/dist/cli/cli-program.d.ts.map +1 -1
  14. package/dist/cli/cli-program.js +12 -0
  15. package/dist/cli/cli-program.js.map +1 -1
  16. package/dist/cli/config.d.ts +12 -0
  17. package/dist/cli/config.d.ts.map +1 -1
  18. package/dist/cli/config.js +86 -0
  19. package/dist/cli/config.js.map +1 -1
  20. package/dist/cli/decisions.d.ts +28 -0
  21. package/dist/cli/decisions.d.ts.map +1 -0
  22. package/dist/cli/decisions.js +140 -0
  23. package/dist/cli/decisions.js.map +1 -0
  24. package/dist/cli/doctor.d.ts +6 -0
  25. package/dist/cli/doctor.d.ts.map +1 -1
  26. package/dist/cli/doctor.js +46 -0
  27. package/dist/cli/doctor.js.map +1 -1
  28. package/dist/cli/failover-runner.d.ts.map +1 -1
  29. package/dist/cli/failover-runner.js +19 -1
  30. package/dist/cli/failover-runner.js.map +1 -1
  31. package/dist/cli/feedback.d.ts.map +1 -1
  32. package/dist/cli/feedback.js +5 -1
  33. package/dist/cli/feedback.js.map +1 -1
  34. package/dist/cli/hooks.d.ts +30 -0
  35. package/dist/cli/hooks.d.ts.map +1 -0
  36. package/dist/cli/hooks.js +224 -0
  37. package/dist/cli/hooks.js.map +1 -0
  38. package/dist/cli/knowledge.d.ts +9 -1
  39. package/dist/cli/knowledge.d.ts.map +1 -1
  40. package/dist/cli/knowledge.js +93 -2
  41. package/dist/cli/knowledge.js.map +1 -1
  42. package/dist/cli/loop-executor.d.ts.map +1 -1
  43. package/dist/cli/loop-executor.js +47 -12
  44. package/dist/cli/loop-executor.js.map +1 -1
  45. package/dist/cli/model.d.ts +20 -0
  46. package/dist/cli/model.d.ts.map +1 -1
  47. package/dist/cli/model.js +294 -14
  48. package/dist/cli/model.js.map +1 -1
  49. package/dist/cli/rate.d.ts +78 -0
  50. package/dist/cli/rate.d.ts.map +1 -0
  51. package/dist/cli/rate.js +294 -0
  52. package/dist/cli/rate.js.map +1 -0
  53. package/dist/config/process-env.d.ts.map +1 -1
  54. package/dist/config/process-env.js +10 -0
  55. package/dist/config/process-env.js.map +1 -1
  56. package/dist/enterprise/vault.d.ts.map +1 -1
  57. package/dist/enterprise/vault.js +5 -21
  58. package/dist/enterprise/vault.js.map +1 -1
  59. package/dist/gateway/hook-contract.d.ts +33 -0
  60. package/dist/gateway/hook-contract.d.ts.map +1 -1
  61. package/dist/gateway/hook-contract.js +97 -0
  62. package/dist/gateway/hook-contract.js.map +1 -1
  63. package/dist/inference/gemini-adapter.d.ts.map +1 -1
  64. package/dist/inference/gemini-adapter.js +4 -0
  65. package/dist/inference/gemini-adapter.js.map +1 -1
  66. package/dist/inference/interface.d.ts +37 -0
  67. package/dist/inference/interface.d.ts.map +1 -1
  68. package/dist/inference/native-tools.d.ts +16 -1
  69. package/dist/inference/native-tools.d.ts.map +1 -1
  70. package/dist/inference/native-tools.js +34 -10
  71. package/dist/inference/native-tools.js.map +1 -1
  72. package/dist/inference/route-resolver.d.ts +30 -0
  73. package/dist/inference/route-resolver.d.ts.map +1 -1
  74. package/dist/inference/route-resolver.js +158 -1
  75. package/dist/inference/route-resolver.js.map +1 -1
  76. package/dist/inference/tools.d.ts +16 -1
  77. package/dist/inference/tools.d.ts.map +1 -1
  78. package/dist/inference/tools.js +51 -18
  79. package/dist/inference/tools.js.map +1 -1
  80. package/dist/learning/acceptance-model.d.ts +247 -0
  81. package/dist/learning/acceptance-model.d.ts.map +1 -0
  82. package/dist/learning/acceptance-model.js +708 -0
  83. package/dist/learning/acceptance-model.js.map +1 -0
  84. package/dist/learning/auto-router.d.ts +68 -24
  85. package/dist/learning/auto-router.d.ts.map +1 -1
  86. package/dist/learning/auto-router.js +301 -96
  87. package/dist/learning/auto-router.js.map +1 -1
  88. package/dist/learning/autonomy-policy.d.ts +12 -16
  89. package/dist/learning/autonomy-policy.d.ts.map +1 -1
  90. package/dist/learning/autonomy-policy.js +50 -48
  91. package/dist/learning/autonomy-policy.js.map +1 -1
  92. package/dist/learning/behavioural-labels.d.ts +86 -0
  93. package/dist/learning/behavioural-labels.d.ts.map +1 -0
  94. package/dist/learning/behavioural-labels.js +166 -0
  95. package/dist/learning/behavioural-labels.js.map +1 -0
  96. package/dist/learning/capability-evidence.d.ts +167 -0
  97. package/dist/learning/capability-evidence.d.ts.map +1 -0
  98. package/dist/learning/capability-evidence.js +210 -0
  99. package/dist/learning/capability-evidence.js.map +1 -0
  100. package/dist/learning/capability-readiness.d.ts +64 -0
  101. package/dist/learning/capability-readiness.d.ts.map +1 -0
  102. package/dist/learning/capability-readiness.js +74 -0
  103. package/dist/learning/capability-readiness.js.map +1 -0
  104. package/dist/learning/capability-types.d.ts +169 -0
  105. package/dist/learning/capability-types.d.ts.map +1 -0
  106. package/dist/learning/capability-types.js +20 -0
  107. package/dist/learning/capability-types.js.map +1 -0
  108. package/dist/learning/catalog-feed.d.ts +89 -0
  109. package/dist/learning/catalog-feed.d.ts.map +1 -0
  110. package/dist/learning/catalog-feed.js +211 -0
  111. package/dist/learning/catalog-feed.js.map +1 -0
  112. package/dist/learning/cli-intent-effects.d.ts +60 -0
  113. package/dist/learning/cli-intent-effects.d.ts.map +1 -0
  114. package/dist/learning/cli-intent-effects.js +153 -0
  115. package/dist/learning/cli-intent-effects.js.map +1 -0
  116. package/dist/learning/command-adaptation.d.ts +41 -0
  117. package/dist/learning/command-adaptation.d.ts.map +1 -0
  118. package/dist/learning/command-adaptation.js +122 -0
  119. package/dist/learning/command-adaptation.js.map +1 -0
  120. package/dist/learning/consent-picture.d.ts +42 -0
  121. package/dist/learning/consent-picture.d.ts.map +1 -0
  122. package/dist/learning/consent-picture.js +53 -0
  123. package/dist/learning/consent-picture.js.map +1 -0
  124. package/dist/learning/context-budget.d.ts +37 -6
  125. package/dist/learning/context-budget.d.ts.map +1 -1
  126. package/dist/learning/context-budget.js +38 -9
  127. package/dist/learning/context-budget.js.map +1 -1
  128. package/dist/learning/decision-log.d.ts +114 -0
  129. package/dist/learning/decision-log.d.ts.map +1 -0
  130. package/dist/learning/decision-log.js +296 -0
  131. package/dist/learning/decision-log.js.map +1 -0
  132. package/dist/learning/deliverable-class.d.ts.map +1 -1
  133. package/dist/learning/deliverable-class.js +19 -0
  134. package/dist/learning/deliverable-class.js.map +1 -1
  135. package/dist/learning/deliverable-corpus.d.ts +117 -0
  136. package/dist/learning/deliverable-corpus.d.ts.map +1 -0
  137. package/dist/learning/deliverable-corpus.js +176 -0
  138. package/dist/learning/deliverable-corpus.js.map +1 -0
  139. package/dist/learning/error-repair.d.ts +22 -1
  140. package/dist/learning/error-repair.d.ts.map +1 -1
  141. package/dist/learning/error-repair.js +37 -0
  142. package/dist/learning/error-repair.js.map +1 -1
  143. package/dist/learning/exhaustion-report.d.ts +136 -0
  144. package/dist/learning/exhaustion-report.d.ts.map +1 -0
  145. package/dist/learning/exhaustion-report.js +239 -0
  146. package/dist/learning/exhaustion-report.js.map +1 -0
  147. package/dist/learning/failure-bookkeeping.d.ts +16 -0
  148. package/dist/learning/failure-bookkeeping.d.ts.map +1 -1
  149. package/dist/learning/failure-bookkeeping.js +51 -5
  150. package/dist/learning/failure-bookkeeping.js.map +1 -1
  151. package/dist/learning/gate-friction.d.ts +39 -0
  152. package/dist/learning/gate-friction.d.ts.map +1 -0
  153. package/dist/learning/gate-friction.js +91 -0
  154. package/dist/learning/gate-friction.js.map +1 -0
  155. package/dist/learning/harness-narration.d.ts +34 -0
  156. package/dist/learning/harness-narration.d.ts.map +1 -0
  157. package/dist/learning/harness-narration.js +58 -0
  158. package/dist/learning/harness-narration.js.map +1 -0
  159. package/dist/learning/hub-skill-catalog.d.ts.map +1 -1
  160. package/dist/learning/hub-skill-catalog.js +1 -2
  161. package/dist/learning/hub-skill-catalog.js.map +1 -1
  162. package/dist/learning/hybrid-router.d.ts +65 -0
  163. package/dist/learning/hybrid-router.d.ts.map +1 -1
  164. package/dist/learning/hybrid-router.js +186 -0
  165. package/dist/learning/hybrid-router.js.map +1 -1
  166. package/dist/learning/knowledge-base.d.ts +229 -3
  167. package/dist/learning/knowledge-base.d.ts.map +1 -1
  168. package/dist/learning/knowledge-base.js +603 -25
  169. package/dist/learning/knowledge-base.js.map +1 -1
  170. package/dist/learning/knowledge-eval.d.ts +171 -0
  171. package/dist/learning/knowledge-eval.d.ts.map +1 -0
  172. package/dist/learning/knowledge-eval.js +196 -0
  173. package/dist/learning/knowledge-eval.js.map +1 -0
  174. package/dist/learning/knowledge-turn.d.ts +92 -0
  175. package/dist/learning/knowledge-turn.d.ts.map +1 -0
  176. package/dist/learning/knowledge-turn.js +190 -0
  177. package/dist/learning/knowledge-turn.js.map +1 -0
  178. package/dist/learning/learned-commands.d.ts +64 -0
  179. package/dist/learning/learned-commands.d.ts.map +1 -0
  180. package/dist/learning/learned-commands.js +108 -0
  181. package/dist/learning/learned-commands.js.map +1 -0
  182. package/dist/learning/lexical-search.d.ts +50 -0
  183. package/dist/learning/lexical-search.d.ts.map +1 -0
  184. package/dist/learning/lexical-search.js +111 -0
  185. package/dist/learning/lexical-search.js.map +1 -0
  186. package/dist/learning/machine-facts.d.ts +71 -0
  187. package/dist/learning/machine-facts.d.ts.map +1 -0
  188. package/dist/learning/machine-facts.js +164 -0
  189. package/dist/learning/machine-facts.js.map +1 -0
  190. package/dist/learning/markdown-outline.d.ts +73 -0
  191. package/dist/learning/markdown-outline.d.ts.map +1 -0
  192. package/dist/learning/markdown-outline.js +136 -0
  193. package/dist/learning/markdown-outline.js.map +1 -0
  194. package/dist/learning/model-first-router.d.ts +33 -1
  195. package/dist/learning/model-first-router.d.ts.map +1 -1
  196. package/dist/learning/model-first-router.js +123 -9
  197. package/dist/learning/model-first-router.js.map +1 -1
  198. package/dist/learning/model-identity.d.ts +80 -0
  199. package/dist/learning/model-identity.d.ts.map +1 -0
  200. package/dist/learning/model-identity.js +115 -0
  201. package/dist/learning/model-identity.js.map +1 -0
  202. package/dist/learning/model-registry.d.ts +141 -2
  203. package/dist/learning/model-registry.d.ts.map +1 -1
  204. package/dist/learning/model-registry.js +324 -38
  205. package/dist/learning/model-registry.js.map +1 -1
  206. package/dist/learning/model-scoring.d.ts.map +1 -1
  207. package/dist/learning/model-scoring.js +8 -1
  208. package/dist/learning/model-scoring.js.map +1 -1
  209. package/dist/learning/model-selection.d.ts.map +1 -1
  210. package/dist/learning/model-selection.js +41 -0
  211. package/dist/learning/model-selection.js.map +1 -1
  212. package/dist/learning/outcome-observation.d.ts +49 -0
  213. package/dist/learning/outcome-observation.d.ts.map +1 -0
  214. package/dist/learning/outcome-observation.js +95 -0
  215. package/dist/learning/outcome-observation.js.map +1 -0
  216. package/dist/learning/pair-entitlement.d.ts +184 -0
  217. package/dist/learning/pair-entitlement.d.ts.map +1 -0
  218. package/dist/learning/pair-entitlement.js +214 -0
  219. package/dist/learning/pair-entitlement.js.map +1 -0
  220. package/dist/learning/provider-fallback.d.ts +23 -1
  221. package/dist/learning/provider-fallback.d.ts.map +1 -1
  222. package/dist/learning/provider-fallback.js +24 -0
  223. package/dist/learning/provider-fallback.js.map +1 -1
  224. package/dist/learning/reasoning-trace.d.ts +65 -1
  225. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  226. package/dist/learning/reasoning-trace.js +123 -1
  227. package/dist/learning/reasoning-trace.js.map +1 -1
  228. package/dist/learning/requirement-probe.d.ts +55 -0
  229. package/dist/learning/requirement-probe.d.ts.map +1 -0
  230. package/dist/learning/requirement-probe.js +77 -0
  231. package/dist/learning/requirement-probe.js.map +1 -0
  232. package/dist/learning/resilient-call.d.ts +20 -1
  233. package/dist/learning/resilient-call.d.ts.map +1 -1
  234. package/dist/learning/resilient-call.js +42 -4
  235. package/dist/learning/resilient-call.js.map +1 -1
  236. package/dist/learning/response-usability.d.ts +85 -0
  237. package/dist/learning/response-usability.d.ts.map +1 -0
  238. package/dist/learning/response-usability.js +135 -0
  239. package/dist/learning/response-usability.js.map +1 -0
  240. package/dist/learning/router-bandit.d.ts +132 -0
  241. package/dist/learning/router-bandit.d.ts.map +1 -1
  242. package/dist/learning/router-bandit.js +253 -0
  243. package/dist/learning/router-bandit.js.map +1 -1
  244. package/dist/learning/routing-history.d.ts +41 -2
  245. package/dist/learning/routing-history.d.ts.map +1 -1
  246. package/dist/learning/routing-history.js.map +1 -1
  247. package/dist/learning/run-trace.d.ts +21 -0
  248. package/dist/learning/run-trace.d.ts.map +1 -1
  249. package/dist/learning/run-trace.js +38 -0
  250. package/dist/learning/run-trace.js.map +1 -1
  251. package/dist/learning/session-grant.d.ts +75 -0
  252. package/dist/learning/session-grant.d.ts.map +1 -0
  253. package/dist/learning/session-grant.js +112 -0
  254. package/dist/learning/session-grant.js.map +1 -0
  255. package/dist/learning/task-model-continuity.d.ts +73 -0
  256. package/dist/learning/task-model-continuity.d.ts.map +1 -0
  257. package/dist/learning/task-model-continuity.js +87 -0
  258. package/dist/learning/task-model-continuity.js.map +1 -0
  259. package/dist/learning/turn-feedback.d.ts +93 -0
  260. package/dist/learning/turn-feedback.d.ts.map +1 -0
  261. package/dist/learning/turn-feedback.js +127 -0
  262. package/dist/learning/turn-feedback.js.map +1 -0
  263. package/dist/learning/turn-report.d.ts +104 -2
  264. package/dist/learning/turn-report.d.ts.map +1 -1
  265. package/dist/learning/turn-report.js +161 -6
  266. package/dist/learning/turn-report.js.map +1 -1
  267. package/dist/learning/verdict-routing.d.ts +51 -0
  268. package/dist/learning/verdict-routing.d.ts.map +1 -0
  269. package/dist/learning/verdict-routing.js +122 -0
  270. package/dist/learning/verdict-routing.js.map +1 -0
  271. package/dist/mcp/client.d.ts.map +1 -1
  272. package/dist/mcp/client.js +5 -0
  273. package/dist/mcp/client.js.map +1 -1
  274. package/dist/mcp/manager.d.ts +10 -0
  275. package/dist/mcp/manager.d.ts.map +1 -1
  276. package/dist/mcp/manager.js +20 -0
  277. package/dist/mcp/manager.js.map +1 -1
  278. package/dist/mcp/mcp-discovery.d.ts +34 -0
  279. package/dist/mcp/mcp-discovery.d.ts.map +1 -0
  280. package/dist/mcp/mcp-discovery.js +71 -0
  281. package/dist/mcp/mcp-discovery.js.map +1 -0
  282. package/dist/mcp/mcp-schema-cache.d.ts +17 -0
  283. package/dist/mcp/mcp-schema-cache.d.ts.map +1 -1
  284. package/dist/mcp/mcp-schema-cache.js +47 -3
  285. package/dist/mcp/mcp-schema-cache.js.map +1 -1
  286. package/dist/mcp/types.d.ts +28 -0
  287. package/dist/mcp/types.d.ts.map +1 -1
  288. package/dist/mcp/types.js.map +1 -1
  289. package/dist/parity/drivers.d.ts.map +1 -1
  290. package/dist/parity/drivers.js +3 -0
  291. package/dist/parity/drivers.js.map +1 -1
  292. package/dist/parity/observation.d.ts +12 -0
  293. package/dist/parity/observation.d.ts.map +1 -1
  294. package/dist/parity/observation.js +14 -0
  295. package/dist/parity/observation.js.map +1 -1
  296. package/dist/tools/ask-user.d.ts.map +1 -1
  297. package/dist/tools/ask-user.js +4 -0
  298. package/dist/tools/ask-user.js.map +1 -1
  299. package/dist/tools/capability-registry.d.ts +150 -0
  300. package/dist/tools/capability-registry.d.ts.map +1 -0
  301. package/dist/tools/capability-registry.js +400 -0
  302. package/dist/tools/capability-registry.js.map +1 -0
  303. package/dist/tools/child-agent-runtime.d.ts.map +1 -1
  304. package/dist/tools/child-agent-runtime.js +19 -5
  305. package/dist/tools/child-agent-runtime.js.map +1 -1
  306. package/dist/tools/coding-tools.d.ts +17 -5
  307. package/dist/tools/coding-tools.d.ts.map +1 -1
  308. package/dist/tools/coding-tools.js +80 -11
  309. package/dist/tools/coding-tools.js.map +1 -1
  310. package/dist/tools/command-memo.d.ts +116 -0
  311. package/dist/tools/command-memo.d.ts.map +1 -0
  312. package/dist/tools/command-memo.js +202 -0
  313. package/dist/tools/command-memo.js.map +1 -0
  314. package/dist/tools/git-tool.d.ts.map +1 -1
  315. package/dist/tools/git-tool.js +13 -3
  316. package/dist/tools/git-tool.js.map +1 -1
  317. package/dist/tools/harness-directive.d.ts +42 -0
  318. package/dist/tools/harness-directive.d.ts.map +1 -0
  319. package/dist/tools/harness-directive.js +45 -0
  320. package/dist/tools/harness-directive.js.map +1 -0
  321. package/dist/tools/loop-skill-hint.d.ts +74 -174
  322. package/dist/tools/loop-skill-hint.d.ts.map +1 -1
  323. package/dist/tools/loop-skill-hint.js +110 -536
  324. package/dist/tools/loop-skill-hint.js.map +1 -1
  325. package/dist/tools/mcp-client-tool.d.ts +2 -0
  326. package/dist/tools/mcp-client-tool.d.ts.map +1 -1
  327. package/dist/tools/mcp-client-tool.js +3 -0
  328. package/dist/tools/mcp-client-tool.js.map +1 -1
  329. package/dist/tools/modality/shared.d.ts +1 -4
  330. package/dist/tools/modality/shared.d.ts.map +1 -1
  331. package/dist/tools/modality/shared.js +7 -22
  332. package/dist/tools/modality/shared.js.map +1 -1
  333. package/dist/tools/plan-store.d.ts +20 -2
  334. package/dist/tools/plan-store.d.ts.map +1 -1
  335. package/dist/tools/plan-store.js +51 -7
  336. package/dist/tools/plan-store.js.map +1 -1
  337. package/dist/tools/registry.d.ts +76 -3
  338. package/dist/tools/registry.d.ts.map +1 -1
  339. package/dist/tools/registry.js +486 -26
  340. package/dist/tools/registry.js.map +1 -1
  341. package/dist/tools/remediation-ladder.d.ts.map +1 -1
  342. package/dist/tools/remediation-ladder.js +8 -0
  343. package/dist/tools/remediation-ladder.js.map +1 -1
  344. package/dist/tools/run-cli.d.ts.map +1 -1
  345. package/dist/tools/run-cli.js +29 -4
  346. package/dist/tools/run-cli.js.map +1 -1
  347. package/dist/tools/run-terminal.d.ts +11 -0
  348. package/dist/tools/run-terminal.d.ts.map +1 -1
  349. package/dist/tools/run-terminal.js +265 -10
  350. package/dist/tools/run-terminal.js.map +1 -1
  351. package/dist/tools/tool-loop.d.ts +321 -13
  352. package/dist/tools/tool-loop.d.ts.map +1 -1
  353. package/dist/tools/tool-loop.js +867 -66
  354. package/dist/tools/tool-loop.js.map +1 -1
  355. package/dist/utils/atomic-store.d.ts +53 -0
  356. package/dist/utils/atomic-store.d.ts.map +1 -0
  357. package/dist/utils/atomic-store.js +178 -0
  358. package/dist/utils/atomic-store.js.map +1 -0
  359. package/dist/utils/binary-probe.d.ts +25 -0
  360. package/dist/utils/binary-probe.d.ts.map +1 -0
  361. package/dist/utils/binary-probe.js +48 -0
  362. package/dist/utils/binary-probe.js.map +1 -0
  363. package/dist/utils/workspace-path.d.ts +50 -0
  364. package/dist/utils/workspace-path.d.ts.map +1 -0
  365. package/dist/utils/workspace-path.js +86 -0
  366. package/dist/utils/workspace-path.js.map +1 -0
  367. package/dist/web-dashboard/chat-console.d.ts +24 -0
  368. package/dist/web-dashboard/chat-console.d.ts.map +1 -1
  369. package/dist/web-dashboard/chat-console.js +38 -0
  370. package/dist/web-dashboard/chat-console.js.map +1 -1
  371. package/dist/web-dashboard/project-context.d.ts +27 -2
  372. package/dist/web-dashboard/project-context.d.ts.map +1 -1
  373. package/dist/web-dashboard/project-context.js +32 -2
  374. package/dist/web-dashboard/project-context.js.map +1 -1
  375. package/dist/web-dashboard/server.d.ts +11 -0
  376. package/dist/web-dashboard/server.d.ts.map +1 -1
  377. package/dist/web-dashboard/server.js +362 -11
  378. package/dist/web-dashboard/server.js.map +1 -1
  379. package/dist/web-dashboard/src/types.d.ts +170 -1
  380. package/dist/web-dashboard/src/types.d.ts.map +1 -1
  381. package/dist/web-dashboard/workspace-guard.d.ts +11 -2
  382. package/dist/web-dashboard/workspace-guard.d.ts.map +1 -1
  383. package/dist/web-dashboard/workspace-guard.js +24 -12
  384. package/dist/web-dashboard/workspace-guard.js.map +1 -1
  385. package/dist/web-dashboard/workspace-resolution.d.ts +87 -0
  386. package/dist/web-dashboard/workspace-resolution.d.ts.map +1 -0
  387. package/dist/web-dashboard/workspace-resolution.js +105 -0
  388. package/dist/web-dashboard/workspace-resolution.js.map +1 -0
  389. package/package.json +1 -1
  390. package/src/web-dashboard/public/assets/index-zjbdJnO-.js +210 -0
  391. package/src/web-dashboard/public/assets/index-zjbdJnO-.js.map +1 -0
  392. package/src/web-dashboard/public/index.html +1 -1
  393. package/dist/agents/nvda-addon.d.ts +0 -14
  394. package/dist/agents/nvda-addon.js +0 -52
  395. package/dist/forwarded.d.ts +0 -2
  396. package/dist/forwarded.js +0 -2
  397. package/src/web-dashboard/public/assets/index-DhiuR4S2.js +0 -210
  398. package/src/web-dashboard/public/assets/index-DhiuR4S2.js.map +0 -1
@@ -27,7 +27,16 @@ 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';
32
41
  import { checkStepArtifacts } from './step-artifact.js';
33
42
  // ─── Tool definitions ───────────────────────────────────────────────────────
@@ -94,6 +103,19 @@ const askUserSchema = z.object({
94
103
  .describe('2–4 answer choices the user can pick from'),
95
104
  multi_select: z.boolean().default(false).describe('Allow multiple selections (checkbox)'),
96
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
+ }
97
119
  /** E3c — publish tool args: bump type + safety flags (irreversible action). */
98
120
  export const publishToolSchema = z.object({
99
121
  goal: z.string().optional().describe('Optional release goal (e.g. "Release v1.2.0")'),
@@ -307,8 +329,12 @@ const editFileSchema = z.object({
307
329
  });
308
330
  /** P0.3 — write_file tool args: create/overwrite a file (confirmed). */
309
331
  const writeFileSchema = z.object({
310
- path: z.string().describe('Path to write, relative to the workspace root (parent directories are created as needed). Overwrites existing content.'),
311
- 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.'),
312
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.'),
313
339
  });
314
340
  /** P0.4 — run_terminal tool args: verify commands + gated shell execution. */
@@ -316,6 +342,7 @@ const runTerminalSchema = z.object({
316
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.'),
317
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).'),
318
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.'),
319
346
  });
320
347
  /** The C2 requirementState check as a reusable tool. */
321
348
  const verifyRequirementSchema = z.object({
@@ -348,6 +375,11 @@ const registry = new Map();
348
375
  */
349
376
  export function registerTool(tool) {
350
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);
351
383
  }
352
384
  /** Look up a tool by name. */
353
385
  export function getTool(name) {
@@ -435,6 +467,7 @@ registerTool({
435
467
  // same pipeline core + the publish workflow, so any ask auto-runs.
436
468
  registerTool({
437
469
  name: 'document',
470
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
438
471
  description: 'Write documentation for the project (README, API docs, guides) through the agent pipeline',
439
472
  category: 'pipeline',
440
473
  inputSchema: taskGoalArgs,
@@ -443,6 +476,7 @@ registerTool({
443
476
  });
444
477
  registerTool({
445
478
  name: 'website',
479
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
446
480
  description: 'Create or build a website / web app end-to-end through the agent pipeline',
447
481
  category: 'pipeline',
448
482
  inputSchema: taskGoalArgs,
@@ -451,6 +485,7 @@ registerTool({
451
485
  });
452
486
  registerTool({
453
487
  name: 'analyze',
488
+ capability: { effectClass: 'read' },
454
489
  description: 'Analyze the project (state, architecture, comparison against another project) and produce findings',
455
490
  category: 'pipeline',
456
491
  inputSchema: taskGoalArgs,
@@ -467,6 +502,7 @@ registerTool({
467
502
  });
468
503
  registerTool({
469
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' }] } },
470
506
  description: 'Publish a release to npm/GitHub (version bump, changelog, build, publish). IRREVERSIBLE — confirm bump type and target with ask_user first.',
471
507
  category: 'workflow',
472
508
  inputSchema: publishToolSchema,
@@ -485,6 +521,7 @@ registerTool({
485
521
  });
486
522
  registerTool({
487
523
  name: 'ask_user',
524
+ capability: { effectClass: 'read' },
488
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.',
489
526
  category: 'experience',
490
527
  inputSchema: askUserSchema,
@@ -538,6 +575,14 @@ registerTool({
538
575
  // question reaches the user once.
539
576
  const pending = ctx.pendingConfirmation;
540
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 &&
541
586
  (ctx.writesAuthorized?.authorized === true || Boolean(ctx.envelope)) &&
542
587
  detectPermissionSeeking(question) &&
543
588
  !IRREVERSIBLE_ACTION_RE.test(question)) {
@@ -565,18 +610,131 @@ registerTool({
565
610
  // One confirmation refusal buys exactly ONE ask — clear it before rendering.
566
611
  if (pending)
567
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');
568
639
  const render = ctx.askUser || (await import('./ask-user.js')).renderAskUser;
569
- const answer = await render(question, choices, multi_select);
640
+ const answer = await render(question, offeredChoices, multi_select);
570
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
+ }
571
669
  // Recorded SHOWN and with the answer, so the trace can hand both back when
572
670
  // the agent tries to ask it a second time (and so a self-report can say what
573
671
  // the user actually replied, instead of "asked 4×, no answer recorded").
574
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
+ }
575
732
  return `User answered: ${picked}${answer.custom ? ` (custom: ${answer.custom})` : ''}`;
576
733
  },
577
734
  });
578
735
  registerTool({
579
736
  name: 'suggest_followups',
737
+ capability: { effectClass: 'read' },
580
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).',
581
739
  category: 'experience',
582
740
  inputSchema: suggestFollowupsSchema,
@@ -595,6 +753,7 @@ registerTool({
595
753
  // LLM needed — runs entirely on file system access.
596
754
  registerTool({
597
755
  name: 'code_search',
756
+ capability: { effectClass: 'read' },
598
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.',
599
758
  category: 'workflow',
600
759
  inputSchema: codeSearchSchema,
@@ -627,6 +786,7 @@ registerTool({
627
786
  // Backed by `src/tools/coding-tools.ts` (pure fs — no new dependencies).
628
787
  registerTool({
629
788
  name: 'read_file',
789
+ capability: { effectClass: 'read' },
630
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.',
631
791
  category: 'workflow',
632
792
  inputSchema: readFileSchema,
@@ -635,6 +795,7 @@ registerTool({
635
795
  });
636
796
  registerTool({
637
797
  name: 'list_dir',
798
+ capability: { effectClass: 'read' },
638
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.',
639
800
  category: 'workflow',
640
801
  inputSchema: listDirSchema,
@@ -643,6 +804,7 @@ registerTool({
643
804
  });
644
805
  registerTool({
645
806
  name: 'glob',
807
+ capability: { effectClass: 'read' },
646
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.',
647
809
  category: 'workflow',
648
810
  inputSchema: globSchema,
@@ -651,6 +813,7 @@ registerTool({
651
813
  });
652
814
  registerTool({
653
815
  name: 'edit_file',
816
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
654
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.',
655
818
  category: 'workflow',
656
819
  inputSchema: editFileSchema,
@@ -659,7 +822,8 @@ registerTool({
659
822
  });
660
823
  registerTool({
661
824
  name: 'write_file',
662
- 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.',
663
827
  category: 'workflow',
664
828
  inputSchema: writeFileSchema,
665
829
  endsAgentStep: false,
@@ -667,6 +831,7 @@ registerTool({
667
831
  });
668
832
  registerTool({
669
833
  name: 'run_terminal',
834
+ capability: { effectClass: 'local-state', grantCategory: 'terminal' },
670
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.',
671
836
  category: 'workflow',
672
837
  inputSchema: runTerminalSchema,
@@ -690,11 +855,25 @@ registerTool({
690
855
  const { action, goal, steps, id, status, note } = planTodoSchema.parse(args);
691
856
  const { defaultPlanStore } = await import('./plan-store.js');
692
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 = '';
693
863
  if (action === 'create') {
694
864
  if (!goal || !steps || steps.length === 0) {
695
865
  return 'Error: plan_todo create needs goal + steps (id + description each).';
696
866
  }
867
+ const before = store.snapshot?.() ?? null;
697
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
+ }
698
877
  }
699
878
  else if (id && status) {
700
879
  // A1 — never accept an update that matched nothing. Silently returning
@@ -733,7 +912,7 @@ registerTool({
733
912
  ctx.emit?.('plan:changed', snapshot);
734
913
  }
735
914
  }
736
- return store.toText ? store.toText() : `Plan updated (${action}).`;
915
+ return (store.toText ? store.toText() : `Plan updated (${action}).`) + carriedNote;
737
916
  },
738
917
  });
739
918
  // ─── WS1 (#23) finding tool — a claim, and the verdict the gate computed ────
@@ -753,6 +932,7 @@ registerTool(createFindingTool());
753
932
  // `src/tools/web-research.ts` (mocked-fetch tests, no network).
754
933
  registerTool({
755
934
  name: 'web_search',
935
+ capability: { effectClass: 'read' },
756
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.',
757
937
  category: 'workflow',
758
938
  inputSchema: webSearchSchema,
@@ -775,6 +955,7 @@ registerTool({
775
955
  });
776
956
  registerTool({
777
957
  name: 'read_page',
958
+ capability: { effectClass: 'read' },
778
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.',
779
960
  category: 'workflow',
780
961
  inputSchema: readPageSchema,
@@ -792,6 +973,7 @@ registerTool({
792
973
  // ─── I2–I5 modality packs (browser / image / voice / vision) ────────────────
793
974
  registerTool({
794
975
  name: 'browser',
976
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
795
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).',
796
978
  category: 'workflow',
797
979
  inputSchema: browserSchema,
@@ -809,6 +991,7 @@ registerTool({
809
991
  });
810
992
  registerTool({
811
993
  name: 'generate_image',
994
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
812
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.',
813
996
  category: 'workflow',
814
997
  inputSchema: imageGenSchema,
@@ -845,6 +1028,7 @@ registerTool({
845
1028
  });
846
1029
  registerTool({
847
1030
  name: 'speak',
1031
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
848
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.',
849
1033
  category: 'workflow',
850
1034
  inputSchema: speakSchema,
@@ -868,6 +1052,7 @@ registerTool({
868
1052
  });
869
1053
  registerTool({
870
1054
  name: 'transcribe',
1055
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
871
1056
  description: 'Transcribe an audio file to text (whisper.cpp or faster-whisper, local and free). Requires whisper on PATH. Returns the transcript.',
872
1057
  category: 'workflow',
873
1058
  inputSchema: transcribeSchema,
@@ -881,6 +1066,7 @@ registerTool({
881
1066
  });
882
1067
  registerTool({
883
1068
  name: 'describe_image',
1069
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
884
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.',
885
1071
  category: 'workflow',
886
1072
  inputSchema: describeImageSchema,
@@ -897,6 +1083,7 @@ registerTool({
897
1083
  });
898
1084
  registerTool({
899
1085
  name: 'gateway_send',
1086
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
900
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.',
901
1088
  category: 'workflow',
902
1089
  inputSchema: gatewaySendSchema,
@@ -905,6 +1092,7 @@ registerTool({
905
1092
  });
906
1093
  registerTool({
907
1094
  name: 'run_cli',
1095
+ capability: { effectClass: 'local-state', grantCategory: 'terminal' },
908
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.',
909
1097
  category: 'workflow',
910
1098
  inputSchema: runCliSchema,
@@ -913,6 +1101,7 @@ registerTool({
913
1101
  });
914
1102
  registerTool({
915
1103
  name: 'verify_requirement',
1104
+ capability: { effectClass: 'read' },
916
1105
  description: 'Verify whether a user request is complete enough to act on (requirementState: complete vs needs-clarification), returning the missing information.',
917
1106
  category: 'experience',
918
1107
  inputSchema: verifyRequirementSchema,
@@ -945,6 +1134,7 @@ registerTool({
945
1134
  // exec (no shell) make injection structurally impossible.
946
1135
  registerTool({
947
1136
  name: 'clone_repo',
1137
+ capability: { effectClass: 'external', grantCategory: 'external', reversibleHow: 'delete the cloned directory' },
948
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.',
949
1139
  category: 'workflow',
950
1140
  inputSchema: cloneRepoSchema,
@@ -960,6 +1150,7 @@ registerTool({
960
1150
  // while reset --hard/clean remain structurally unexpressible AND deny-guarded.
961
1151
  registerTool({
962
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' },
963
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.',
964
1155
  category: 'workflow',
965
1156
  inputSchema: gitToolSchema,
@@ -1131,6 +1322,7 @@ registerTool({
1131
1322
  // Browser Supervisor tool
1132
1323
  registerTool({
1133
1324
  name: 'browser_supervisor',
1325
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
1134
1326
  description: 'Browser CDP supervisor: monitor health, detect dialogs, track frames.',
1135
1327
  category: 'workflow',
1136
1328
  inputSchema: z.object({
@@ -1152,6 +1344,7 @@ registerTool({
1152
1344
  // Browser Dialog tool
1153
1345
  registerTool({
1154
1346
  name: 'browser_dialog',
1347
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
1155
1348
  description: 'Handle native browser dialogs: accept, dismiss, or enter values.',
1156
1349
  category: 'workflow',
1157
1350
  inputSchema: z.object({
@@ -1179,6 +1372,7 @@ registerTool({
1179
1372
  // Camofox tool
1180
1373
  registerTool({
1181
1374
  name: 'camofox',
1375
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
1182
1376
  description: 'Camofox anti-detection browser: open pages, screenshots, interact via a11y refs.',
1183
1377
  category: 'workflow',
1184
1378
  inputSchema: z.object({
@@ -1206,6 +1400,7 @@ registerTool({
1206
1400
  // Docker tool
1207
1401
  registerTool({
1208
1402
  name: 'docker',
1403
+ capability: { effectClass: 'local-state', grantCategory: 'terminal' },
1209
1404
  description: 'Docker management: containers, images, compose, volumes, networks.',
1210
1405
  category: 'workflow',
1211
1406
  inputSchema: z.object({
@@ -1320,6 +1515,7 @@ registerTool({
1320
1515
  // Kanban tool
1321
1516
  registerTool({
1322
1517
  name: 'kanban',
1518
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
1323
1519
  description: 'Kanban board: create boards, manage cards, track priorities.',
1324
1520
  category: 'workflow',
1325
1521
  inputSchema: z.object({
@@ -1438,6 +1634,7 @@ registerTool({
1438
1634
  // Blueprint tool
1439
1635
  registerTool({
1440
1636
  name: 'blueprint',
1637
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
1441
1638
  description: 'Project blueprints: create project templates with file structure.',
1442
1639
  category: 'workflow',
1443
1640
  inputSchema: z.object({
@@ -1459,6 +1656,7 @@ registerTool({
1459
1656
  // File ops tool
1460
1657
  registerTool({
1461
1658
  name: 'file_ops',
1659
+ capability: { effectClass: 'local-write', grantCategory: 'write', reversibleHow: "a git revert" },
1462
1660
  description: 'Advanced file operations: state tracking, safe writes, extraction, preview.',
1463
1661
  category: 'workflow',
1464
1662
  inputSchema: z.object({
@@ -1669,6 +1867,7 @@ registerTool({
1669
1867
  // tool-loop.ts trusts). See TOOL_TRUTHFULNESS_TRACKER.md findings #3 and #9.
1670
1868
  registerTool({
1671
1869
  name: 'messaging',
1870
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
1672
1871
  description: 'NOT CONNECTED — this tool has no delivery adapter and every send/react refuses with code "not_configured". '
1673
1872
  + 'Do NOT use it to deliver anything and never report a delivery from it. Use gateway_send instead: it sends '
1674
1873
  + 'through the configured gateway (WhatsApp by contact name or number, Telegram, Slack, Discord, email) and '
@@ -1700,6 +1899,7 @@ registerTool({
1700
1899
  // Vision tool — comprehensive image analysis, OCR, UI element detection
1701
1900
  registerTool({
1702
1901
  name: 'vision',
1902
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
1703
1903
  description: 'Analyze an image and transcribe its text (OCR) through the configured vision backend (a local Ollama '
1704
1904
  + 'vision model or a Gemini key); `analyze` also reports real pixel dimensions. detect-elements, '
1705
1905
  + 'find-element and compare have NO backend and return ok:false with code "not_configured" — an empty '
@@ -1781,6 +1981,7 @@ registerTool({
1781
1981
  // Process registry tool
1782
1982
  registerTool({
1783
1983
  name: 'process_registry',
1984
+ capability: { effectClass: 'read' },
1784
1985
  description: 'Track all running processes with metadata and cleanup.',
1785
1986
  category: 'workflow',
1786
1987
  inputSchema: z.object({
@@ -1806,6 +2007,7 @@ registerTool({
1806
2007
  // Code execution tool
1807
2008
  registerTool({
1808
2009
  name: 'code_execution',
2010
+ capability: { effectClass: 'local-state', grantCategory: 'terminal' },
1809
2011
  description: 'Execute code in sandboxed environment with timeout and resource limits.',
1810
2012
  category: 'workflow',
1811
2013
  inputSchema: z.object({
@@ -1863,6 +2065,7 @@ registerTool({
1863
2065
  // Threat patterns tool
1864
2066
  registerTool({
1865
2067
  name: 'threat_patterns',
2068
+ capability: { effectClass: 'read' },
1866
2069
  description: 'Detect threat patterns: shell execution, external HTTP, obfuscation.',
1867
2070
  category: 'workflow',
1868
2071
  inputSchema: z.object({
@@ -1877,6 +2080,7 @@ registerTool({
1877
2080
  // URL safety tool
1878
2081
  registerTool({
1879
2082
  name: 'url_safety',
2083
+ capability: { effectClass: 'read' },
1880
2084
  description: 'Check URL safety: suspicious TLDs, patterns, length.',
1881
2085
  category: 'workflow',
1882
2086
  inputSchema: z.object({
@@ -1891,6 +2095,7 @@ registerTool({
1891
2095
  // Path security tool
1892
2096
  registerTool({
1893
2097
  name: 'path_security',
2098
+ capability: { effectClass: 'read' },
1894
2099
  description: 'Check path security: traversal, null bytes, directory escape.',
1895
2100
  category: 'workflow',
1896
2101
  inputSchema: z.object({
@@ -1906,6 +2111,7 @@ registerTool({
1906
2111
  // Security score tool
1907
2112
  registerTool({
1908
2113
  name: 'security_score',
2114
+ capability: { effectClass: 'read' },
1909
2115
  description: 'Calculate security score for a file: code injection, secrets, threats.',
1910
2116
  category: 'workflow',
1911
2117
  inputSchema: z.object({
@@ -1926,6 +2132,7 @@ registerTool({
1926
2132
  // and no network, and reports masked values only — see src/security/secret-scan.ts.
1927
2133
  registerTool({
1928
2134
  name: 'secret_scan',
2135
+ capability: { effectClass: 'read' },
1929
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.',
1930
2137
  category: 'workflow',
1931
2138
  inputSchema: z.object({
@@ -1975,21 +2182,28 @@ registerTool({
1975
2182
  // ~85% of per-turn schema tokens saved, zero capability lost.
1976
2183
  registerTool({
1977
2184
  name: 'tool_search',
1978
- 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; ' +
1979
2189
  '"load" activates a whole toolset (media, browser, channels, docker, ' +
1980
2190
  'productivity, publish, core-pipeline, …) for THIS turn — call it before ' +
1981
2191
  'using any tool outside the always-available core set. "load" returns ' +
1982
- '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.',
1983
2194
  category: 'workflow',
1984
2195
  inputSchema: z.object({
1985
- action: z.enum(['search', 'load']).optional().describe('Defaults to "search".'),
1986
- 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)'),
1987
2198
  limit: z.number().optional().describe('Max results (action=search)'),
1988
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)'),
1989
2203
  }),
1990
2204
  endsAgentStep: false,
1991
2205
  run: async (args, ctx) => {
1992
- const { action = 'search', query, limit, toolset } = args;
2206
+ const { action = 'search', query, limit, toolset, command, verb, note } = args;
1993
2207
  // ── load: activate a toolset for this turn (tiered exposure) ──
1994
2208
  if (action === 'load') {
1995
2209
  if (!toolset) {
@@ -2032,13 +2246,181 @@ registerTool({
2032
2246
  : 'Toolset recorded, but this context has no live schema loader (loop not in tiered mode) — tools remain governed by the standard enablement gate.',
2033
2247
  });
2034
2248
  }
2035
- // ── 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.
2036
2327
  const m = await import('./infra-tools.js');
2037
2328
  const engine = m.getToolSearchEngine();
2038
- // Index from registry
2039
2329
  const { listTools } = await import('./registry.js');
2040
- engine.index(listTools().map((t) => ({ name: t.name, description: t.description })));
2041
- 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
+ });
2042
2424
  },
2043
2425
  });
2044
2426
  // Budget config tool
@@ -2067,6 +2449,7 @@ registerTool({
2067
2449
  // Fuzzy match tool
2068
2450
  registerTool({
2069
2451
  name: 'fuzzy_match',
2452
+ capability: { effectClass: 'read' },
2070
2453
  description: 'Fuzzy string matching with Levenshtein distance.',
2071
2454
  category: 'workflow',
2072
2455
  inputSchema: z.object({
@@ -2100,6 +2483,7 @@ registerTool({
2100
2483
  // OSV check tool
2101
2484
  registerTool({
2102
2485
  name: 'osv_check',
2486
+ capability: { effectClass: 'read' },
2103
2487
  description: 'Check for known vulnerabilities in npm packages via OSV API.',
2104
2488
  category: 'workflow',
2105
2489
  inputSchema: z.object({
@@ -2129,6 +2513,7 @@ registerTool({
2129
2513
  // Image source tool
2130
2514
  registerTool({
2131
2515
  name: 'image_source',
2516
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2132
2517
  description: 'Detect image source: format, size, is screenshot.',
2133
2518
  category: 'workflow',
2134
2519
  inputSchema: z.object({
@@ -2144,6 +2529,7 @@ registerTool({
2144
2529
  // Discord tool
2145
2530
  registerTool({
2146
2531
  name: 'discord',
2532
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2147
2533
  description: 'Discord server management: list servers, channels, members, messages, roles. Send messages.',
2148
2534
  category: 'workflow',
2149
2535
  inputSchema: z.object({
@@ -2171,6 +2557,7 @@ registerTool({
2171
2557
  // Home Assistant tool
2172
2558
  registerTool({
2173
2559
  name: 'homeassistant',
2560
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2174
2561
  description: 'Home Assistant smart home: list entities, get state, call services (turn_on/off, set_temperature).',
2175
2562
  category: 'workflow',
2176
2563
  inputSchema: z.object({
@@ -2199,6 +2586,7 @@ registerTool({
2199
2586
  // Microsoft Graph tool
2200
2587
  registerTool({
2201
2588
  name: 'microsoft_graph',
2589
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2202
2590
  description: 'Microsoft Graph API: email, calendar, OneDrive, contacts.',
2203
2591
  category: 'workflow',
2204
2592
  inputSchema: z.object({
@@ -2249,6 +2637,7 @@ registerTool({
2249
2637
  // Feishu document tool
2250
2638
  registerTool({
2251
2639
  name: 'feishu_doc',
2640
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2252
2641
  description: 'Feishu document management: create, read, update, delete documents.',
2253
2642
  category: 'workflow',
2254
2643
  inputSchema: z.object({
@@ -2273,6 +2662,7 @@ registerTool({
2273
2662
  // Feishu drive tool
2274
2663
  registerTool({
2275
2664
  name: 'feishu_drive',
2665
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2276
2666
  description: 'Feishu drive management: list files, create folders, move, copy, search.',
2277
2667
  category: 'workflow',
2278
2668
  inputSchema: z.object({
@@ -2302,6 +2692,7 @@ registerTool({
2302
2692
  // Video generation tool
2303
2693
  registerTool({
2304
2694
  name: 'video_generate',
2695
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2305
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.',
2306
2697
  category: 'workflow',
2307
2698
  inputSchema: z.object({
@@ -2410,6 +2801,7 @@ registerTool({
2410
2801
  // NeuTTS synthesis tool
2411
2802
  registerTool({
2412
2803
  name: 'neutts_synth',
2804
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2413
2805
  description: 'High-quality text-to-speech synthesis via NeuTTS.',
2414
2806
  category: 'workflow',
2415
2807
  inputSchema: z.object({
@@ -2433,6 +2825,7 @@ registerTool({
2433
2825
  // Terminal tool — multi-environment execution
2434
2826
  registerTool({
2435
2827
  name: 'terminal',
2828
+ capability: { effectClass: 'local-state', grantCategory: 'terminal' },
2436
2829
  description: 'Execute commands in local, Docker, Modal, SSH environments with background support.',
2437
2830
  category: 'workflow',
2438
2831
  inputSchema: z.object({
@@ -2487,6 +2880,7 @@ registerTool({
2487
2880
  // Send message tool — cross-channel messaging
2488
2881
  registerTool({
2489
2882
  name: 'send_message',
2883
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2490
2884
  description: 'Send messages to Telegram, Discord, Slack, WhatsApp, Email. Supports media attachments (images, videos, audio, documents) on WhatsApp.',
2491
2885
  category: 'workflow',
2492
2886
  inputSchema: z.object({
@@ -2748,6 +3142,7 @@ registerTool({
2748
3142
  // MCP Tool — Dynamic MCP server connection and tool invocation
2749
3143
  registerTool({
2750
3144
  name: 'mcp_tool',
3145
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2751
3146
  description: 'Connect to external MCP servers and invoke their tools — stdio, HTTP, or SSE transport.',
2752
3147
  category: 'workflow',
2753
3148
  inputSchema: z.object({
@@ -2775,6 +3170,7 @@ registerTool({
2775
3170
  // Computer Use — Desktop automation via cua-driver
2776
3171
  registerTool({
2777
3172
  name: 'computer_use',
3173
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2778
3174
  description: 'Desktop control via cua-driver — screenshots, mouse, keyboard, scroll without stealing focus.',
2779
3175
  category: 'workflow',
2780
3176
  inputSchema: z.object({
@@ -2863,6 +3259,7 @@ registerTool({
2863
3259
  // OpenRouter Client — Multi-LLM routing
2864
3260
  registerTool({
2865
3261
  name: 'openrouter_client',
3262
+ capability: { effectClass: 'external', grantCategory: 'external' },
2866
3263
  description: 'Route LLM calls to multiple providers via OpenRouter — cost optimization and fallback.',
2867
3264
  category: 'workflow',
2868
3265
  inputSchema: z.object({
@@ -2887,6 +3284,7 @@ registerTool({
2887
3284
  // TTS Streaming — Provider-agnostic streaming TTS
2888
3285
  registerTool({
2889
3286
  name: 'tts_streaming',
3287
+ capability: { effectClass: 'external', reversible: false, grantCategory: 'external' },
2890
3288
  description: 'Convert text to speech using various providers — streaming and batch modes.',
2891
3289
  category: 'workflow',
2892
3290
  inputSchema: z.object({
@@ -2965,6 +3363,7 @@ registerTool({
2965
3363
  // that cannot, so the model can distinguish "read it" from "could not read it".
2966
3364
  registerTool({
2967
3365
  name: 'read_extract',
3366
+ capability: { effectClass: 'read' },
2968
3367
  description: 'Extract text from a document. WORKS for PDF, DOCX, XLSX, PPTX, HTML, CSV/TSV, JSON, XML, YAML and '
2969
3368
  + 'plain text/Markdown; PDF and spreadsheet table rows are preserved so values stay paired with their '
2970
3369
  + 'columns. Returns success:false with a typed code when it cannot read the file — "no_data" means a '
@@ -2997,23 +3396,29 @@ registerTool({
2997
3396
  // knowledge / web-research.
2998
3397
  registerTool({
2999
3398
  name: 'knowledge',
3000
- description: 'Answer questions from the USER\'S OWN tagged documents. `add` ingests files/folders under a tag '
3001
- + '(extract → chunk → embed, done once); `query` retrieves the tag\'s most relevant passages, labeled '
3002
- + 'with their source file, so the data part of an answer comes from the user\'s data and the general '
3003
- + 'part from model knowledge or web-research. Use this instead of re-reading a large document on every '
3004
- + 'turn. Actions: add | query | list | forget | stats. Tags are normalized (spaces/capitals/underscores '
3005
- + 'become lowercase-hyphen), e.g. "Dheeraj health report" → "dheeraj-health-report".',
3006
- category: 'workflow',
3007
- inputSchema: z.object({
3008
- action: z.enum(['add', 'query', 'list', 'forget', 'stats']).describe('Action'),
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'),
3009
3412
  tag: z.string().optional().describe('Tag/hashtag scoping the data (e.g. "dheeraj-health-report")'),
3010
- paths: z.array(z.string()).optional().describe('Files or folders to ingest (action: add)'),
3413
+ paths: z.array(z.string()).optional().describe('Files or folders to ingest (action: add | sync)'),
3011
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'),
3012
3417
  topK: z.number().optional().describe('Chunks to retrieve (action: query, default 6)'),
3013
3418
  }),
3014
3419
  endsAgentStep: false,
3015
3420
  run: (args) => import('../learning/knowledge-base.js').then(async (m) => {
3016
- const { action, tag, paths, question, topK } = args;
3421
+ const { action, tag, paths, question, doc, section, topK } = args;
3017
3422
  switch (action) {
3018
3423
  case 'add': {
3019
3424
  if (!tag)
@@ -3024,7 +3429,11 @@ registerTool({
3024
3429
  const skipped = result.skipped.length
3025
3430
  ? `\nSkipped: ${result.skipped.map((s) => `${s.path} (${s.reason})`).join('; ')}`
3026
3431
  : '';
3027
- return `✅ Ingested ${result.files} document(s) / ${result.chunks} chunk(s) under tag '${result.tag}'.`
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}'.`
3028
3437
  + `\nQuery it with action: query, tag: '${result.tag}'.${skipped}`;
3029
3438
  }
3030
3439
  case 'query': {
@@ -3035,6 +3444,54 @@ registerTool({
3035
3444
  const hits = await m.queryKnowledge(tag, question, { topK });
3036
3445
  return m.formatKnowledgeContext(tag, hits);
3037
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
+ }
3038
3495
  case 'list': {
3039
3496
  const tags = m.listKnowledgeTags();
3040
3497
  if (tags.length === 0)
@@ -3112,6 +3569,7 @@ registerTool({
3112
3569
  });
3113
3570
  registerTool({
3114
3571
  name: 'search_memory',
3572
+ capability: { effectClass: 'read' },
3115
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.',
3116
3574
  category: 'workflow',
3117
3575
  inputSchema: z.object({
@@ -3173,6 +3631,7 @@ registerTool({
3173
3631
  });
3174
3632
  registerTool({
3175
3633
  name: 'list_memories',
3634
+ capability: { effectClass: 'read' },
3176
3635
  description: 'List all memory entries with optional filters.',
3177
3636
  category: 'workflow',
3178
3637
  inputSchema: z.object({
@@ -3189,6 +3648,7 @@ registerTool({
3189
3648
  });
3190
3649
  registerTool({
3191
3650
  name: 'memory_stats',
3651
+ capability: { effectClass: 'read' },
3192
3652
  description: 'Get memory statistics.',
3193
3653
  category: 'workflow',
3194
3654
  inputSchema: z.object({}),