@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.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/src/config/index.d.ts +3 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/index.js +1 -0
- package/dist/src/config/repository-config.d.ts +44 -0
- package/dist/src/config/repository-config.d.ts.map +1 -0
- package/dist/src/config/repository-config.js +88 -0
- package/dist/src/config/repository-config.test.d.ts +2 -0
- package/dist/src/config/repository-config.test.d.ts.map +1 -0
- package/dist/src/config/repository-config.test.js +249 -0
- package/dist/src/deployment/deployment-checker.d.ts +110 -0
- package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
- package/dist/src/deployment/deployment-checker.js +242 -0
- package/dist/src/deployment/index.d.ts +3 -0
- package/dist/src/deployment/index.d.ts.map +1 -0
- package/dist/src/deployment/index.js +2 -0
- package/dist/src/frontend/index.d.ts +2 -0
- package/dist/src/frontend/index.d.ts.map +1 -0
- package/dist/src/frontend/index.js +1 -0
- package/dist/src/frontend/types.d.ts +106 -0
- package/dist/src/frontend/types.d.ts.map +1 -0
- package/dist/src/frontend/types.js +11 -0
- package/dist/src/governor/decision-engine.d.ts +52 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -0
- package/dist/src/governor/decision-engine.js +220 -0
- package/dist/src/governor/decision-engine.test.d.ts +2 -0
- package/dist/src/governor/decision-engine.test.d.ts.map +1 -0
- package/dist/src/governor/decision-engine.test.js +629 -0
- package/dist/src/governor/event-bus.d.ts +43 -0
- package/dist/src/governor/event-bus.d.ts.map +1 -0
- package/dist/src/governor/event-bus.js +8 -0
- package/dist/src/governor/event-deduplicator.d.ts +43 -0
- package/dist/src/governor/event-deduplicator.d.ts.map +1 -0
- package/dist/src/governor/event-deduplicator.js +53 -0
- package/dist/src/governor/event-driven-governor.d.ts +131 -0
- package/dist/src/governor/event-driven-governor.d.ts.map +1 -0
- package/dist/src/governor/event-driven-governor.js +379 -0
- package/dist/src/governor/event-driven-governor.test.d.ts +2 -0
- package/dist/src/governor/event-driven-governor.test.d.ts.map +1 -0
- package/dist/src/governor/event-driven-governor.test.js +673 -0
- package/dist/src/governor/event-types.d.ts +78 -0
- package/dist/src/governor/event-types.d.ts.map +1 -0
- package/dist/src/governor/event-types.js +32 -0
- package/dist/src/governor/governor-types.d.ts +82 -0
- package/dist/src/governor/governor-types.d.ts.map +1 -0
- package/dist/src/governor/governor-types.js +21 -0
- package/dist/src/governor/governor.d.ts +100 -0
- package/dist/src/governor/governor.d.ts.map +1 -0
- package/dist/src/governor/governor.js +262 -0
- package/dist/src/governor/governor.test.d.ts +2 -0
- package/dist/src/governor/governor.test.d.ts.map +1 -0
- package/dist/src/governor/governor.test.js +514 -0
- package/dist/src/governor/human-touchpoints.d.ts +131 -0
- package/dist/src/governor/human-touchpoints.d.ts.map +1 -0
- package/dist/src/governor/human-touchpoints.js +251 -0
- package/dist/src/governor/human-touchpoints.test.d.ts +2 -0
- package/dist/src/governor/human-touchpoints.test.d.ts.map +1 -0
- package/dist/src/governor/human-touchpoints.test.js +366 -0
- package/dist/src/governor/in-memory-event-bus.d.ts +29 -0
- package/dist/src/governor/in-memory-event-bus.d.ts.map +1 -0
- package/dist/src/governor/in-memory-event-bus.js +79 -0
- package/dist/src/governor/index.d.ts +14 -0
- package/dist/src/governor/index.d.ts.map +1 -0
- package/dist/src/governor/index.js +13 -0
- package/dist/src/governor/override-parser.d.ts +60 -0
- package/dist/src/governor/override-parser.d.ts.map +1 -0
- package/dist/src/governor/override-parser.js +98 -0
- package/dist/src/governor/override-parser.test.d.ts +2 -0
- package/dist/src/governor/override-parser.test.d.ts.map +1 -0
- package/dist/src/governor/override-parser.test.js +312 -0
- package/dist/src/governor/platform-adapter.d.ts +69 -0
- package/dist/src/governor/platform-adapter.d.ts.map +1 -0
- package/dist/src/governor/platform-adapter.js +11 -0
- package/dist/src/governor/processing-state.d.ts +66 -0
- package/dist/src/governor/processing-state.d.ts.map +1 -0
- package/dist/src/governor/processing-state.js +43 -0
- package/dist/src/governor/processing-state.test.d.ts +2 -0
- package/dist/src/governor/processing-state.test.d.ts.map +1 -0
- package/dist/src/governor/processing-state.test.js +96 -0
- package/dist/src/governor/top-of-funnel.d.ts +118 -0
- package/dist/src/governor/top-of-funnel.d.ts.map +1 -0
- package/dist/src/governor/top-of-funnel.js +168 -0
- package/dist/src/governor/top-of-funnel.test.d.ts +2 -0
- package/dist/src/governor/top-of-funnel.test.d.ts.map +1 -0
- package/dist/src/governor/top-of-funnel.test.js +331 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +10 -0
- package/dist/src/linear-cli.d.ts +38 -0
- package/dist/src/linear-cli.d.ts.map +1 -0
- package/dist/src/linear-cli.js +674 -0
- package/dist/src/logger.d.ts +117 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +430 -0
- package/dist/src/manifest/generate.d.ts +20 -0
- package/dist/src/manifest/generate.d.ts.map +1 -0
- package/dist/src/manifest/generate.js +65 -0
- package/dist/src/manifest/index.d.ts +4 -0
- package/dist/src/manifest/index.d.ts.map +1 -0
- package/dist/src/manifest/index.js +2 -0
- package/dist/src/manifest/route-manifest.d.ts +34 -0
- package/dist/src/manifest/route-manifest.d.ts.map +1 -0
- package/dist/src/manifest/route-manifest.js +148 -0
- package/dist/src/orchestrator/activity-emitter.d.ts +119 -0
- package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/activity-emitter.js +306 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/api-activity-emitter.js +417 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
- package/dist/src/orchestrator/heartbeat-writer.js +137 -0
- package/dist/src/orchestrator/index.d.ts +20 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -0
- package/dist/src/orchestrator/index.js +22 -0
- package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
- package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
- package/dist/src/orchestrator/log-analyzer.js +572 -0
- package/dist/src/orchestrator/log-config.d.ts +39 -0
- package/dist/src/orchestrator/log-config.d.ts.map +1 -0
- package/dist/src/orchestrator/log-config.js +45 -0
- package/dist/src/orchestrator/orchestrator.d.ts +316 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/src/orchestrator/orchestrator.js +3290 -0
- package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.js +135 -0
- package/dist/src/orchestrator/parse-work-result.test.d.ts +2 -0
- package/dist/src/orchestrator/parse-work-result.test.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.test.js +234 -0
- package/dist/src/orchestrator/progress-logger.d.ts +72 -0
- package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/progress-logger.js +135 -0
- package/dist/src/orchestrator/session-logger.d.ts +159 -0
- package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/session-logger.js +275 -0
- package/dist/src/orchestrator/state-recovery.d.ts +96 -0
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
- package/dist/src/orchestrator/state-recovery.js +302 -0
- package/dist/src/orchestrator/state-types.d.ts +165 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -0
- package/dist/src/orchestrator/state-types.js +7 -0
- package/dist/src/orchestrator/stream-parser.d.ts +151 -0
- package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
- package/dist/src/orchestrator/stream-parser.js +137 -0
- package/dist/src/orchestrator/types.d.ts +232 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +4 -0
- package/dist/src/orchestrator/validate-git-remote.test.d.ts +2 -0
- package/dist/src/orchestrator/validate-git-remote.test.d.ts.map +1 -0
- package/dist/src/orchestrator/validate-git-remote.test.js +61 -0
- package/dist/src/providers/a2a-auth.d.ts +81 -0
- package/dist/src/providers/a2a-auth.d.ts.map +1 -0
- package/dist/src/providers/a2a-auth.js +188 -0
- package/dist/src/providers/a2a-auth.test.d.ts +2 -0
- package/dist/src/providers/a2a-auth.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-auth.test.js +232 -0
- package/dist/src/providers/a2a-provider.d.ts +254 -0
- package/dist/src/providers/a2a-provider.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.integration.test.d.ts +9 -0
- package/dist/src/providers/a2a-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.integration.test.js +665 -0
- package/dist/src/providers/a2a-provider.js +811 -0
- package/dist/src/providers/a2a-provider.test.d.ts +2 -0
- package/dist/src/providers/a2a-provider.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.test.js +681 -0
- package/dist/src/providers/amp-provider.d.ts +20 -0
- package/dist/src/providers/amp-provider.d.ts.map +1 -0
- package/dist/src/providers/amp-provider.js +24 -0
- package/dist/src/providers/claude-provider.d.ts +18 -0
- package/dist/src/providers/claude-provider.d.ts.map +1 -0
- package/dist/src/providers/claude-provider.js +437 -0
- package/dist/src/providers/codex-provider.d.ts +133 -0
- package/dist/src/providers/codex-provider.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.js +381 -0
- package/dist/src/providers/codex-provider.test.d.ts +2 -0
- package/dist/src/providers/codex-provider.test.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.test.js +387 -0
- package/dist/src/providers/index.d.ts +44 -0
- package/dist/src/providers/index.d.ts.map +1 -0
- package/dist/src/providers/index.js +85 -0
- package/dist/src/providers/spring-ai-provider.d.ts +90 -0
- package/dist/src/providers/spring-ai-provider.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.integration.test.d.ts +13 -0
- package/dist/src/providers/spring-ai-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.integration.test.js +351 -0
- package/dist/src/providers/spring-ai-provider.js +317 -0
- package/dist/src/providers/spring-ai-provider.test.d.ts +2 -0
- package/dist/src/providers/spring-ai-provider.test.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.test.js +200 -0
- package/dist/src/providers/types.d.ts +165 -0
- package/dist/src/providers/types.d.ts.map +1 -0
- package/dist/src/providers/types.js +13 -0
- package/dist/src/templates/adapters.d.ts +51 -0
- package/dist/src/templates/adapters.d.ts.map +1 -0
- package/dist/src/templates/adapters.js +104 -0
- package/dist/src/templates/adapters.test.d.ts +2 -0
- package/dist/src/templates/adapters.test.d.ts.map +1 -0
- package/dist/src/templates/adapters.test.js +165 -0
- package/dist/src/templates/agent-definition.d.ts +85 -0
- package/dist/src/templates/agent-definition.d.ts.map +1 -0
- package/dist/src/templates/agent-definition.js +97 -0
- package/dist/src/templates/agent-definition.test.d.ts +2 -0
- package/dist/src/templates/agent-definition.test.d.ts.map +1 -0
- package/dist/src/templates/agent-definition.test.js +209 -0
- package/dist/src/templates/index.d.ts +14 -0
- package/dist/src/templates/index.d.ts.map +1 -0
- package/dist/src/templates/index.js +11 -0
- package/dist/src/templates/loader.d.ts +41 -0
- package/dist/src/templates/loader.d.ts.map +1 -0
- package/dist/src/templates/loader.js +114 -0
- package/dist/src/templates/registry.d.ts +80 -0
- package/dist/src/templates/registry.d.ts.map +1 -0
- package/dist/src/templates/registry.js +177 -0
- package/dist/src/templates/registry.test.d.ts +2 -0
- package/dist/src/templates/registry.test.d.ts.map +1 -0
- package/dist/src/templates/registry.test.js +198 -0
- package/dist/src/templates/renderer.d.ts +29 -0
- package/dist/src/templates/renderer.d.ts.map +1 -0
- package/dist/src/templates/renderer.js +35 -0
- package/dist/src/templates/strategy-templates.test.d.ts +2 -0
- package/dist/src/templates/strategy-templates.test.d.ts.map +1 -0
- package/dist/src/templates/strategy-templates.test.js +619 -0
- package/dist/src/templates/types.d.ts +233 -0
- package/dist/src/templates/types.d.ts.map +1 -0
- package/dist/src/templates/types.js +127 -0
- package/dist/src/templates/types.test.d.ts +2 -0
- package/dist/src/templates/types.test.d.ts.map +1 -0
- package/dist/src/templates/types.test.js +232 -0
- package/dist/src/tools/index.d.ts +6 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +3 -0
- package/dist/src/tools/linear-runner.d.ts +34 -0
- package/dist/src/tools/linear-runner.d.ts.map +1 -0
- package/dist/src/tools/linear-runner.js +700 -0
- package/dist/src/tools/plugins/linear.d.ts +9 -0
- package/dist/src/tools/plugins/linear.d.ts.map +1 -0
- package/dist/src/tools/plugins/linear.js +138 -0
- package/dist/src/tools/registry.d.ts +9 -0
- package/dist/src/tools/registry.d.ts.map +1 -0
- package/dist/src/tools/registry.js +18 -0
- package/dist/src/tools/types.d.ts +18 -0
- package/dist/src/tools/types.d.ts.map +1 -0
- package/dist/src/tools/types.js +1 -0
- 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 @@
|
|
|
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"}
|