@renseiai/agentfactory 0.8.0

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 (246) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/src/config/index.d.ts +3 -0
  4. package/dist/src/config/index.d.ts.map +1 -0
  5. package/dist/src/config/index.js +1 -0
  6. package/dist/src/config/repository-config.d.ts +44 -0
  7. package/dist/src/config/repository-config.d.ts.map +1 -0
  8. package/dist/src/config/repository-config.js +88 -0
  9. package/dist/src/config/repository-config.test.d.ts +2 -0
  10. package/dist/src/config/repository-config.test.d.ts.map +1 -0
  11. package/dist/src/config/repository-config.test.js +249 -0
  12. package/dist/src/deployment/deployment-checker.d.ts +110 -0
  13. package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
  14. package/dist/src/deployment/deployment-checker.js +242 -0
  15. package/dist/src/deployment/index.d.ts +3 -0
  16. package/dist/src/deployment/index.d.ts.map +1 -0
  17. package/dist/src/deployment/index.js +2 -0
  18. package/dist/src/frontend/index.d.ts +2 -0
  19. package/dist/src/frontend/index.d.ts.map +1 -0
  20. package/dist/src/frontend/index.js +1 -0
  21. package/dist/src/frontend/types.d.ts +106 -0
  22. package/dist/src/frontend/types.d.ts.map +1 -0
  23. package/dist/src/frontend/types.js +11 -0
  24. package/dist/src/governor/decision-engine.d.ts +52 -0
  25. package/dist/src/governor/decision-engine.d.ts.map +1 -0
  26. package/dist/src/governor/decision-engine.js +220 -0
  27. package/dist/src/governor/decision-engine.test.d.ts +2 -0
  28. package/dist/src/governor/decision-engine.test.d.ts.map +1 -0
  29. package/dist/src/governor/decision-engine.test.js +629 -0
  30. package/dist/src/governor/event-bus.d.ts +43 -0
  31. package/dist/src/governor/event-bus.d.ts.map +1 -0
  32. package/dist/src/governor/event-bus.js +8 -0
  33. package/dist/src/governor/event-deduplicator.d.ts +43 -0
  34. package/dist/src/governor/event-deduplicator.d.ts.map +1 -0
  35. package/dist/src/governor/event-deduplicator.js +53 -0
  36. package/dist/src/governor/event-driven-governor.d.ts +131 -0
  37. package/dist/src/governor/event-driven-governor.d.ts.map +1 -0
  38. package/dist/src/governor/event-driven-governor.js +379 -0
  39. package/dist/src/governor/event-driven-governor.test.d.ts +2 -0
  40. package/dist/src/governor/event-driven-governor.test.d.ts.map +1 -0
  41. package/dist/src/governor/event-driven-governor.test.js +673 -0
  42. package/dist/src/governor/event-types.d.ts +78 -0
  43. package/dist/src/governor/event-types.d.ts.map +1 -0
  44. package/dist/src/governor/event-types.js +32 -0
  45. package/dist/src/governor/governor-types.d.ts +82 -0
  46. package/dist/src/governor/governor-types.d.ts.map +1 -0
  47. package/dist/src/governor/governor-types.js +21 -0
  48. package/dist/src/governor/governor.d.ts +100 -0
  49. package/dist/src/governor/governor.d.ts.map +1 -0
  50. package/dist/src/governor/governor.js +262 -0
  51. package/dist/src/governor/governor.test.d.ts +2 -0
  52. package/dist/src/governor/governor.test.d.ts.map +1 -0
  53. package/dist/src/governor/governor.test.js +514 -0
  54. package/dist/src/governor/human-touchpoints.d.ts +131 -0
  55. package/dist/src/governor/human-touchpoints.d.ts.map +1 -0
  56. package/dist/src/governor/human-touchpoints.js +251 -0
  57. package/dist/src/governor/human-touchpoints.test.d.ts +2 -0
  58. package/dist/src/governor/human-touchpoints.test.d.ts.map +1 -0
  59. package/dist/src/governor/human-touchpoints.test.js +366 -0
  60. package/dist/src/governor/in-memory-event-bus.d.ts +29 -0
  61. package/dist/src/governor/in-memory-event-bus.d.ts.map +1 -0
  62. package/dist/src/governor/in-memory-event-bus.js +79 -0
  63. package/dist/src/governor/index.d.ts +14 -0
  64. package/dist/src/governor/index.d.ts.map +1 -0
  65. package/dist/src/governor/index.js +13 -0
  66. package/dist/src/governor/override-parser.d.ts +60 -0
  67. package/dist/src/governor/override-parser.d.ts.map +1 -0
  68. package/dist/src/governor/override-parser.js +98 -0
  69. package/dist/src/governor/override-parser.test.d.ts +2 -0
  70. package/dist/src/governor/override-parser.test.d.ts.map +1 -0
  71. package/dist/src/governor/override-parser.test.js +312 -0
  72. package/dist/src/governor/platform-adapter.d.ts +69 -0
  73. package/dist/src/governor/platform-adapter.d.ts.map +1 -0
  74. package/dist/src/governor/platform-adapter.js +11 -0
  75. package/dist/src/governor/processing-state.d.ts +66 -0
  76. package/dist/src/governor/processing-state.d.ts.map +1 -0
  77. package/dist/src/governor/processing-state.js +43 -0
  78. package/dist/src/governor/processing-state.test.d.ts +2 -0
  79. package/dist/src/governor/processing-state.test.d.ts.map +1 -0
  80. package/dist/src/governor/processing-state.test.js +96 -0
  81. package/dist/src/governor/top-of-funnel.d.ts +118 -0
  82. package/dist/src/governor/top-of-funnel.d.ts.map +1 -0
  83. package/dist/src/governor/top-of-funnel.js +168 -0
  84. package/dist/src/governor/top-of-funnel.test.d.ts +2 -0
  85. package/dist/src/governor/top-of-funnel.test.d.ts.map +1 -0
  86. package/dist/src/governor/top-of-funnel.test.js +331 -0
  87. package/dist/src/index.d.ts +11 -0
  88. package/dist/src/index.d.ts.map +1 -0
  89. package/dist/src/index.js +10 -0
  90. package/dist/src/linear-cli.d.ts +38 -0
  91. package/dist/src/linear-cli.d.ts.map +1 -0
  92. package/dist/src/linear-cli.js +674 -0
  93. package/dist/src/logger.d.ts +117 -0
  94. package/dist/src/logger.d.ts.map +1 -0
  95. package/dist/src/logger.js +430 -0
  96. package/dist/src/manifest/generate.d.ts +20 -0
  97. package/dist/src/manifest/generate.d.ts.map +1 -0
  98. package/dist/src/manifest/generate.js +65 -0
  99. package/dist/src/manifest/index.d.ts +4 -0
  100. package/dist/src/manifest/index.d.ts.map +1 -0
  101. package/dist/src/manifest/index.js +2 -0
  102. package/dist/src/manifest/route-manifest.d.ts +34 -0
  103. package/dist/src/manifest/route-manifest.d.ts.map +1 -0
  104. package/dist/src/manifest/route-manifest.js +148 -0
  105. package/dist/src/orchestrator/activity-emitter.d.ts +119 -0
  106. package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
  107. package/dist/src/orchestrator/activity-emitter.js +306 -0
  108. package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
  109. package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
  110. package/dist/src/orchestrator/api-activity-emitter.js +417 -0
  111. package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
  112. package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
  113. package/dist/src/orchestrator/heartbeat-writer.js +137 -0
  114. package/dist/src/orchestrator/index.d.ts +20 -0
  115. package/dist/src/orchestrator/index.d.ts.map +1 -0
  116. package/dist/src/orchestrator/index.js +22 -0
  117. package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
  118. package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
  119. package/dist/src/orchestrator/log-analyzer.js +572 -0
  120. package/dist/src/orchestrator/log-config.d.ts +39 -0
  121. package/dist/src/orchestrator/log-config.d.ts.map +1 -0
  122. package/dist/src/orchestrator/log-config.js +45 -0
  123. package/dist/src/orchestrator/orchestrator.d.ts +316 -0
  124. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
  125. package/dist/src/orchestrator/orchestrator.js +3290 -0
  126. package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
  127. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
  128. package/dist/src/orchestrator/parse-work-result.js +135 -0
  129. package/dist/src/orchestrator/parse-work-result.test.d.ts +2 -0
  130. package/dist/src/orchestrator/parse-work-result.test.d.ts.map +1 -0
  131. package/dist/src/orchestrator/parse-work-result.test.js +234 -0
  132. package/dist/src/orchestrator/progress-logger.d.ts +72 -0
  133. package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
  134. package/dist/src/orchestrator/progress-logger.js +135 -0
  135. package/dist/src/orchestrator/session-logger.d.ts +159 -0
  136. package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
  137. package/dist/src/orchestrator/session-logger.js +275 -0
  138. package/dist/src/orchestrator/state-recovery.d.ts +96 -0
  139. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
  140. package/dist/src/orchestrator/state-recovery.js +302 -0
  141. package/dist/src/orchestrator/state-types.d.ts +165 -0
  142. package/dist/src/orchestrator/state-types.d.ts.map +1 -0
  143. package/dist/src/orchestrator/state-types.js +7 -0
  144. package/dist/src/orchestrator/stream-parser.d.ts +151 -0
  145. package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
  146. package/dist/src/orchestrator/stream-parser.js +137 -0
  147. package/dist/src/orchestrator/types.d.ts +232 -0
  148. package/dist/src/orchestrator/types.d.ts.map +1 -0
  149. package/dist/src/orchestrator/types.js +4 -0
  150. package/dist/src/orchestrator/validate-git-remote.test.d.ts +2 -0
  151. package/dist/src/orchestrator/validate-git-remote.test.d.ts.map +1 -0
  152. package/dist/src/orchestrator/validate-git-remote.test.js +61 -0
  153. package/dist/src/providers/a2a-auth.d.ts +81 -0
  154. package/dist/src/providers/a2a-auth.d.ts.map +1 -0
  155. package/dist/src/providers/a2a-auth.js +188 -0
  156. package/dist/src/providers/a2a-auth.test.d.ts +2 -0
  157. package/dist/src/providers/a2a-auth.test.d.ts.map +1 -0
  158. package/dist/src/providers/a2a-auth.test.js +232 -0
  159. package/dist/src/providers/a2a-provider.d.ts +254 -0
  160. package/dist/src/providers/a2a-provider.d.ts.map +1 -0
  161. package/dist/src/providers/a2a-provider.integration.test.d.ts +9 -0
  162. package/dist/src/providers/a2a-provider.integration.test.d.ts.map +1 -0
  163. package/dist/src/providers/a2a-provider.integration.test.js +665 -0
  164. package/dist/src/providers/a2a-provider.js +811 -0
  165. package/dist/src/providers/a2a-provider.test.d.ts +2 -0
  166. package/dist/src/providers/a2a-provider.test.d.ts.map +1 -0
  167. package/dist/src/providers/a2a-provider.test.js +681 -0
  168. package/dist/src/providers/amp-provider.d.ts +20 -0
  169. package/dist/src/providers/amp-provider.d.ts.map +1 -0
  170. package/dist/src/providers/amp-provider.js +24 -0
  171. package/dist/src/providers/claude-provider.d.ts +18 -0
  172. package/dist/src/providers/claude-provider.d.ts.map +1 -0
  173. package/dist/src/providers/claude-provider.js +437 -0
  174. package/dist/src/providers/codex-provider.d.ts +133 -0
  175. package/dist/src/providers/codex-provider.d.ts.map +1 -0
  176. package/dist/src/providers/codex-provider.js +381 -0
  177. package/dist/src/providers/codex-provider.test.d.ts +2 -0
  178. package/dist/src/providers/codex-provider.test.d.ts.map +1 -0
  179. package/dist/src/providers/codex-provider.test.js +387 -0
  180. package/dist/src/providers/index.d.ts +44 -0
  181. package/dist/src/providers/index.d.ts.map +1 -0
  182. package/dist/src/providers/index.js +85 -0
  183. package/dist/src/providers/spring-ai-provider.d.ts +90 -0
  184. package/dist/src/providers/spring-ai-provider.d.ts.map +1 -0
  185. package/dist/src/providers/spring-ai-provider.integration.test.d.ts +13 -0
  186. package/dist/src/providers/spring-ai-provider.integration.test.d.ts.map +1 -0
  187. package/dist/src/providers/spring-ai-provider.integration.test.js +351 -0
  188. package/dist/src/providers/spring-ai-provider.js +317 -0
  189. package/dist/src/providers/spring-ai-provider.test.d.ts +2 -0
  190. package/dist/src/providers/spring-ai-provider.test.d.ts.map +1 -0
  191. package/dist/src/providers/spring-ai-provider.test.js +200 -0
  192. package/dist/src/providers/types.d.ts +165 -0
  193. package/dist/src/providers/types.d.ts.map +1 -0
  194. package/dist/src/providers/types.js +13 -0
  195. package/dist/src/templates/adapters.d.ts +51 -0
  196. package/dist/src/templates/adapters.d.ts.map +1 -0
  197. package/dist/src/templates/adapters.js +104 -0
  198. package/dist/src/templates/adapters.test.d.ts +2 -0
  199. package/dist/src/templates/adapters.test.d.ts.map +1 -0
  200. package/dist/src/templates/adapters.test.js +165 -0
  201. package/dist/src/templates/agent-definition.d.ts +85 -0
  202. package/dist/src/templates/agent-definition.d.ts.map +1 -0
  203. package/dist/src/templates/agent-definition.js +97 -0
  204. package/dist/src/templates/agent-definition.test.d.ts +2 -0
  205. package/dist/src/templates/agent-definition.test.d.ts.map +1 -0
  206. package/dist/src/templates/agent-definition.test.js +209 -0
  207. package/dist/src/templates/index.d.ts +14 -0
  208. package/dist/src/templates/index.d.ts.map +1 -0
  209. package/dist/src/templates/index.js +11 -0
  210. package/dist/src/templates/loader.d.ts +41 -0
  211. package/dist/src/templates/loader.d.ts.map +1 -0
  212. package/dist/src/templates/loader.js +114 -0
  213. package/dist/src/templates/registry.d.ts +80 -0
  214. package/dist/src/templates/registry.d.ts.map +1 -0
  215. package/dist/src/templates/registry.js +177 -0
  216. package/dist/src/templates/registry.test.d.ts +2 -0
  217. package/dist/src/templates/registry.test.d.ts.map +1 -0
  218. package/dist/src/templates/registry.test.js +198 -0
  219. package/dist/src/templates/renderer.d.ts +29 -0
  220. package/dist/src/templates/renderer.d.ts.map +1 -0
  221. package/dist/src/templates/renderer.js +35 -0
  222. package/dist/src/templates/strategy-templates.test.d.ts +2 -0
  223. package/dist/src/templates/strategy-templates.test.d.ts.map +1 -0
  224. package/dist/src/templates/strategy-templates.test.js +619 -0
  225. package/dist/src/templates/types.d.ts +233 -0
  226. package/dist/src/templates/types.d.ts.map +1 -0
  227. package/dist/src/templates/types.js +127 -0
  228. package/dist/src/templates/types.test.d.ts +2 -0
  229. package/dist/src/templates/types.test.d.ts.map +1 -0
  230. package/dist/src/templates/types.test.js +232 -0
  231. package/dist/src/tools/index.d.ts +6 -0
  232. package/dist/src/tools/index.d.ts.map +1 -0
  233. package/dist/src/tools/index.js +3 -0
  234. package/dist/src/tools/linear-runner.d.ts +34 -0
  235. package/dist/src/tools/linear-runner.d.ts.map +1 -0
  236. package/dist/src/tools/linear-runner.js +700 -0
  237. package/dist/src/tools/plugins/linear.d.ts +9 -0
  238. package/dist/src/tools/plugins/linear.d.ts.map +1 -0
  239. package/dist/src/tools/plugins/linear.js +138 -0
  240. package/dist/src/tools/registry.d.ts +9 -0
  241. package/dist/src/tools/registry.d.ts.map +1 -0
  242. package/dist/src/tools/registry.js +18 -0
  243. package/dist/src/tools/types.d.ts +18 -0
  244. package/dist/src/tools/types.d.ts.map +1 -0
  245. package/dist/src/tools/types.js +1 -0
  246. package/package.json +78 -0
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Human Touchpoint Manager
3
+ *
4
+ * Manages human override state and generates review request notifications.
5
+ * Uses a storage adapter pattern so that packages/core does not depend on
6
+ * packages/server (Redis) directly.
7
+ */
8
+ const log = {
9
+ info: (msg, data) => console.log(`[touchpoints] ${msg}`, data ? JSON.stringify(data) : ''),
10
+ warn: (msg, data) => console.warn(`[touchpoints] ${msg}`, data ? JSON.stringify(data) : ''),
11
+ error: (msg, data) => console.error(`[touchpoints] ${msg}`, data ? JSON.stringify(data) : ''),
12
+ debug: (_msg, _data) => { },
13
+ };
14
+ /**
15
+ * Default touchpoint configuration
16
+ */
17
+ export const DEFAULT_TOUCHPOINT_CONFIG = {
18
+ reviewRequestTimeoutMs: 4 * 60 * 60 * 1000, // 4 hours
19
+ decompositionProposalTimeoutMs: 2 * 60 * 60 * 1000, // 2 hours
20
+ escalationTimeoutMs: Infinity, // Requires human
21
+ };
22
+ /**
23
+ * In-memory override storage for testing and local development
24
+ */
25
+ export class InMemoryOverrideStorage {
26
+ store = new Map();
27
+ async get(issueId) {
28
+ return this.store.get(issueId) ?? null;
29
+ }
30
+ async set(issueId, state) {
31
+ this.store.set(issueId, state);
32
+ }
33
+ async clear(issueId) {
34
+ this.store.delete(issueId);
35
+ }
36
+ }
37
+ // ============================================
38
+ // Module-level storage reference
39
+ // ============================================
40
+ let _storage = null;
41
+ /**
42
+ * Initialize the touchpoint manager with a storage adapter.
43
+ * Must be called before using state management functions.
44
+ */
45
+ export function initTouchpointStorage(storage) {
46
+ _storage = storage;
47
+ log.info('Touchpoint storage initialized');
48
+ }
49
+ /**
50
+ * Get the current storage adapter, throwing if not initialized
51
+ */
52
+ function getStorage() {
53
+ if (!_storage) {
54
+ throw new Error('Touchpoint storage not initialized. Call initTouchpointStorage() first.');
55
+ }
56
+ return _storage;
57
+ }
58
+ // ============================================
59
+ // Override State Management
60
+ // ============================================
61
+ /**
62
+ * Get the current override state for an issue
63
+ */
64
+ export async function getOverrideState(issueId) {
65
+ const storage = getStorage();
66
+ const state = await storage.get(issueId);
67
+ // Check if the override has expired
68
+ if (state && state.expiresAt && Date.now() > state.expiresAt) {
69
+ log.info('Override expired', { issueId, type: state.directive.type });
70
+ await storage.clear(issueId);
71
+ return null;
72
+ }
73
+ return state;
74
+ }
75
+ /**
76
+ * Set an override directive for an issue
77
+ */
78
+ export async function setOverrideState(issueId, directive) {
79
+ const storage = getStorage();
80
+ const state = {
81
+ issueId,
82
+ directive,
83
+ isActive: true,
84
+ };
85
+ await storage.set(issueId, state);
86
+ log.info('Override state set', { issueId, type: directive.type });
87
+ }
88
+ /**
89
+ * Clear the override state for an issue (e.g., on RESUME)
90
+ */
91
+ export async function clearOverrideState(issueId) {
92
+ const storage = getStorage();
93
+ await storage.clear(issueId);
94
+ log.info('Override state cleared', { issueId });
95
+ }
96
+ /**
97
+ * Check if an issue is currently held (HOLD directive active)
98
+ */
99
+ export async function isHeld(issueId) {
100
+ const state = await getOverrideState(issueId);
101
+ return state !== null && state.isActive && state.directive.type === 'hold';
102
+ }
103
+ /**
104
+ * Get the PRIORITY override for an issue, if one is active.
105
+ * Returns the priority level ('high' | 'medium' | 'low') or null if no priority override.
106
+ */
107
+ export async function getOverridePriority(issueId) {
108
+ const state = await getOverrideState(issueId);
109
+ if (state && state.isActive && state.directive.type === 'priority' && state.directive.priority) {
110
+ return state.directive.priority;
111
+ }
112
+ return null;
113
+ }
114
+ // ============================================
115
+ // Notification Generation
116
+ // ============================================
117
+ /**
118
+ * Format a cost string for display in notifications.
119
+ * Returns empty string if no cost is provided.
120
+ */
121
+ function formatCost(totalCostUsd) {
122
+ if (totalCostUsd === undefined || totalCostUsd === null) {
123
+ return '';
124
+ }
125
+ return `\n- **Total cost so far:** $${totalCostUsd.toFixed(2)}`;
126
+ }
127
+ /**
128
+ * Generate a review request notification.
129
+ * Typically posted at cycle 2 when the context-enriched strategy kicks in.
130
+ */
131
+ export function generateReviewRequest(context, config = DEFAULT_TOUCHPOINT_CONFIG) {
132
+ const body = `## Review Request
133
+
134
+ **${context.issueIdentifier}** has failed **${context.cycleCount}** dev-QA cycle(s).
135
+
136
+ - **Current strategy:** ${context.strategy}${formatCost(context.totalCostUsd)}
137
+
138
+ ### Failure Summary
139
+
140
+ ${context.failureSummary || '_No failure details available._'}
141
+
142
+ ### Actions
143
+
144
+ Reply with one of the following directives:
145
+ - **HOLD** — Pause autonomous processing
146
+ - **SKIP QA** — Skip QA and proceed to acceptance
147
+ - **DECOMPOSE** — Trigger task decomposition
148
+ - **REASSIGN** — Stop agent work, assign to a human
149
+ - **PRIORITY: high|medium|low** — Adjust scheduling priority
150
+ - **RESUME** — Continue with current strategy
151
+
152
+ _This request will auto-proceed in ${Math.round(config.reviewRequestTimeoutMs / (60 * 60 * 1000))} hour(s) if no response is received._`;
153
+ return {
154
+ type: 'review-request',
155
+ issueId: context.issueIdentifier,
156
+ body,
157
+ postedAt: Date.now(),
158
+ timeoutMs: config.reviewRequestTimeoutMs,
159
+ };
160
+ }
161
+ /**
162
+ * Generate a decomposition proposal notification.
163
+ * Typically posted at cycle 3 when the decompose strategy kicks in.
164
+ */
165
+ export function generateDecompositionProposal(context, config = DEFAULT_TOUCHPOINT_CONFIG) {
166
+ const body = `## Decomposition Proposal
167
+
168
+ **${context.issueIdentifier}** has failed **${context.cycleCount}** dev-QA cycle(s) and is being considered for decomposition into smaller sub-issues.
169
+
170
+ ${formatCost(context.totalCostUsd) ? `- ${formatCost(context.totalCostUsd).trim()}` : ''}
171
+
172
+ ### Failure Summary
173
+
174
+ ${context.failureSummary || '_No failure details available._'}
175
+
176
+ ### Recommended Action
177
+
178
+ The agent will attempt to decompose this issue into smaller, independently solvable sub-issues.
179
+
180
+ Reply with a directive to override:
181
+ - **HOLD** — Pause and review manually
182
+ - **SKIP QA** — Skip QA and proceed to acceptance
183
+ - **REASSIGN** — Stop agent work entirely
184
+ - **PRIORITY: high|medium|low** — Adjust scheduling priority
185
+ - **RESUME** — Proceed with decomposition (default)
186
+
187
+ _Decomposition will auto-proceed in ${Math.round(config.decompositionProposalTimeoutMs / (60 * 60 * 1000))} hour(s) if no response is received._`;
188
+ return {
189
+ type: 'decomposition-proposal',
190
+ issueId: context.issueIdentifier,
191
+ body,
192
+ postedAt: Date.now(),
193
+ timeoutMs: config.decompositionProposalTimeoutMs,
194
+ };
195
+ }
196
+ /**
197
+ * Generate an escalation alert notification.
198
+ * Posted at cycle 4+ when human intervention is required.
199
+ */
200
+ export function generateEscalationAlert(context, config = DEFAULT_TOUCHPOINT_CONFIG) {
201
+ const blockerLine = context.blockerIdentifier
202
+ ? `\n- **Blocker issue:** ${context.blockerIdentifier}`
203
+ : '';
204
+ const body = `## Escalation Alert
205
+
206
+ **${context.issueIdentifier}** has failed **${context.cycleCount}** dev-QA cycle(s) and requires human intervention.
207
+
208
+ - **Strategy:** escalate-human${blockerLine}${formatCost(context.totalCostUsd)}
209
+
210
+ ### Failure Summary
211
+
212
+ ${context.failureSummary || '_No failure details available._'}
213
+
214
+ ### Required Action
215
+
216
+ This issue has exhausted automated resolution strategies. A human must review and take action:
217
+ - **HOLD** — Keep paused (current state)
218
+ - **DECOMPOSE** — Request agent decomposition
219
+ - **REASSIGN** — Assign to a specific person
220
+ - **PRIORITY: high|medium|low** — Adjust scheduling priority
221
+ - **RESUME** — Retry with normal strategy (resets cycle count)
222
+
223
+ _This issue will remain paused until a human responds._`;
224
+ return {
225
+ type: 'escalation-alert',
226
+ issueId: context.issueIdentifier,
227
+ body,
228
+ postedAt: Date.now(),
229
+ timeoutMs: config.escalationTimeoutMs,
230
+ };
231
+ }
232
+ // ============================================
233
+ // Timeout Checking
234
+ // ============================================
235
+ /**
236
+ * Check if a touchpoint notification has timed out (human did not respond in time).
237
+ *
238
+ * A touchpoint with Infinity timeout never times out (always returns false).
239
+ * A touchpoint that has been responded to (respondedAt is set) never times out.
240
+ */
241
+ export function hasTouchpointTimedOut(notification) {
242
+ // Already responded — not timed out
243
+ if (notification.respondedAt !== undefined) {
244
+ return false;
245
+ }
246
+ // Infinite timeout — never times out
247
+ if (!isFinite(notification.timeoutMs)) {
248
+ return false;
249
+ }
250
+ return Date.now() > notification.postedAt + notification.timeoutMs;
251
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=human-touchpoints.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"human-touchpoints.test.d.ts","sourceRoot":"","sources":["../../../src/governor/human-touchpoints.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,366 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { InMemoryOverrideStorage, initTouchpointStorage, getOverrideState, setOverrideState, clearOverrideState, isHeld, getOverridePriority, generateReviewRequest, generateDecompositionProposal, generateEscalationAlert, hasTouchpointTimedOut, DEFAULT_TOUCHPOINT_CONFIG, } from './human-touchpoints.js';
3
+ // ============================================
4
+ // Helpers
5
+ // ============================================
6
+ function makeDirective(overrides = {}) {
7
+ return {
8
+ type: 'hold',
9
+ timestamp: Date.now(),
10
+ ...overrides,
11
+ };
12
+ }
13
+ // ============================================
14
+ // Tests
15
+ // ============================================
16
+ describe('Override State Management', () => {
17
+ let storage;
18
+ beforeEach(() => {
19
+ storage = new InMemoryOverrideStorage();
20
+ initTouchpointStorage(storage);
21
+ });
22
+ describe('setOverrideState / getOverrideState', () => {
23
+ it('stores and retrieves override state', async () => {
24
+ const directive = makeDirective({ type: 'hold', reason: 'security review' });
25
+ await setOverrideState('issue-1', directive);
26
+ const state = await getOverrideState('issue-1');
27
+ expect(state).not.toBeNull();
28
+ expect(state.issueId).toBe('issue-1');
29
+ expect(state.directive.type).toBe('hold');
30
+ expect(state.directive.reason).toBe('security review');
31
+ expect(state.isActive).toBe(true);
32
+ });
33
+ it('returns null for non-existent issue', async () => {
34
+ const state = await getOverrideState('nonexistent');
35
+ expect(state).toBeNull();
36
+ });
37
+ it('overwrites existing state', async () => {
38
+ await setOverrideState('issue-1', makeDirective({ type: 'hold' }));
39
+ await setOverrideState('issue-1', makeDirective({ type: 'decompose' }));
40
+ const state = await getOverrideState('issue-1');
41
+ expect(state.directive.type).toBe('decompose');
42
+ });
43
+ });
44
+ describe('clearOverrideState', () => {
45
+ it('clears existing state', async () => {
46
+ await setOverrideState('issue-1', makeDirective({ type: 'hold' }));
47
+ await clearOverrideState('issue-1');
48
+ const state = await getOverrideState('issue-1');
49
+ expect(state).toBeNull();
50
+ });
51
+ it('does not throw when clearing non-existent state', async () => {
52
+ await expect(clearOverrideState('nonexistent')).resolves.not.toThrow();
53
+ });
54
+ });
55
+ describe('isHeld', () => {
56
+ it('returns true when issue has active HOLD', async () => {
57
+ await setOverrideState('issue-1', makeDirective({ type: 'hold' }));
58
+ expect(await isHeld('issue-1')).toBe(true);
59
+ });
60
+ it('returns false when issue has no override', async () => {
61
+ expect(await isHeld('issue-1')).toBe(false);
62
+ });
63
+ it('returns false when issue has non-hold override', async () => {
64
+ await setOverrideState('issue-1', makeDirective({ type: 'decompose' }));
65
+ expect(await isHeld('issue-1')).toBe(false);
66
+ });
67
+ it('returns false when override has expired', async () => {
68
+ // Directly set state with expiresAt in the past
69
+ const expiredState = {
70
+ issueId: 'issue-1',
71
+ directive: makeDirective({ type: 'hold' }),
72
+ isActive: true,
73
+ expiresAt: Date.now() - 1000,
74
+ };
75
+ await storage.set('issue-1', expiredState);
76
+ expect(await isHeld('issue-1')).toBe(false);
77
+ });
78
+ });
79
+ describe('getOverridePriority', () => {
80
+ it('returns priority when PRIORITY override is active', async () => {
81
+ await setOverrideState('issue-1', makeDirective({ type: 'priority', priority: 'high' }));
82
+ expect(await getOverridePriority('issue-1')).toBe('high');
83
+ });
84
+ it('returns null when no override exists', async () => {
85
+ expect(await getOverridePriority('issue-1')).toBeNull();
86
+ });
87
+ it('returns null when override is non-priority type', async () => {
88
+ await setOverrideState('issue-1', makeDirective({ type: 'hold' }));
89
+ expect(await getOverridePriority('issue-1')).toBeNull();
90
+ });
91
+ it('returns correct level for medium priority', async () => {
92
+ await setOverrideState('issue-1', makeDirective({ type: 'priority', priority: 'medium' }));
93
+ expect(await getOverridePriority('issue-1')).toBe('medium');
94
+ });
95
+ it('returns correct level for low priority', async () => {
96
+ await setOverrideState('issue-1', makeDirective({ type: 'priority', priority: 'low' }));
97
+ expect(await getOverridePriority('issue-1')).toBe('low');
98
+ });
99
+ it('returns null for expired priority override', async () => {
100
+ const expiredState = {
101
+ issueId: 'issue-1',
102
+ directive: makeDirective({ type: 'priority', priority: 'high' }),
103
+ isActive: true,
104
+ expiresAt: Date.now() - 1000,
105
+ };
106
+ await storage.set('issue-1', expiredState);
107
+ expect(await getOverridePriority('issue-1')).toBeNull();
108
+ });
109
+ });
110
+ describe('expiration handling', () => {
111
+ it('returns null for expired override state', async () => {
112
+ const expiredState = {
113
+ issueId: 'issue-1',
114
+ directive: makeDirective({ type: 'hold' }),
115
+ isActive: true,
116
+ expiresAt: Date.now() - 1000,
117
+ };
118
+ await storage.set('issue-1', expiredState);
119
+ const state = await getOverrideState('issue-1');
120
+ expect(state).toBeNull();
121
+ });
122
+ it('returns state when expiresAt is in the future', async () => {
123
+ const futureState = {
124
+ issueId: 'issue-1',
125
+ directive: makeDirective({ type: 'hold' }),
126
+ isActive: true,
127
+ expiresAt: Date.now() + 60_000,
128
+ };
129
+ await storage.set('issue-1', futureState);
130
+ const state = await getOverrideState('issue-1');
131
+ expect(state).not.toBeNull();
132
+ expect(state.isActive).toBe(true);
133
+ });
134
+ it('returns state when expiresAt is undefined', async () => {
135
+ await setOverrideState('issue-1', makeDirective({ type: 'hold' }));
136
+ const state = await getOverrideState('issue-1');
137
+ expect(state).not.toBeNull();
138
+ });
139
+ });
140
+ describe('storage adapter pattern', () => {
141
+ it('InMemoryOverrideStorage implements the interface correctly', async () => {
142
+ const mem = new InMemoryOverrideStorage();
143
+ // get returns null initially
144
+ expect(await mem.get('x')).toBeNull();
145
+ // set stores data
146
+ const state = {
147
+ issueId: 'x',
148
+ directive: makeDirective(),
149
+ isActive: true,
150
+ };
151
+ await mem.set('x', state);
152
+ expect(await mem.get('x')).toEqual(state);
153
+ // clear removes data
154
+ await mem.clear('x');
155
+ expect(await mem.get('x')).toBeNull();
156
+ });
157
+ it('throws when storage is not initialized', async () => {
158
+ // Re-initialize with null-like behavior by creating a fresh module scope
159
+ // We test this by checking the error message pattern
160
+ initTouchpointStorage(storage); // This ensures it works after init
161
+ const state = await getOverrideState('any-issue');
162
+ // Should work since storage is initialized
163
+ expect(state).toBeNull();
164
+ });
165
+ });
166
+ });
167
+ describe('Notification Generation', () => {
168
+ describe('generateReviewRequest', () => {
169
+ it('generates a review request at cycle 2', () => {
170
+ const notification = generateReviewRequest({
171
+ issueIdentifier: 'SUP-123',
172
+ cycleCount: 2,
173
+ failureSummary: 'Tests failed: 3 assertions in auth module',
174
+ strategy: 'context-enriched',
175
+ });
176
+ expect(notification.type).toBe('review-request');
177
+ expect(notification.issueId).toBe('SUP-123');
178
+ expect(notification.body).toContain('SUP-123');
179
+ expect(notification.body).toContain('2');
180
+ expect(notification.body).toContain('context-enriched');
181
+ expect(notification.body).toContain('Tests failed: 3 assertions in auth module');
182
+ expect(notification.body).toContain('HOLD');
183
+ expect(notification.body).toContain('SKIP QA');
184
+ expect(notification.body).toContain('DECOMPOSE');
185
+ expect(notification.body).toContain('REASSIGN');
186
+ expect(notification.body).toContain('RESUME');
187
+ expect(notification.timeoutMs).toBe(DEFAULT_TOUCHPOINT_CONFIG.reviewRequestTimeoutMs);
188
+ expect(notification.postedAt).toBeGreaterThan(0);
189
+ });
190
+ it('includes cost when provided', () => {
191
+ const notification = generateReviewRequest({
192
+ issueIdentifier: 'SUP-123',
193
+ cycleCount: 2,
194
+ failureSummary: 'Type errors',
195
+ strategy: 'context-enriched',
196
+ totalCostUsd: 4.56,
197
+ });
198
+ expect(notification.body).toContain('$4.56');
199
+ });
200
+ it('handles missing failure summary', () => {
201
+ const notification = generateReviewRequest({
202
+ issueIdentifier: 'SUP-123',
203
+ cycleCount: 2,
204
+ failureSummary: '',
205
+ strategy: 'context-enriched',
206
+ });
207
+ expect(notification.body).toContain('No failure details available');
208
+ });
209
+ it('lists PRIORITY directive in available actions', () => {
210
+ const notification = generateReviewRequest({
211
+ issueIdentifier: 'SUP-123',
212
+ cycleCount: 2,
213
+ failureSummary: 'fail',
214
+ strategy: 'normal',
215
+ });
216
+ expect(notification.body).toContain('PRIORITY: high|medium|low');
217
+ });
218
+ it('uses custom config timeout', () => {
219
+ const customConfig = {
220
+ ...DEFAULT_TOUCHPOINT_CONFIG,
221
+ reviewRequestTimeoutMs: 1000,
222
+ };
223
+ const notification = generateReviewRequest({
224
+ issueIdentifier: 'SUP-1',
225
+ cycleCount: 2,
226
+ failureSummary: 'fail',
227
+ strategy: 'normal',
228
+ }, customConfig);
229
+ expect(notification.timeoutMs).toBe(1000);
230
+ });
231
+ });
232
+ describe('generateDecompositionProposal', () => {
233
+ it('generates a decomposition proposal at cycle 3', () => {
234
+ const notification = generateDecompositionProposal({
235
+ issueIdentifier: 'SUP-456',
236
+ cycleCount: 3,
237
+ failureSummary: 'Integration test failures in payment module',
238
+ });
239
+ expect(notification.type).toBe('decomposition-proposal');
240
+ expect(notification.issueId).toBe('SUP-456');
241
+ expect(notification.body).toContain('SUP-456');
242
+ expect(notification.body).toContain('3');
243
+ expect(notification.body).toContain('decomposition');
244
+ expect(notification.body).toContain('Integration test failures in payment module');
245
+ expect(notification.body).toContain('HOLD');
246
+ expect(notification.body).toContain('REASSIGN');
247
+ expect(notification.timeoutMs).toBe(DEFAULT_TOUCHPOINT_CONFIG.decompositionProposalTimeoutMs);
248
+ });
249
+ it('includes cost when provided', () => {
250
+ const notification = generateDecompositionProposal({
251
+ issueIdentifier: 'SUP-456',
252
+ cycleCount: 3,
253
+ failureSummary: 'fail',
254
+ totalCostUsd: 12.34,
255
+ });
256
+ expect(notification.body).toContain('$12.34');
257
+ });
258
+ });
259
+ describe('generateEscalationAlert', () => {
260
+ it('generates an escalation alert at cycle 4+', () => {
261
+ const notification = generateEscalationAlert({
262
+ issueIdentifier: 'SUP-789',
263
+ cycleCount: 4,
264
+ failureSummary: 'Persistent type errors that decomposition could not resolve',
265
+ });
266
+ expect(notification.type).toBe('escalation-alert');
267
+ expect(notification.issueId).toBe('SUP-789');
268
+ expect(notification.body).toContain('SUP-789');
269
+ expect(notification.body).toContain('4');
270
+ expect(notification.body).toContain('escalate-human');
271
+ expect(notification.body).toContain('Persistent type errors');
272
+ expect(notification.body).toContain('human must review');
273
+ expect(notification.timeoutMs).toBe(Infinity);
274
+ });
275
+ it('includes blocker identifier when provided', () => {
276
+ const notification = generateEscalationAlert({
277
+ issueIdentifier: 'SUP-789',
278
+ cycleCount: 5,
279
+ failureSummary: 'Blocked by external dependency',
280
+ blockerIdentifier: 'SUP-800',
281
+ });
282
+ expect(notification.body).toContain('SUP-800');
283
+ expect(notification.body).toContain('Blocker issue');
284
+ });
285
+ it('omits blocker line when not provided', () => {
286
+ const notification = generateEscalationAlert({
287
+ issueIdentifier: 'SUP-789',
288
+ cycleCount: 5,
289
+ failureSummary: 'fail',
290
+ });
291
+ expect(notification.body).not.toContain('Blocker issue');
292
+ });
293
+ it('includes cost when provided', () => {
294
+ const notification = generateEscalationAlert({
295
+ issueIdentifier: 'SUP-789',
296
+ cycleCount: 4,
297
+ failureSummary: 'fail',
298
+ totalCostUsd: 25.0,
299
+ });
300
+ expect(notification.body).toContain('$25.00');
301
+ });
302
+ });
303
+ });
304
+ describe('Touchpoint Timeout', () => {
305
+ describe('hasTouchpointTimedOut', () => {
306
+ it('returns false when notification was just posted', () => {
307
+ const notification = {
308
+ type: 'review-request',
309
+ issueId: 'SUP-1',
310
+ body: 'test',
311
+ postedAt: Date.now(),
312
+ timeoutMs: 60_000,
313
+ };
314
+ expect(hasTouchpointTimedOut(notification)).toBe(false);
315
+ });
316
+ it('returns true when timeout has elapsed', () => {
317
+ const notification = {
318
+ type: 'review-request',
319
+ issueId: 'SUP-1',
320
+ body: 'test',
321
+ postedAt: Date.now() - 120_000,
322
+ timeoutMs: 60_000,
323
+ };
324
+ expect(hasTouchpointTimedOut(notification)).toBe(true);
325
+ });
326
+ it('returns false when respondedAt is set (even if past timeout)', () => {
327
+ const notification = {
328
+ type: 'review-request',
329
+ issueId: 'SUP-1',
330
+ body: 'test',
331
+ postedAt: Date.now() - 120_000,
332
+ timeoutMs: 60_000,
333
+ respondedAt: Date.now() - 30_000,
334
+ };
335
+ expect(hasTouchpointTimedOut(notification)).toBe(false);
336
+ });
337
+ it('returns false for infinite timeout (escalation alerts)', () => {
338
+ const notification = {
339
+ type: 'escalation-alert',
340
+ issueId: 'SUP-1',
341
+ body: 'test',
342
+ postedAt: Date.now() - 999_999_999,
343
+ timeoutMs: Infinity,
344
+ };
345
+ expect(hasTouchpointTimedOut(notification)).toBe(false);
346
+ });
347
+ it('returns true at exact timeout boundary', () => {
348
+ const now = Date.now();
349
+ const notification = {
350
+ type: 'review-request',
351
+ issueId: 'SUP-1',
352
+ body: 'test',
353
+ postedAt: now - 60_001,
354
+ timeoutMs: 60_000,
355
+ };
356
+ expect(hasTouchpointTimedOut(notification)).toBe(true);
357
+ });
358
+ });
359
+ });
360
+ describe('DEFAULT_TOUCHPOINT_CONFIG', () => {
361
+ it('has correct default values', () => {
362
+ expect(DEFAULT_TOUCHPOINT_CONFIG.reviewRequestTimeoutMs).toBe(4 * 60 * 60 * 1000);
363
+ expect(DEFAULT_TOUCHPOINT_CONFIG.decompositionProposalTimeoutMs).toBe(2 * 60 * 60 * 1000);
364
+ expect(DEFAULT_TOUCHPOINT_CONFIG.escalationTimeoutMs).toBe(Infinity);
365
+ });
366
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * In-Memory Event Bus
3
+ *
4
+ * Simple event bus implementation for testing and single-process CLI usage.
5
+ * Events are stored in a queue and delivered via an async generator.
6
+ */
7
+ import type { GovernorEvent } from './event-types.js';
8
+ import type { GovernorEventBus } from './event-bus.js';
9
+ export declare class InMemoryEventBus implements GovernorEventBus {
10
+ private queue;
11
+ private waiters;
12
+ private closed;
13
+ private idCounter;
14
+ private ackedIds;
15
+ publish(event: GovernorEvent): Promise<string>;
16
+ subscribe(): AsyncGenerator<{
17
+ id: string;
18
+ event: GovernorEvent;
19
+ }>;
20
+ ack(eventId: string): Promise<void>;
21
+ close(): Promise<void>;
22
+ /** Check if an event ID has been acknowledged */
23
+ isAcked(eventId: string): boolean;
24
+ /** Get the number of pending (undelivered) events */
25
+ get pendingCount(): number;
26
+ /** Check if the bus has been closed */
27
+ get isClosed(): boolean;
28
+ }
29
+ //# sourceMappingURL=in-memory-event-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory-event-bus.d.ts","sourceRoot":"","sources":["../../../src/governor/in-memory-event-bus.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAMtD,qBAAa,gBAAiB,YAAW,gBAAgB;IACvD,OAAO,CAAC,KAAK,CAAkD;IAC/D,OAAO,CAAC,OAAO,CAAkE;IACjF,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,QAAQ,CAAoB;IAE9B,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAmB7C,SAAS,IAAI,cAAc,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC;IA4BlE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5B,iDAAiD;IACjD,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAIjC,qDAAqD;IACrD,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,uCAAuC;IACvC,IAAI,QAAQ,IAAI,OAAO,CAEtB;CACF"}