botinabox 2.5.0 → 2.5.2

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 (278) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +190 -190
  3. package/bin/botinabox.mjs +2 -2
  4. package/dist/channels/discord/adapter.d.ts +32 -0
  5. package/dist/channels/discord/adapter.js +70 -0
  6. package/dist/channels/discord/inbound.d.ts +25 -0
  7. package/dist/channels/discord/inbound.js +24 -0
  8. package/dist/channels/discord/models.d.ts +8 -0
  9. package/dist/channels/discord/models.js +5 -0
  10. package/dist/channels/discord/outbound.d.ts +14 -0
  11. package/dist/channels/discord/outbound.js +38 -0
  12. package/dist/channels/slack/adapter.d.ts +33 -0
  13. package/dist/channels/slack/adapter.js +74 -0
  14. package/dist/channels/slack/inbound.d.ts +59 -0
  15. package/dist/channels/slack/inbound.js +96 -0
  16. package/dist/channels/slack/models.d.ts +9 -0
  17. package/dist/channels/slack/models.js +5 -0
  18. package/dist/channels/slack/outbound.d.ts +12 -0
  19. package/dist/channels/slack/outbound.js +18 -0
  20. package/dist/channels/slack/transcribe.d.ts +41 -0
  21. package/dist/channels/slack/transcribe.js +106 -0
  22. package/dist/channels/webhook/adapter.d.ts +23 -0
  23. package/dist/channels/webhook/adapter.js +86 -0
  24. package/dist/channels/webhook/hmac.d.ts +13 -0
  25. package/dist/channels/webhook/hmac.js +26 -0
  26. package/dist/channels/webhook/models.d.ts +9 -0
  27. package/dist/channels/webhook/models.js +5 -0
  28. package/dist/channels/webhook/server.d.ts +20 -0
  29. package/dist/channels/webhook/server.js +91 -0
  30. package/dist/chat-pipeline-BWrtVqEP.d.ts +652 -0
  31. package/dist/chat-pipeline-C-XlLGNl.d.ts +648 -0
  32. package/dist/chat-pipeline-CR1KF6eX.d.ts +652 -0
  33. package/dist/chat-pipeline-DisuC8SB.d.ts +643 -0
  34. package/dist/chunk-2LGXQPEA.js +41 -0
  35. package/dist/chunk-3X3YKI4T.js +357 -0
  36. package/dist/chunk-D47AIFOD.js +351 -0
  37. package/dist/chunk-DSNJKNEW.js +328 -0
  38. package/dist/chunk-GS2JFL6I.js +144 -0
  39. package/dist/chunk-J6S6QMUY.js +144 -0
  40. package/dist/chunk-QLA6YOFN.js +22 -0
  41. package/dist/chunk-UACT2WXX.js +381 -0
  42. package/dist/cli/templates/config.yml.d.ts +7 -0
  43. package/dist/cli/templates/config.yml.js +61 -0
  44. package/dist/cli/templates/env.d.ts +1 -0
  45. package/dist/cli/templates/env.js +30 -0
  46. package/dist/cli/templates/index.ts.d.ts +2 -0
  47. package/dist/cli/templates/index.ts.js +30 -0
  48. package/dist/cli/templates/package.json.d.ts +5 -0
  49. package/dist/cli/templates/package.json.js +28 -0
  50. package/dist/cli.js +0 -0
  51. package/dist/connector-DDahQw-2.d.ts +63 -0
  52. package/dist/connectors/google/calendar-connector.d.ts +40 -0
  53. package/dist/connectors/google/calendar-connector.js +243 -0
  54. package/dist/connectors/google/gmail-connector.d.ts +42 -0
  55. package/dist/connectors/google/gmail-connector.js +345 -0
  56. package/dist/connectors/google/index.d.ts +67 -1
  57. package/dist/connectors/google/index.js +240 -0
  58. package/dist/connectors/google/oauth.d.ts +48 -0
  59. package/dist/connectors/google/oauth.js +112 -0
  60. package/dist/connectors/google/types.d.ts +78 -0
  61. package/dist/connectors/google/types.js +2 -0
  62. package/dist/core/chat/auto-discovery.d.ts +16 -0
  63. package/dist/core/chat/auto-discovery.js +54 -0
  64. package/dist/core/chat/channel-registry.d.ts +45 -0
  65. package/dist/core/chat/channel-registry.js +96 -0
  66. package/dist/core/chat/chat-pipeline.d.ts +113 -0
  67. package/dist/core/chat/chat-pipeline.js +395 -0
  68. package/dist/core/chat/chat-responder.d.ts +90 -0
  69. package/dist/core/chat/chat-responder.js +185 -0
  70. package/dist/core/chat/formatter.d.ts +11 -0
  71. package/dist/core/chat/formatter.js +60 -0
  72. package/dist/core/chat/index.d.ts +24 -0
  73. package/dist/core/chat/index.js +18 -0
  74. package/dist/core/chat/message-interpreter.d.ts +91 -0
  75. package/dist/core/chat/message-interpreter.js +166 -0
  76. package/dist/core/chat/message-store.d.ts +66 -0
  77. package/dist/core/chat/message-store.js +131 -0
  78. package/dist/core/chat/notification-queue.d.ts +34 -0
  79. package/dist/core/chat/notification-queue.js +111 -0
  80. package/dist/core/chat/pipeline.d.ts +38 -0
  81. package/dist/core/chat/pipeline.js +89 -0
  82. package/dist/core/chat/policies.d.ts +16 -0
  83. package/dist/core/chat/policies.js +25 -0
  84. package/dist/core/chat/routing.d.ts +17 -0
  85. package/dist/core/chat/routing.js +36 -0
  86. package/dist/core/chat/session-key.d.ts +30 -0
  87. package/dist/core/chat/session-key.js +65 -0
  88. package/dist/core/chat/session-manager.d.ts +17 -0
  89. package/dist/core/chat/session-manager.js +23 -0
  90. package/dist/core/chat/text-chunker.d.ts +9 -0
  91. package/dist/core/chat/text-chunker.js +48 -0
  92. package/dist/core/chat/triage-router.d.ts +75 -0
  93. package/dist/core/chat/triage-router.js +142 -0
  94. package/dist/core/chat/types.d.ts +5 -0
  95. package/dist/core/chat/types.js +5 -0
  96. package/dist/core/config/defaults.d.ts +2 -0
  97. package/dist/core/config/defaults.js +38 -0
  98. package/dist/core/config/index.d.ts +6 -0
  99. package/dist/core/config/index.js +4 -0
  100. package/dist/core/config/interpolate.d.ts +5 -0
  101. package/dist/core/config/interpolate.js +27 -0
  102. package/dist/core/config/loader.d.ts +24 -0
  103. package/dist/core/config/loader.js +59 -0
  104. package/dist/core/config/schema.d.ts +5 -0
  105. package/dist/core/config/schema.js +119 -0
  106. package/dist/core/data/core-entity-contexts.d.ts +14 -0
  107. package/dist/core/data/core-entity-contexts.js +197 -0
  108. package/dist/core/data/core-migrations.d.ts +5 -0
  109. package/dist/core/data/core-migrations.js +45 -0
  110. package/dist/core/data/core-schema.d.ts +6 -0
  111. package/dist/core/data/core-schema.js +454 -0
  112. package/dist/core/data/data-store.d.ts +67 -0
  113. package/dist/core/data/data-store.js +218 -0
  114. package/dist/core/data/domain-entity-contexts.d.ts +29 -0
  115. package/dist/core/data/domain-entity-contexts.js +321 -0
  116. package/dist/core/data/domain-schema.d.ts +36 -0
  117. package/dist/core/data/domain-schema.js +323 -0
  118. package/dist/core/data/index.d.ts +7 -0
  119. package/dist/core/data/index.js +7 -0
  120. package/dist/core/data/types.d.ts +111 -0
  121. package/dist/core/data/types.js +1 -0
  122. package/dist/core/hooks/hook-bus.d.ts +18 -0
  123. package/dist/core/hooks/hook-bus.js +120 -0
  124. package/dist/core/hooks/index.d.ts +2 -0
  125. package/dist/core/hooks/index.js +1 -0
  126. package/dist/core/hooks/types.d.ts +19 -0
  127. package/dist/core/hooks/types.js +1 -0
  128. package/dist/core/index.d.ts +4 -0
  129. package/dist/core/index.js +4 -0
  130. package/dist/core/llm/auto-discovery.d.ts +11 -0
  131. package/dist/core/llm/auto-discovery.js +49 -0
  132. package/dist/core/llm/cost-tracker.d.ts +6 -0
  133. package/dist/core/llm/cost-tracker.js +38 -0
  134. package/dist/core/llm/index.d.ts +4 -0
  135. package/dist/core/llm/index.js +3 -0
  136. package/dist/core/llm/model-router.d.ts +25 -0
  137. package/dist/core/llm/model-router.js +49 -0
  138. package/dist/core/llm/provider-registry.d.ts +9 -0
  139. package/dist/core/llm/provider-registry.js +25 -0
  140. package/dist/core/llm/types.d.ts +2 -0
  141. package/dist/core/llm/types.js +2 -0
  142. package/dist/core/orchestrator/adapters/api-adapter.d.ts +34 -0
  143. package/dist/core/orchestrator/adapters/api-adapter.js +88 -0
  144. package/dist/core/orchestrator/adapters/cli-adapter.d.ts +22 -0
  145. package/dist/core/orchestrator/adapters/cli-adapter.js +69 -0
  146. package/dist/core/orchestrator/adapters/deterministic-adapter.d.ts +35 -0
  147. package/dist/core/orchestrator/adapters/deterministic-adapter.js +75 -0
  148. package/dist/core/orchestrator/adapters/env-whitelist.d.ts +4 -0
  149. package/dist/core/orchestrator/adapters/env-whitelist.js +27 -0
  150. package/dist/core/orchestrator/adapters/output-extractor.d.ts +11 -0
  151. package/dist/core/orchestrator/adapters/output-extractor.js +59 -0
  152. package/dist/core/orchestrator/adapters/process-manager.d.ts +15 -0
  153. package/dist/core/orchestrator/adapters/process-manager.js +26 -0
  154. package/dist/core/orchestrator/adapters/tool-loop.d.ts +22 -0
  155. package/dist/core/orchestrator/adapters/tool-loop.js +66 -0
  156. package/dist/core/orchestrator/agent-registry.d.ts +31 -0
  157. package/dist/core/orchestrator/agent-registry.js +135 -0
  158. package/dist/core/orchestrator/budget-controller.d.ts +19 -0
  159. package/dist/core/orchestrator/budget-controller.js +73 -0
  160. package/dist/core/orchestrator/chain-guard.d.ts +14 -0
  161. package/dist/core/orchestrator/chain-guard.js +23 -0
  162. package/dist/core/orchestrator/circuit-breaker.d.ts +65 -0
  163. package/dist/core/orchestrator/circuit-breaker.js +159 -0
  164. package/dist/core/orchestrator/claude-stream-parser.d.ts +31 -0
  165. package/dist/core/orchestrator/claude-stream-parser.js +99 -0
  166. package/dist/core/orchestrator/config-revisions.d.ts +6 -0
  167. package/dist/core/orchestrator/config-revisions.js +17 -0
  168. package/dist/core/orchestrator/dependency-resolver.d.ts +20 -0
  169. package/dist/core/orchestrator/dependency-resolver.js +78 -0
  170. package/dist/core/orchestrator/governance-gate.d.ts +110 -0
  171. package/dist/core/orchestrator/governance-gate.js +170 -0
  172. package/dist/core/orchestrator/learning-pipeline.d.ts +109 -0
  173. package/dist/core/orchestrator/learning-pipeline.js +249 -0
  174. package/dist/core/orchestrator/loop-detector.d.ts +51 -0
  175. package/dist/core/orchestrator/loop-detector.js +133 -0
  176. package/dist/core/orchestrator/ndjson-logger.d.ts +6 -0
  177. package/dist/core/orchestrator/ndjson-logger.js +18 -0
  178. package/dist/core/orchestrator/permission-relay.d.ts +72 -0
  179. package/dist/core/orchestrator/permission-relay.js +164 -0
  180. package/dist/core/orchestrator/run-manager.d.ts +31 -0
  181. package/dist/core/orchestrator/run-manager.js +178 -0
  182. package/dist/core/orchestrator/scheduler.d.ts +70 -0
  183. package/dist/core/orchestrator/scheduler.js +198 -0
  184. package/dist/core/orchestrator/secret-store.d.ts +57 -0
  185. package/dist/core/orchestrator/secret-store.js +171 -0
  186. package/dist/core/orchestrator/session-manager.d.ts +13 -0
  187. package/dist/core/orchestrator/session-manager.js +66 -0
  188. package/dist/core/orchestrator/task-queue.d.ts +34 -0
  189. package/dist/core/orchestrator/task-queue.js +83 -0
  190. package/dist/core/orchestrator/template-interpolate.d.ts +5 -0
  191. package/dist/core/orchestrator/template-interpolate.js +18 -0
  192. package/dist/core/orchestrator/user-registry.d.ts +47 -0
  193. package/dist/core/orchestrator/user-registry.js +76 -0
  194. package/dist/core/orchestrator/wakeup-queue.d.ts +9 -0
  195. package/dist/core/orchestrator/wakeup-queue.js +45 -0
  196. package/dist/core/orchestrator/workflow-engine.d.ts +47 -0
  197. package/dist/core/orchestrator/workflow-engine.js +204 -0
  198. package/dist/core/security/audit.d.ts +20 -0
  199. package/dist/core/security/audit.js +33 -0
  200. package/dist/core/security/column-validator.d.ts +20 -0
  201. package/dist/core/security/column-validator.js +37 -0
  202. package/dist/core/security/index.d.ts +5 -0
  203. package/dist/core/security/index.js +5 -0
  204. package/dist/core/security/process-env.d.ts +13 -0
  205. package/dist/core/security/process-env.js +49 -0
  206. package/dist/core/security/sanitizer.d.ts +11 -0
  207. package/dist/core/security/sanitizer.js +39 -0
  208. package/dist/core/security/types.d.ts +11 -0
  209. package/dist/core/security/types.js +1 -0
  210. package/dist/core/update/auto-update.d.ts +21 -0
  211. package/dist/core/update/auto-update.js +102 -0
  212. package/dist/core/update/backup-manager.d.ts +7 -0
  213. package/dist/core/update/backup-manager.js +24 -0
  214. package/dist/core/update/index.d.ts +8 -0
  215. package/dist/core/update/index.js +6 -0
  216. package/dist/core/update/migration-hooks.d.ts +11 -0
  217. package/dist/core/update/migration-hooks.js +10 -0
  218. package/dist/core/update/types.d.ts +11 -0
  219. package/dist/core/update/types.js +1 -0
  220. package/dist/core/update/update-checker.d.ts +11 -0
  221. package/dist/core/update/update-checker.js +63 -0
  222. package/dist/core/update/update-manager.d.ts +25 -0
  223. package/dist/core/update/update-manager.js +101 -0
  224. package/dist/core/update/version-utils.d.ts +6 -0
  225. package/dist/core/update/version-utils.js +34 -0
  226. package/dist/gmail-connector-2FVYTQJH.js +6 -0
  227. package/dist/gmail-connector-MNUBRNFM.js +6 -0
  228. package/dist/gmail-connector-PS2VLGNE.js +6 -0
  229. package/dist/gmail-connector-ULSMN6X2.js +6 -0
  230. package/dist/gmail-connector-URRFX6A3.js +6 -0
  231. package/dist/inbound-AFBUPSPG.js +10 -0
  232. package/dist/inbound-AFOHYNUY.js +6 -0
  233. package/dist/inbound-CGIXRXGC.js +8 -0
  234. package/dist/inbound-MCOLRH6U.js +10 -0
  235. package/dist/inbound-SNEMBLGA.js +6 -0
  236. package/dist/inbound-ZJHAYVMF.js +10 -0
  237. package/dist/index.d.ts +11 -1
  238. package/dist/index.js +27 -11
  239. package/dist/provider-qqJYv9nv.d.ts +75 -0
  240. package/dist/providers/anthropic/models.d.ts +2 -0
  241. package/dist/providers/anthropic/models.js +29 -0
  242. package/dist/providers/anthropic/provider.d.ts +13 -0
  243. package/dist/providers/anthropic/provider.js +119 -0
  244. package/dist/providers/anthropic/tool-converter.d.ts +10 -0
  245. package/dist/providers/anthropic/tool-converter.js +7 -0
  246. package/dist/providers/ollama/provider.d.ts +17 -0
  247. package/dist/providers/ollama/provider.js +185 -0
  248. package/dist/providers/openai/models.d.ts +2 -0
  249. package/dist/providers/openai/models.js +29 -0
  250. package/dist/providers/openai/provider.d.ts +13 -0
  251. package/dist/providers/openai/provider.js +163 -0
  252. package/dist/providers/openai/tool-converter.d.ts +10 -0
  253. package/dist/providers/openai/tool-converter.js +10 -0
  254. package/dist/shared/constants.d.ts +50 -0
  255. package/dist/shared/constants.js +64 -0
  256. package/dist/shared/index.d.ts +14 -0
  257. package/dist/shared/index.js +14 -0
  258. package/dist/shared/types/agent.d.ts +36 -0
  259. package/dist/shared/types/agent.js +2 -0
  260. package/dist/shared/types/channel.d.ts +70 -0
  261. package/dist/shared/types/channel.js +2 -0
  262. package/dist/shared/types/config.d.ts +111 -0
  263. package/dist/shared/types/config.js +2 -0
  264. package/dist/shared/types/connector.d.ts +77 -0
  265. package/dist/shared/types/connector.js +2 -0
  266. package/dist/shared/types/execution.d.ts +29 -0
  267. package/dist/shared/types/execution.js +2 -0
  268. package/dist/shared/types/provider.d.ts +73 -0
  269. package/dist/shared/types/provider.js +2 -0
  270. package/dist/shared/types/task.d.ts +47 -0
  271. package/dist/shared/types/task.js +2 -0
  272. package/dist/shared/types/workflow.d.ts +39 -0
  273. package/dist/shared/types/workflow.js +2 -0
  274. package/dist/shared/utils.d.ts +6 -0
  275. package/dist/shared/utils.js +13 -0
  276. package/dist/update-check.d.ts +5 -0
  277. package/dist/update-check.js +56 -0
  278. package/package.json +100 -100
@@ -0,0 +1,249 @@
1
+ /**
2
+ * LearningPipeline — turns execution experience into durable knowledge.
3
+ * Story 6.5
4
+ *
5
+ * Promotion ladder:
6
+ * Execution → Feedback (structured capture)
7
+ * → 3+ similar → Playbook (generalized rule)
8
+ * → 3+ projects → Skill (executable behavior)
9
+ * → Agent-Skill Matrix → Per-Agent Context
10
+ *
11
+ * Two-axis evaluation:
12
+ * - Accuracy: was the output correct?
13
+ * - Efficiency: how fast / how many tokens?
14
+ */
15
+ const DEFAULT_PLAYBOOK_THRESHOLD = 3;
16
+ const DEFAULT_SKILL_THRESHOLD = 3;
17
+ export class LearningPipeline {
18
+ db;
19
+ hooks;
20
+ playbookThreshold;
21
+ skillThreshold;
22
+ autoPromote;
23
+ constructor(db, hooks, config) {
24
+ this.db = db;
25
+ this.hooks = hooks;
26
+ this.playbookThreshold = config?.playbookThreshold ?? DEFAULT_PLAYBOOK_THRESHOLD;
27
+ this.skillThreshold = config?.skillThreshold ?? DEFAULT_SKILL_THRESHOLD;
28
+ this.autoPromote = config?.autoPromote ?? false;
29
+ }
30
+ // --- Feedback Layer ---
31
+ /**
32
+ * Capture a structured feedback record from an execution.
33
+ */
34
+ async captureFeedback(entry) {
35
+ const row = await this.db.insert('feedback', {
36
+ agent_id: entry.agentId,
37
+ task_id: entry.taskId,
38
+ issue: entry.issue,
39
+ root_cause: entry.rootCause,
40
+ severity: entry.severity,
41
+ repeatable: entry.repeatable ? 1 : 0,
42
+ accuracy_score: entry.accuracyScore,
43
+ efficiency_score: entry.efficiencyScore,
44
+ tags: JSON.stringify(entry.tags ?? []),
45
+ });
46
+ const feedbackId = row['id'];
47
+ await this.hooks.emit('learning.feedback_captured', {
48
+ feedbackId,
49
+ agentId: entry.agentId,
50
+ issue: entry.issue,
51
+ severity: entry.severity,
52
+ });
53
+ // Check for auto-promotion
54
+ if (this.autoPromote) {
55
+ await this.checkPlaybookPromotion(entry.issue);
56
+ }
57
+ return feedbackId;
58
+ }
59
+ /**
60
+ * Get all feedback records, optionally filtered.
61
+ */
62
+ async listFeedback(filter) {
63
+ const where = {};
64
+ if (filter?.agentId)
65
+ where['agent_id'] = filter.agentId;
66
+ if (filter?.severity)
67
+ where['severity'] = filter.severity;
68
+ if (filter?.repeatable !== undefined)
69
+ where['repeatable'] = filter.repeatable ? 1 : 0;
70
+ return this.db.query('feedback', Object.keys(where).length ? { where } : undefined);
71
+ }
72
+ // --- Playbook Layer ---
73
+ /**
74
+ * Check if feedback records with similar issues should be promoted to a playbook.
75
+ * Groups by issue text similarity (exact match for now).
76
+ */
77
+ async checkPlaybookPromotion(issue) {
78
+ const allFeedback = await this.db.query('feedback', {
79
+ where: { issue },
80
+ });
81
+ if (allFeedback.length < this.playbookThreshold) {
82
+ return undefined;
83
+ }
84
+ // Check if a playbook already exists for this pattern
85
+ const existingPlaybooks = await this.db.query('playbooks', {
86
+ where: { pattern: issue },
87
+ });
88
+ if (existingPlaybooks.length > 0) {
89
+ return existingPlaybooks[0]['id'];
90
+ }
91
+ // Auto-promote: create a playbook from the feedback
92
+ const feedbackIds = allFeedback.map((f) => f['id']);
93
+ const rootCauses = allFeedback
94
+ .map((f) => f['root_cause'])
95
+ .filter(Boolean);
96
+ const rule = rootCauses.length > 0
97
+ ? `When encountering "${issue}": ${rootCauses[0]}`
98
+ : `Pattern detected: "${issue}" — review and add specific guidance.`;
99
+ const playbookId = await this.promoteToPlaybook({
100
+ pattern: issue,
101
+ rule,
102
+ feedbackIds,
103
+ projectScoped: true,
104
+ });
105
+ return playbookId;
106
+ }
107
+ /**
108
+ * Manually create a playbook from a set of feedback records.
109
+ */
110
+ async promoteToPlaybook(entry) {
111
+ const row = await this.db.insert('playbooks', {
112
+ pattern: entry.pattern,
113
+ rule: entry.rule,
114
+ feedback_ids: JSON.stringify(entry.feedbackIds),
115
+ project_scoped: entry.projectScoped ? 1 : 0,
116
+ });
117
+ const playbookId = row['id'];
118
+ // Link to agents if specified
119
+ if (entry.agentIds) {
120
+ for (const agentId of entry.agentIds) {
121
+ await this.db.insert('agent_playbooks', {
122
+ agent_id: agentId,
123
+ playbook_id: playbookId,
124
+ });
125
+ }
126
+ }
127
+ await this.hooks.emit('learning.playbook_promoted', {
128
+ playbookId,
129
+ pattern: entry.pattern,
130
+ feedbackCount: entry.feedbackIds.length,
131
+ });
132
+ return playbookId;
133
+ }
134
+ /**
135
+ * List playbooks, optionally filtered.
136
+ */
137
+ async listPlaybooks(filter) {
138
+ const where = {};
139
+ if (filter?.projectScoped !== undefined) {
140
+ where['project_scoped'] = filter.projectScoped ? 1 : 0;
141
+ }
142
+ return this.db.query('playbooks', Object.keys(where).length ? { where } : undefined);
143
+ }
144
+ // --- Skill Layer ---
145
+ /**
146
+ * Check if a playbook should be promoted to a skill.
147
+ * A playbook becomes a skill when it works across multiple projects
148
+ * (indicated by being referenced by agents in different contexts).
149
+ */
150
+ async checkSkillPromotion(playbookId) {
151
+ const playbook = await this.db.get('playbooks', { id: playbookId });
152
+ if (!playbook)
153
+ return undefined;
154
+ // Count distinct agents using this playbook
155
+ const links = await this.db.query('agent_playbooks', {
156
+ where: { playbook_id: playbookId },
157
+ });
158
+ if (links.length < this.skillThreshold) {
159
+ return undefined;
160
+ }
161
+ // Check if skill already exists for this pattern
162
+ const pattern = playbook['pattern'];
163
+ const existingSkills = await this.db.query('skills', {
164
+ where: { name: pattern },
165
+ });
166
+ if (existingSkills.length > 0) {
167
+ return existingSkills[0]['id'];
168
+ }
169
+ // Promote to skill
170
+ const slug = pattern
171
+ .toLowerCase()
172
+ .replace(/[^a-z0-9]+/g, '-')
173
+ .replace(/^-|-$/g, '')
174
+ .slice(0, 64);
175
+ const skillId = await this.promoteToSkill({
176
+ name: pattern,
177
+ slug,
178
+ description: `Auto-promoted from playbook: ${pattern}`,
179
+ behavior: playbook['rule'],
180
+ sourcePlaybookIds: [playbookId],
181
+ });
182
+ return skillId;
183
+ }
184
+ /**
185
+ * Manually promote a playbook to a reusable skill.
186
+ */
187
+ async promoteToSkill(entry) {
188
+ const row = await this.db.insert('skills', {
189
+ name: entry.name,
190
+ slug: entry.slug,
191
+ description: entry.description,
192
+ category: entry.category ?? 'learned',
193
+ definition: JSON.stringify({
194
+ behavior: entry.behavior,
195
+ source_playbook_ids: entry.sourcePlaybookIds,
196
+ }),
197
+ });
198
+ const skillId = row['id'];
199
+ await this.hooks.emit('learning.skill_promoted', {
200
+ skillId,
201
+ name: entry.name,
202
+ slug: entry.slug,
203
+ sourcePlaybookCount: entry.sourcePlaybookIds.length,
204
+ });
205
+ return skillId;
206
+ }
207
+ /**
208
+ * Assign a skill to an agent.
209
+ */
210
+ async assignSkill(agentId, skillId) {
211
+ await this.db.link('agent_skills', {
212
+ agent_id: agentId,
213
+ skill_id: skillId,
214
+ });
215
+ await this.hooks.emit('learning.skill_assigned', { agentId, skillId });
216
+ }
217
+ /**
218
+ * Get learning metrics for an agent.
219
+ */
220
+ async getMetrics(agentId) {
221
+ const feedback = await this.db.query('feedback', { where: { agent_id: agentId } });
222
+ const accuracyScores = feedback
223
+ .map((f) => f['accuracy_score'])
224
+ .filter((s) => s !== null && s !== undefined);
225
+ const efficiencyScores = feedback
226
+ .map((f) => f['efficiency_score'])
227
+ .filter((s) => s !== null && s !== undefined);
228
+ let playbookCount = 0;
229
+ try {
230
+ const links = await this.db.query('agent_playbooks', { where: { agent_id: agentId } });
231
+ playbookCount = links.length;
232
+ }
233
+ catch {
234
+ // Table may not exist
235
+ }
236
+ const skillLinks = await this.db.query('agent_skills', { where: { agent_id: agentId } });
237
+ return {
238
+ feedbackCount: feedback.length,
239
+ avgAccuracy: accuracyScores.length > 0
240
+ ? accuracyScores.reduce((a, b) => a + b, 0) / accuracyScores.length
241
+ : null,
242
+ avgEfficiency: efficiencyScores.length > 0
243
+ ? efficiencyScores.reduce((a, b) => a + b, 0) / efficiencyScores.length
244
+ : null,
245
+ playbookCount,
246
+ skillCount: skillLinks.length,
247
+ };
248
+ }
249
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * LoopDetector — pattern-based loop detection for agent routing.
3
+ * Story 6.2
4
+ *
5
+ * Complements chain-guard's depth limit with active pattern detection:
6
+ * - Self-loop: agent routes a task back to itself
7
+ * - Ping-pong: two agents bounce tasks between each other (A→B→A→B)
8
+ * - Blocked re-entry: a task re-enters the system after being blocked
9
+ */
10
+ import type { DataStore } from '../data/data-store.js';
11
+ export declare enum LoopType {
12
+ SELF_LOOP = "self_loop",
13
+ PING_PONG = "ping_pong",
14
+ BLOCKED_REENTRY = "blocked_reentry"
15
+ }
16
+ export interface LoopDetection {
17
+ type: LoopType;
18
+ agents: string[];
19
+ taskId: string;
20
+ chainOriginId?: string;
21
+ message: string;
22
+ }
23
+ export interface LoopDetectorConfig {
24
+ /** Number of recent followup records to scan. Default: 10 */
25
+ windowSize?: number;
26
+ /** Minimum repetitions to confirm ping-pong. Default: 2 */
27
+ pingPongThreshold?: number;
28
+ }
29
+ export declare class LoopDetector {
30
+ private db;
31
+ private readonly windowSize;
32
+ private readonly pingPongThreshold;
33
+ constructor(db: DataStore, config?: LoopDetectorConfig);
34
+ /**
35
+ * Check for loops before creating a followup task.
36
+ * Returns a LoopDetection if a loop pattern is found, undefined otherwise.
37
+ */
38
+ check(sourceAgentId: string, targetAgentId: string, taskId: string, chainOriginId?: string): Promise<LoopDetection | undefined>;
39
+ /**
40
+ * Check if an agent is routing to itself.
41
+ */
42
+ private checkSelfLoop;
43
+ /**
44
+ * Check if a previously blocked task is being re-entered.
45
+ */
46
+ private checkBlockedReentry;
47
+ /**
48
+ * Check for A→B→A→B ping-pong by scanning recent tasks in the chain.
49
+ */
50
+ private checkPingPong;
51
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * LoopDetector — pattern-based loop detection for agent routing.
3
+ * Story 6.2
4
+ *
5
+ * Complements chain-guard's depth limit with active pattern detection:
6
+ * - Self-loop: agent routes a task back to itself
7
+ * - Ping-pong: two agents bounce tasks between each other (A→B→A→B)
8
+ * - Blocked re-entry: a task re-enters the system after being blocked
9
+ */
10
+ export var LoopType;
11
+ (function (LoopType) {
12
+ LoopType["SELF_LOOP"] = "self_loop";
13
+ LoopType["PING_PONG"] = "ping_pong";
14
+ LoopType["BLOCKED_REENTRY"] = "blocked_reentry";
15
+ })(LoopType || (LoopType = {}));
16
+ const DEFAULT_WINDOW = 10;
17
+ const DEFAULT_PING_PONG_THRESHOLD = 2;
18
+ export class LoopDetector {
19
+ db;
20
+ windowSize;
21
+ pingPongThreshold;
22
+ constructor(db, config) {
23
+ this.db = db;
24
+ this.windowSize = config?.windowSize ?? DEFAULT_WINDOW;
25
+ this.pingPongThreshold = config?.pingPongThreshold ?? DEFAULT_PING_PONG_THRESHOLD;
26
+ }
27
+ /**
28
+ * Check for loops before creating a followup task.
29
+ * Returns a LoopDetection if a loop pattern is found, undefined otherwise.
30
+ */
31
+ async check(sourceAgentId, targetAgentId, taskId, chainOriginId) {
32
+ // 1. Self-loop: source == target
33
+ const selfLoop = this.checkSelfLoop(sourceAgentId, targetAgentId, taskId);
34
+ if (selfLoop)
35
+ return selfLoop;
36
+ // 2. Blocked re-entry
37
+ const blocked = await this.checkBlockedReentry(targetAgentId, taskId, chainOriginId);
38
+ if (blocked)
39
+ return blocked;
40
+ // 3. Ping-pong: look at recent chain history
41
+ const pingPong = await this.checkPingPong(sourceAgentId, targetAgentId, chainOriginId);
42
+ if (pingPong)
43
+ return pingPong;
44
+ return undefined;
45
+ }
46
+ /**
47
+ * Check if an agent is routing to itself.
48
+ */
49
+ checkSelfLoop(sourceAgentId, targetAgentId, taskId) {
50
+ if (sourceAgentId === targetAgentId) {
51
+ return {
52
+ type: LoopType.SELF_LOOP,
53
+ agents: [sourceAgentId],
54
+ taskId,
55
+ message: `Self-loop detected: agent ${sourceAgentId} is routing to itself`,
56
+ };
57
+ }
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Check if a previously blocked task is being re-entered.
62
+ */
63
+ async checkBlockedReentry(targetAgentId, taskId, chainOriginId) {
64
+ // Look for blocked/failed tasks in the same chain
65
+ const originId = chainOriginId ?? taskId;
66
+ const chainTasks = await this.db.query('tasks', {
67
+ where: { chain_origin_id: originId },
68
+ });
69
+ const blockedInChain = chainTasks.filter((t) => (t['status'] === 'blocked' || t['status'] === 'failed') &&
70
+ t['assignee_id'] === targetAgentId);
71
+ if (blockedInChain.length > 0) {
72
+ return {
73
+ type: LoopType.BLOCKED_REENTRY,
74
+ agents: [targetAgentId],
75
+ taskId,
76
+ chainOriginId: originId,
77
+ message: `Blocked re-entry: agent ${targetAgentId} already has a blocked/failed task in chain ${originId}`,
78
+ };
79
+ }
80
+ return undefined;
81
+ }
82
+ /**
83
+ * Check for A→B→A→B ping-pong by scanning recent tasks in the chain.
84
+ */
85
+ async checkPingPong(sourceAgentId, targetAgentId, chainOriginId) {
86
+ if (!chainOriginId)
87
+ return undefined;
88
+ // Get recent tasks in this chain, ordered by creation
89
+ const chainTasks = await this.db.query('tasks', {
90
+ where: { chain_origin_id: chainOriginId },
91
+ });
92
+ // Sort by chain_depth (ascending) then created_at
93
+ const sorted = chainTasks
94
+ .sort((a, b) => {
95
+ const depthDiff = (a['chain_depth'] ?? 0) - (b['chain_depth'] ?? 0);
96
+ if (depthDiff !== 0)
97
+ return depthDiff;
98
+ return (a['created_at'] ?? '').localeCompare(b['created_at'] ?? '');
99
+ })
100
+ .slice(-this.windowSize);
101
+ // Extract the agent sequence
102
+ const agentSequence = sorted
103
+ .map((t) => t['assignee_id'])
104
+ .filter(Boolean);
105
+ // Add the proposed next hop
106
+ agentSequence.push(targetAgentId);
107
+ // Detect A→B→A→B pattern
108
+ if (agentSequence.length >= this.pingPongThreshold * 2) {
109
+ const tail = agentSequence.slice(-this.pingPongThreshold * 2);
110
+ const a = tail[0];
111
+ const b = tail[1];
112
+ if (a && b && a !== b) {
113
+ let isPingPong = true;
114
+ for (let i = 0; i < tail.length; i++) {
115
+ if (tail[i] !== (i % 2 === 0 ? a : b)) {
116
+ isPingPong = false;
117
+ break;
118
+ }
119
+ }
120
+ if (isPingPong) {
121
+ return {
122
+ type: LoopType.PING_PONG,
123
+ agents: [a, b],
124
+ taskId: sorted[sorted.length - 1]?.['id'] ?? '',
125
+ chainOriginId,
126
+ message: `Ping-pong detected: agents ${a} and ${b} are bouncing tasks in chain ${chainOriginId}`,
127
+ };
128
+ }
129
+ }
130
+ }
131
+ return undefined;
132
+ }
133
+ }
@@ -0,0 +1,6 @@
1
+ export declare class NdjsonLogger {
2
+ private logPath;
3
+ constructor(logPath: string);
4
+ log(stream: 'stdout' | 'stderr', chunk: string): void;
5
+ close(): void;
6
+ }
@@ -0,0 +1,18 @@
1
+ import { appendFileSync } from 'node:fs';
2
+ export class NdjsonLogger {
3
+ logPath;
4
+ constructor(logPath) {
5
+ this.logPath = logPath;
6
+ }
7
+ log(stream, chunk) {
8
+ const line = JSON.stringify({
9
+ timestamp: new Date().toISOString(),
10
+ stream,
11
+ chunk,
12
+ });
13
+ appendFileSync(this.logPath, line + '\n', 'utf8');
14
+ }
15
+ close() {
16
+ // No-op — synchronous writes flush immediately
17
+ }
18
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * PermissionRelay — remote approval for unattended agent execution.
3
+ * Story 6.6
4
+ *
5
+ * When an agent needs human approval but the operator is away:
6
+ * 1. Post the approval prompt to a messaging platform (Slack, Discord, etc.)
7
+ * 2. Poll for response (approve/deny)
8
+ * 3. Relay the decision back to the agent
9
+ *
10
+ * Dual approval: local terminal + remote messaging. First response wins.
11
+ * Race condition handled by atomic state transition.
12
+ */
13
+ import type { HookBus } from '../hooks/hook-bus.js';
14
+ export type ApprovalStatus = 'pending' | 'approved' | 'denied' | 'expired';
15
+ export interface PermissionPrompt {
16
+ id: string;
17
+ agentId: string;
18
+ action: string;
19
+ context?: string;
20
+ requestedAt: string;
21
+ expiresAt?: string;
22
+ }
23
+ export interface ApprovalResponse {
24
+ promptId: string;
25
+ status: 'approved' | 'denied';
26
+ respondedBy: string;
27
+ respondedAt: string;
28
+ comment?: string;
29
+ }
30
+ /**
31
+ * Provider interface — implement for each messaging platform.
32
+ */
33
+ export interface PermissionProvider {
34
+ readonly id: string;
35
+ /** Post an approval request, return a handle for polling. */
36
+ sendPrompt(prompt: PermissionPrompt): Promise<string>;
37
+ /** Check for a response. Returns undefined if still pending. */
38
+ pollResponse(handle: string): Promise<ApprovalResponse | undefined>;
39
+ /** Cancel a pending prompt (e.g. after local approval). */
40
+ cancelPrompt(handle: string): Promise<void>;
41
+ }
42
+ export interface PermissionRelayConfig {
43
+ /** Registered providers (e.g. Slack, Discord adapters) */
44
+ providers: PermissionProvider[];
45
+ /** Poll interval in ms. Default: 5000 */
46
+ pollIntervalMs?: number;
47
+ /** Timeout for pending approvals in ms. Default: 300_000 (5 min) */
48
+ timeoutMs?: number;
49
+ }
50
+ export declare class PermissionRelay {
51
+ private hooks;
52
+ private readonly providers;
53
+ private readonly pollIntervalMs;
54
+ private readonly timeoutMs;
55
+ private readonly pending;
56
+ constructor(hooks: HookBus, config: PermissionRelayConfig);
57
+ /**
58
+ * Request approval from all configured providers.
59
+ * Returns when the first provider responds (approve or deny).
60
+ */
61
+ requestApproval(prompt: PermissionPrompt): Promise<ApprovalResponse>;
62
+ /**
63
+ * Provide a local approval (from terminal).
64
+ * Resolves the pending request and cancels remote providers.
65
+ */
66
+ approveLocally(promptId: string, approved: boolean): Promise<void>;
67
+ /**
68
+ * Get all pending approval requests.
69
+ */
70
+ getPending(): PermissionPrompt[];
71
+ private cancelOtherProviders;
72
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * PermissionRelay — remote approval for unattended agent execution.
3
+ * Story 6.6
4
+ *
5
+ * When an agent needs human approval but the operator is away:
6
+ * 1. Post the approval prompt to a messaging platform (Slack, Discord, etc.)
7
+ * 2. Poll for response (approve/deny)
8
+ * 3. Relay the decision back to the agent
9
+ *
10
+ * Dual approval: local terminal + remote messaging. First response wins.
11
+ * Race condition handled by atomic state transition.
12
+ */
13
+ const DEFAULT_POLL_INTERVAL_MS = 5_000;
14
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1_000;
15
+ export class PermissionRelay {
16
+ hooks;
17
+ providers;
18
+ pollIntervalMs;
19
+ timeoutMs;
20
+ pending = new Map();
21
+ constructor(hooks, config) {
22
+ this.hooks = hooks;
23
+ this.providers = config.providers;
24
+ this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
25
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
26
+ }
27
+ /**
28
+ * Request approval from all configured providers.
29
+ * Returns when the first provider responds (approve or deny).
30
+ */
31
+ async requestApproval(prompt) {
32
+ // Set expiry
33
+ const expiresAt = new Date(Date.now() + this.timeoutMs).toISOString();
34
+ const promptWithExpiry = { ...prompt, expiresAt };
35
+ await this.hooks.emit('permission.requested', {
36
+ promptId: prompt.id,
37
+ agentId: prompt.agentId,
38
+ action: prompt.action,
39
+ });
40
+ // Send to all providers
41
+ const handles = new Map();
42
+ for (const provider of this.providers) {
43
+ try {
44
+ const handle = await provider.sendPrompt(promptWithExpiry);
45
+ handles.set(provider.id, handle);
46
+ }
47
+ catch {
48
+ // Provider unavailable — continue with others
49
+ }
50
+ }
51
+ if (handles.size === 0) {
52
+ throw new Error('No permission providers available');
53
+ }
54
+ // Race: poll all providers, first response wins
55
+ return new Promise((resolve, reject) => {
56
+ const entry = {
57
+ prompt: promptWithExpiry,
58
+ handles,
59
+ resolve,
60
+ reject,
61
+ };
62
+ this.pending.set(prompt.id, entry);
63
+ // Start polling
64
+ const pollTimer = setInterval(async () => {
65
+ for (const [providerId, handle] of handles) {
66
+ const provider = this.providers.find((p) => p.id === providerId);
67
+ if (!provider)
68
+ continue;
69
+ try {
70
+ const response = await provider.pollResponse(handle);
71
+ if (response) {
72
+ clearInterval(pollTimer);
73
+ clearTimeout(timeoutTimer);
74
+ this.pending.delete(prompt.id);
75
+ // Cancel remaining providers
76
+ await this.cancelOtherProviders(handles, providerId);
77
+ await this.hooks.emit('permission.responded', {
78
+ promptId: prompt.id,
79
+ status: response.status,
80
+ respondedBy: response.respondedBy,
81
+ });
82
+ resolve(response);
83
+ return;
84
+ }
85
+ }
86
+ catch {
87
+ // Poll error — continue
88
+ }
89
+ }
90
+ }, this.pollIntervalMs);
91
+ // Timeout
92
+ const timeoutTimer = setTimeout(async () => {
93
+ clearInterval(pollTimer);
94
+ this.pending.delete(prompt.id);
95
+ // Cancel all providers
96
+ for (const [providerId, handle] of handles) {
97
+ const provider = this.providers.find((p) => p.id === providerId);
98
+ if (provider) {
99
+ try {
100
+ await provider.cancelPrompt(handle);
101
+ }
102
+ catch { /* ignore */ }
103
+ }
104
+ }
105
+ await this.hooks.emit('permission.expired', {
106
+ promptId: prompt.id,
107
+ agentId: prompt.agentId,
108
+ });
109
+ reject(new Error(`Permission request expired after ${this.timeoutMs}ms`));
110
+ }, this.timeoutMs);
111
+ });
112
+ }
113
+ /**
114
+ * Provide a local approval (from terminal).
115
+ * Resolves the pending request and cancels remote providers.
116
+ */
117
+ async approveLocally(promptId, approved) {
118
+ const entry = this.pending.get(promptId);
119
+ if (!entry)
120
+ return;
121
+ const response = {
122
+ promptId,
123
+ status: approved ? 'approved' : 'denied',
124
+ respondedBy: 'local',
125
+ respondedAt: new Date().toISOString(),
126
+ };
127
+ this.pending.delete(promptId);
128
+ // Cancel all remote providers
129
+ for (const [providerId, handle] of entry.handles) {
130
+ const provider = this.providers.find((p) => p.id === providerId);
131
+ if (provider) {
132
+ try {
133
+ await provider.cancelPrompt(handle);
134
+ }
135
+ catch { /* ignore */ }
136
+ }
137
+ }
138
+ await this.hooks.emit('permission.responded', {
139
+ promptId,
140
+ status: response.status,
141
+ respondedBy: 'local',
142
+ });
143
+ entry.resolve(response);
144
+ }
145
+ /**
146
+ * Get all pending approval requests.
147
+ */
148
+ getPending() {
149
+ return Array.from(this.pending.values()).map((e) => e.prompt);
150
+ }
151
+ async cancelOtherProviders(handles, excludeProviderId) {
152
+ for (const [providerId, handle] of handles) {
153
+ if (providerId === excludeProviderId)
154
+ continue;
155
+ const provider = this.providers.find((p) => p.id === providerId);
156
+ if (provider) {
157
+ try {
158
+ await provider.cancelPrompt(handle);
159
+ }
160
+ catch { /* ignore */ }
161
+ }
162
+ }
163
+ }
164
+ }