@renseiai/agentfactory 0.8.7 → 0.8.9
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/dist/src/config/index.d.ts +1 -1
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js +1 -1
- package/dist/src/config/repository-config.d.ts +37 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +47 -0
- package/dist/src/config/repository-config.test.js +140 -1
- package/dist/src/governor/decision-engine.d.ts +3 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -1
- package/dist/src/governor/decision-engine.js +11 -0
- package/dist/src/governor/decision-engine.test.js +33 -0
- package/dist/src/governor/event-types.d.ts +18 -1
- package/dist/src/governor/event-types.d.ts.map +1 -1
- package/dist/src/governor/event-types.js +4 -0
- package/dist/src/governor/governor-types.d.ts +1 -1
- package/dist/src/governor/governor-types.d.ts.map +1 -1
- package/dist/src/governor/governor.d.ts +17 -1
- package/dist/src/governor/governor.d.ts.map +1 -1
- package/dist/src/governor/governor.js +112 -1
- package/dist/src/governor/governor.test.js +155 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts +22 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/github-native.js +243 -0
- package/dist/src/merge-queue/adapters/github-native.test.d.ts +2 -0
- package/dist/src/merge-queue/adapters/github-native.test.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/github-native.test.js +384 -0
- package/dist/src/merge-queue/index.d.ts +18 -0
- package/dist/src/merge-queue/index.d.ts.map +1 -0
- package/dist/src/merge-queue/index.js +28 -0
- package/dist/src/merge-queue/merge-queue.integration.test.d.ts +2 -0
- package/dist/src/merge-queue/merge-queue.integration.test.d.ts.map +1 -0
- package/dist/src/merge-queue/merge-queue.integration.test.js +128 -0
- package/dist/src/merge-queue/types.d.ts +48 -0
- package/dist/src/merge-queue/types.d.ts.map +1 -0
- package/dist/src/merge-queue/types.js +8 -0
- package/dist/src/orchestrator/artifact-tracker.d.ts +93 -0
- package/dist/src/orchestrator/artifact-tracker.d.ts.map +1 -0
- package/dist/src/orchestrator/artifact-tracker.js +235 -0
- package/dist/src/orchestrator/artifact-tracker.test.d.ts +2 -0
- package/dist/src/orchestrator/artifact-tracker.test.d.ts.map +1 -0
- package/dist/src/orchestrator/artifact-tracker.test.js +189 -0
- package/dist/src/orchestrator/context-manager.d.ts +72 -0
- package/dist/src/orchestrator/context-manager.d.ts.map +1 -0
- package/dist/src/orchestrator/context-manager.js +120 -0
- package/dist/src/orchestrator/context-manager.test.d.ts +2 -0
- package/dist/src/orchestrator/context-manager.test.d.ts.map +1 -0
- package/dist/src/orchestrator/context-manager.test.js +137 -0
- package/dist/src/orchestrator/index.d.ts +8 -2
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +8 -1
- package/dist/src/orchestrator/issue-tracker-client.d.ts +4 -0
- package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.d.ts +12 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +282 -2
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +6 -0
- package/dist/src/orchestrator/parse-work-result.test.js +19 -0
- package/dist/src/orchestrator/state-recovery.d.ts +21 -2
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
- package/dist/src/orchestrator/state-recovery.js +54 -2
- package/dist/src/orchestrator/state-recovery.test.js +106 -2
- package/dist/src/orchestrator/state-types.d.ts +62 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -1
- package/dist/src/orchestrator/state-types.js +5 -1
- package/dist/src/orchestrator/summary-builder.d.ts +47 -0
- package/dist/src/orchestrator/summary-builder.d.ts.map +1 -0
- package/dist/src/orchestrator/summary-builder.js +240 -0
- package/dist/src/orchestrator/summary-builder.test.d.ts +2 -0
- package/dist/src/orchestrator/summary-builder.test.d.ts.map +1 -0
- package/dist/src/orchestrator/summary-builder.test.js +236 -0
- package/dist/src/orchestrator/types.d.ts +2 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -1
- package/dist/src/orchestrator/work-types.d.ts +1 -1
- package/dist/src/orchestrator/work-types.d.ts.map +1 -1
- package/dist/src/providers/index.d.ts +64 -1
- package/dist/src/providers/index.d.ts.map +1 -1
- package/dist/src/providers/index.js +132 -1
- package/dist/src/providers/index.test.js +340 -2
- package/dist/src/routing/index.d.ts +7 -0
- package/dist/src/routing/index.d.ts.map +1 -0
- package/dist/src/routing/index.js +6 -0
- package/dist/src/routing/observation-recorder.d.ts +19 -0
- package/dist/src/routing/observation-recorder.d.ts.map +1 -0
- package/dist/src/routing/observation-recorder.js +73 -0
- package/dist/src/routing/observation-recorder.test.d.ts +2 -0
- package/dist/src/routing/observation-recorder.test.d.ts.map +1 -0
- package/dist/src/routing/observation-recorder.test.js +322 -0
- package/dist/src/routing/observation-store.d.ts +40 -0
- package/dist/src/routing/observation-store.d.ts.map +1 -0
- package/dist/src/routing/observation-store.js +1 -0
- package/dist/src/routing/observation-store.test.d.ts +2 -0
- package/dist/src/routing/observation-store.test.d.ts.map +1 -0
- package/dist/src/routing/observation-store.test.js +138 -0
- package/dist/src/routing/posterior-store.d.ts +12 -0
- package/dist/src/routing/posterior-store.d.ts.map +1 -0
- package/dist/src/routing/posterior-store.js +13 -0
- package/dist/src/routing/posterior-store.test.d.ts +2 -0
- package/dist/src/routing/posterior-store.test.d.ts.map +1 -0
- package/dist/src/routing/posterior-store.test.js +37 -0
- package/dist/src/routing/reward.d.ts +16 -0
- package/dist/src/routing/reward.d.ts.map +1 -0
- package/dist/src/routing/reward.js +29 -0
- package/dist/src/routing/reward.test.d.ts +2 -0
- package/dist/src/routing/reward.test.d.ts.map +1 -0
- package/dist/src/routing/reward.test.js +210 -0
- package/dist/src/routing/routing-engine.d.ts +20 -0
- package/dist/src/routing/routing-engine.d.ts.map +1 -0
- package/dist/src/routing/routing-engine.js +113 -0
- package/dist/src/routing/routing-engine.test.d.ts +2 -0
- package/dist/src/routing/routing-engine.test.d.ts.map +1 -0
- package/dist/src/routing/routing-engine.test.js +310 -0
- package/dist/src/routing/types.d.ts +157 -0
- package/dist/src/routing/types.d.ts.map +1 -0
- package/dist/src/routing/types.js +68 -0
- package/dist/src/routing/types.test.d.ts +2 -0
- package/dist/src/routing/types.test.d.ts.map +1 -0
- package/dist/src/routing/types.test.js +184 -0
- package/dist/src/templates/registry.test.js +2 -2
- package/dist/src/templates/types.d.ts +5 -0
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +3 -0
- package/dist/src/workflow/agent-cancellation.d.ts +37 -0
- package/dist/src/workflow/agent-cancellation.d.ts.map +1 -0
- package/dist/src/workflow/agent-cancellation.js +41 -0
- package/dist/src/workflow/agent-cancellation.test.d.ts +2 -0
- package/dist/src/workflow/agent-cancellation.test.d.ts.map +1 -0
- package/dist/src/workflow/agent-cancellation.test.js +86 -0
- package/dist/src/workflow/branching-router.d.ts +38 -0
- package/dist/src/workflow/branching-router.d.ts.map +1 -0
- package/dist/src/workflow/branching-router.js +52 -0
- package/dist/src/workflow/branching-router.test.d.ts +2 -0
- package/dist/src/workflow/branching-router.test.d.ts.map +1 -0
- package/dist/src/workflow/branching-router.test.js +209 -0
- package/dist/src/workflow/concurrency-semaphore.d.ts +21 -0
- package/dist/src/workflow/concurrency-semaphore.d.ts.map +1 -0
- package/dist/src/workflow/concurrency-semaphore.js +46 -0
- package/dist/src/workflow/concurrency-semaphore.test.d.ts +2 -0
- package/dist/src/workflow/concurrency-semaphore.test.d.ts.map +1 -0
- package/dist/src/workflow/concurrency-semaphore.test.js +183 -0
- package/dist/src/workflow/duration.d.ts +28 -0
- package/dist/src/workflow/duration.d.ts.map +1 -0
- package/dist/src/workflow/duration.js +57 -0
- package/dist/src/workflow/duration.test.d.ts +2 -0
- package/dist/src/workflow/duration.test.d.ts.map +1 -0
- package/dist/src/workflow/duration.test.js +74 -0
- package/dist/src/workflow/expression/ast.d.ts +53 -0
- package/dist/src/workflow/expression/ast.d.ts.map +1 -0
- package/dist/src/workflow/expression/ast.js +8 -0
- package/dist/src/workflow/expression/context.d.ts +40 -0
- package/dist/src/workflow/expression/context.d.ts.map +1 -0
- package/dist/src/workflow/expression/context.js +37 -0
- package/dist/src/workflow/expression/evaluator.d.ts +28 -0
- package/dist/src/workflow/expression/evaluator.d.ts.map +1 -0
- package/dist/src/workflow/expression/evaluator.js +165 -0
- package/dist/src/workflow/expression/evaluator.test.d.ts +2 -0
- package/dist/src/workflow/expression/evaluator.test.d.ts.map +1 -0
- package/dist/src/workflow/expression/evaluator.test.js +792 -0
- package/dist/src/workflow/expression/expression.test.d.ts +2 -0
- package/dist/src/workflow/expression/expression.test.d.ts.map +1 -0
- package/dist/src/workflow/expression/expression.test.js +516 -0
- package/dist/src/workflow/expression/helpers.d.ts +21 -0
- package/dist/src/workflow/expression/helpers.d.ts.map +1 -0
- package/dist/src/workflow/expression/helpers.js +56 -0
- package/dist/src/workflow/expression/index.d.ts +55 -0
- package/dist/src/workflow/expression/index.d.ts.map +1 -0
- package/dist/src/workflow/expression/index.js +71 -0
- package/dist/src/workflow/expression/lexer.d.ts +37 -0
- package/dist/src/workflow/expression/lexer.d.ts.map +1 -0
- package/dist/src/workflow/expression/lexer.js +166 -0
- package/dist/src/workflow/expression/parser.d.ts +23 -0
- package/dist/src/workflow/expression/parser.d.ts.map +1 -0
- package/dist/src/workflow/expression/parser.js +181 -0
- package/dist/src/workflow/gate-state.d.ts +115 -0
- package/dist/src/workflow/gate-state.d.ts.map +1 -0
- package/dist/src/workflow/gate-state.js +185 -0
- package/dist/src/workflow/gate-state.test.d.ts +2 -0
- package/dist/src/workflow/gate-state.test.d.ts.map +1 -0
- package/dist/src/workflow/gate-state.test.js +251 -0
- package/dist/src/workflow/gates/gate-evaluator.d.ts +119 -0
- package/dist/src/workflow/gates/gate-evaluator.d.ts.map +1 -0
- package/dist/src/workflow/gates/gate-evaluator.js +243 -0
- package/dist/src/workflow/gates/gate-evaluator.test.d.ts +2 -0
- package/dist/src/workflow/gates/gate-evaluator.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/gate-evaluator.test.js +240 -0
- package/dist/src/workflow/gates/signal-gate.d.ts +114 -0
- package/dist/src/workflow/gates/signal-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/signal-gate.js +216 -0
- package/dist/src/workflow/gates/signal-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/signal-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/signal-gate.test.js +199 -0
- package/dist/src/workflow/gates/timeout-engine.d.ts +96 -0
- package/dist/src/workflow/gates/timeout-engine.d.ts.map +1 -0
- package/dist/src/workflow/gates/timeout-engine.js +162 -0
- package/dist/src/workflow/gates/timeout-engine.test.d.ts +2 -0
- package/dist/src/workflow/gates/timeout-engine.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/timeout-engine.test.js +186 -0
- package/dist/src/workflow/gates/timer-gate.d.ts +125 -0
- package/dist/src/workflow/gates/timer-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/timer-gate.js +381 -0
- package/dist/src/workflow/gates/timer-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/timer-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/timer-gate.test.js +211 -0
- package/dist/src/workflow/gates/webhook-gate.d.ts +132 -0
- package/dist/src/workflow/gates/webhook-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/webhook-gate.js +216 -0
- package/dist/src/workflow/gates/webhook-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/webhook-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/webhook-gate.test.js +182 -0
- package/dist/src/workflow/index.d.ts +31 -3
- package/dist/src/workflow/index.d.ts.map +1 -1
- package/dist/src/workflow/index.js +20 -1
- package/dist/src/workflow/parallelism-executor.d.ts +25 -0
- package/dist/src/workflow/parallelism-executor.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-executor.js +53 -0
- package/dist/src/workflow/parallelism-executor.test.d.ts +2 -0
- package/dist/src/workflow/parallelism-executor.test.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-executor.test.js +191 -0
- package/dist/src/workflow/parallelism-types.d.ts +80 -0
- package/dist/src/workflow/parallelism-types.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-types.js +8 -0
- package/dist/src/workflow/phase-context-injector.d.ts +29 -0
- package/dist/src/workflow/phase-context-injector.d.ts.map +1 -0
- package/dist/src/workflow/phase-context-injector.js +43 -0
- package/dist/src/workflow/phase-context-injector.test.d.ts +2 -0
- package/dist/src/workflow/phase-context-injector.test.d.ts.map +1 -0
- package/dist/src/workflow/phase-context-injector.test.js +123 -0
- package/dist/src/workflow/phase-output-collector.d.ts +39 -0
- package/dist/src/workflow/phase-output-collector.d.ts.map +1 -0
- package/dist/src/workflow/phase-output-collector.js +141 -0
- package/dist/src/workflow/phase-output-collector.test.d.ts +2 -0
- package/dist/src/workflow/phase-output-collector.test.d.ts.map +1 -0
- package/dist/src/workflow/phase-output-collector.test.js +179 -0
- package/dist/src/workflow/retry-resolver.d.ts +51 -0
- package/dist/src/workflow/retry-resolver.d.ts.map +1 -0
- package/dist/src/workflow/retry-resolver.js +70 -0
- package/dist/src/workflow/retry-resolver.test.d.ts +2 -0
- package/dist/src/workflow/retry-resolver.test.d.ts.map +1 -0
- package/dist/src/workflow/retry-resolver.test.js +149 -0
- package/dist/src/workflow/strategies/fan-in-strategy.d.ts +21 -0
- package/dist/src/workflow/strategies/fan-in-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-in-strategy.js +92 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.js +182 -0
- package/dist/src/workflow/strategies/fan-out-strategy.d.ts +16 -0
- package/dist/src/workflow/strategies/fan-out-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-out-strategy.js +47 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.js +97 -0
- package/dist/src/workflow/strategies/index.d.ts +4 -0
- package/dist/src/workflow/strategies/index.d.ts.map +1 -0
- package/dist/src/workflow/strategies/index.js +3 -0
- package/dist/src/workflow/strategies/race-strategy.d.ts +19 -0
- package/dist/src/workflow/strategies/race-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/race-strategy.js +92 -0
- package/dist/src/workflow/strategies/race-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/race-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/race-strategy.test.js +318 -0
- package/dist/src/workflow/transition-engine.d.ts +3 -1
- package/dist/src/workflow/transition-engine.d.ts.map +1 -1
- package/dist/src/workflow/transition-engine.js +26 -7
- package/dist/src/workflow/transition-engine.test.js +215 -11
- package/dist/src/workflow/workflow-registry.d.ts +46 -1
- package/dist/src/workflow/workflow-registry.d.ts.map +1 -1
- package/dist/src/workflow/workflow-registry.js +74 -0
- package/dist/src/workflow/workflow-registry.test.js +54 -0
- package/dist/src/workflow/workflow-types.d.ts +330 -12
- package/dist/src/workflow/workflow-types.d.ts.map +1 -1
- package/dist/src/workflow/workflow-types.js +100 -5
- package/dist/src/workflow/workflow-types.test.js +293 -2
- package/package.json +2 -2
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal Gate Executor
|
|
3
|
+
*
|
|
4
|
+
* Evaluates whether comments or directives satisfy a signal gate.
|
|
5
|
+
* Signal gates pause workflow execution until a matching comment or
|
|
6
|
+
* directive is detected. This module is backward compatible with
|
|
7
|
+
* existing HOLD/RESUME directives from the override-parser system.
|
|
8
|
+
*
|
|
9
|
+
* All evaluator functions are pure (no I/O) — they take inputs and
|
|
10
|
+
* return results without side effects.
|
|
11
|
+
*/
|
|
12
|
+
const log = {
|
|
13
|
+
info: (msg, data) => console.log(`[signal-gate] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
14
|
+
warn: (msg, data) => console.warn(`[signal-gate] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
15
|
+
error: (msg, data) => console.error(`[signal-gate] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
16
|
+
debug: (_msg, _data) => { },
|
|
17
|
+
};
|
|
18
|
+
// ============================================
|
|
19
|
+
// Type Guards
|
|
20
|
+
// ============================================
|
|
21
|
+
/**
|
|
22
|
+
* Type guard to validate that a trigger object has the correct shape
|
|
23
|
+
* for a signal gate trigger.
|
|
24
|
+
*
|
|
25
|
+
* A valid SignalGateTrigger must have:
|
|
26
|
+
* - `source`: either 'comment' or 'directive'
|
|
27
|
+
* - `match`: a non-empty string
|
|
28
|
+
*
|
|
29
|
+
* @param trigger - The trigger record to validate
|
|
30
|
+
* @returns True if the trigger is a valid SignalGateTrigger
|
|
31
|
+
*/
|
|
32
|
+
export function isSignalGateTrigger(trigger) {
|
|
33
|
+
if (typeof trigger.source !== 'string')
|
|
34
|
+
return false;
|
|
35
|
+
if (trigger.source !== 'comment' && trigger.source !== 'directive')
|
|
36
|
+
return false;
|
|
37
|
+
if (typeof trigger.match !== 'string')
|
|
38
|
+
return false;
|
|
39
|
+
if (trigger.match.length === 0)
|
|
40
|
+
return false;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
// ============================================
|
|
44
|
+
// Signal Gate Evaluator
|
|
45
|
+
// ============================================
|
|
46
|
+
/**
|
|
47
|
+
* Extract the first non-empty line from a comment body.
|
|
48
|
+
* Mirrors the directive extraction logic in override-parser.ts.
|
|
49
|
+
*
|
|
50
|
+
* @param body - The full comment body text
|
|
51
|
+
* @returns The trimmed first non-empty line, or empty string if none
|
|
52
|
+
*/
|
|
53
|
+
function extractDirectiveLine(body) {
|
|
54
|
+
const lines = body.split('\n');
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (trimmed.length > 0) {
|
|
58
|
+
return trimmed;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Evaluate whether a comment satisfies a signal gate trigger.
|
|
65
|
+
*
|
|
66
|
+
* This is a pure function (no I/O) that checks if a comment matches
|
|
67
|
+
* the signal gate's trigger configuration.
|
|
68
|
+
*
|
|
69
|
+
* Matching rules:
|
|
70
|
+
* - Bot comments are always skipped (returns { matched: false })
|
|
71
|
+
* - When `trigger.source` is 'directive', only the first non-empty line
|
|
72
|
+
* of the comment is checked (consistent with override-parser.ts)
|
|
73
|
+
* - When `trigger.source` is 'comment', the full trimmed comment text is checked
|
|
74
|
+
* - The `trigger.match` string is first tried as an exact case-insensitive match,
|
|
75
|
+
* then as a regex pattern (case-insensitive)
|
|
76
|
+
*
|
|
77
|
+
* @param gate - The gate definition containing a signal trigger
|
|
78
|
+
* @param comment - The full comment body text
|
|
79
|
+
* @param isBot - Whether the comment was authored by a bot
|
|
80
|
+
* @returns A SignalGateResult indicating whether the comment matched
|
|
81
|
+
*/
|
|
82
|
+
export function evaluateSignalGate(gate, comment, isBot) {
|
|
83
|
+
// Bot comments are always ignored
|
|
84
|
+
if (isBot) {
|
|
85
|
+
log.debug('Skipping bot comment for signal gate', { gateName: gate.name });
|
|
86
|
+
return { matched: false };
|
|
87
|
+
}
|
|
88
|
+
// Validate trigger shape
|
|
89
|
+
if (!isSignalGateTrigger(gate.trigger)) {
|
|
90
|
+
log.warn('Gate has invalid signal trigger configuration', { gateName: gate.name, trigger: gate.trigger });
|
|
91
|
+
return { matched: false };
|
|
92
|
+
}
|
|
93
|
+
const trigger = gate.trigger;
|
|
94
|
+
// Determine the text to match against based on trigger source
|
|
95
|
+
let textToMatch;
|
|
96
|
+
if (trigger.source === 'directive') {
|
|
97
|
+
textToMatch = extractDirectiveLine(comment);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
textToMatch = comment.trim();
|
|
101
|
+
}
|
|
102
|
+
// Empty text cannot match
|
|
103
|
+
if (textToMatch.length === 0) {
|
|
104
|
+
return { matched: false };
|
|
105
|
+
}
|
|
106
|
+
// Try exact case-insensitive match first
|
|
107
|
+
if (textToMatch.toLowerCase() === trigger.match.toLowerCase()) {
|
|
108
|
+
log.debug('Signal gate matched (exact)', { gateName: gate.name, source: textToMatch });
|
|
109
|
+
return { matched: true, source: textToMatch };
|
|
110
|
+
}
|
|
111
|
+
// Try regex match (case-insensitive)
|
|
112
|
+
try {
|
|
113
|
+
const regex = new RegExp(trigger.match, 'i');
|
|
114
|
+
const match = textToMatch.match(regex);
|
|
115
|
+
if (match) {
|
|
116
|
+
log.debug('Signal gate matched (regex)', { gateName: gate.name, source: textToMatch });
|
|
117
|
+
return { matched: true, source: textToMatch };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Invalid regex pattern — treat as exact match only (already tried above)
|
|
122
|
+
log.warn('Invalid regex in signal gate trigger match', { gateName: gate.name, match: trigger.match });
|
|
123
|
+
}
|
|
124
|
+
return { matched: false };
|
|
125
|
+
}
|
|
126
|
+
// ============================================
|
|
127
|
+
// Workflow Query Helpers
|
|
128
|
+
// ============================================
|
|
129
|
+
/**
|
|
130
|
+
* Get all signal gates from a workflow definition that apply to a given phase.
|
|
131
|
+
*
|
|
132
|
+
* Filters gates by:
|
|
133
|
+
* 1. `type === 'signal'`
|
|
134
|
+
* 2. `appliesTo` includes the given phase name, OR `appliesTo` is not defined
|
|
135
|
+
* (gate applies to all phases)
|
|
136
|
+
*
|
|
137
|
+
* @param workflow - The workflow definition containing gate configurations
|
|
138
|
+
* @param phase - The phase name to filter by
|
|
139
|
+
* @returns Array of GateDefinition objects that are signal gates applicable to the phase
|
|
140
|
+
*/
|
|
141
|
+
export function getApplicableSignalGates(workflow, phase) {
|
|
142
|
+
if (!workflow.gates || workflow.gates.length === 0) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
return workflow.gates.filter((gate) => {
|
|
146
|
+
// Must be a signal gate
|
|
147
|
+
if (gate.type !== 'signal')
|
|
148
|
+
return false;
|
|
149
|
+
// If appliesTo is defined, the phase must be in the list
|
|
150
|
+
if (gate.appliesTo && gate.appliesTo.length > 0) {
|
|
151
|
+
return gate.appliesTo.includes(phase);
|
|
152
|
+
}
|
|
153
|
+
// No appliesTo restriction — applies to all phases
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// ============================================
|
|
158
|
+
// HOLD/RESUME Backward Compatibility
|
|
159
|
+
// ============================================
|
|
160
|
+
/**
|
|
161
|
+
* Name used for the implicit HOLD gate created for backward compatibility
|
|
162
|
+
*/
|
|
163
|
+
export const IMPLICIT_HOLD_GATE_NAME = '__implicit-hold';
|
|
164
|
+
/**
|
|
165
|
+
* Name used for the implicit RESUME gate created for backward compatibility
|
|
166
|
+
*/
|
|
167
|
+
export const IMPLICIT_RESUME_GATE_NAME = '__implicit-resume';
|
|
168
|
+
/**
|
|
169
|
+
* Create an implicit signal gate definition that matches the HOLD directive.
|
|
170
|
+
*
|
|
171
|
+
* This provides backward compatibility with the existing HOLD/RESUME system.
|
|
172
|
+
* When a HOLD directive is detected and no explicit signal gate is defined,
|
|
173
|
+
* this creates a gate that will pause the workflow until a RESUME directive
|
|
174
|
+
* is received.
|
|
175
|
+
*
|
|
176
|
+
* The created gate matches:
|
|
177
|
+
* - Directive source: first line of comment
|
|
178
|
+
* - Pattern: `^hold(?:\s*[---]\s*(.+))?$` (matches HOLD or HOLD -- reason)
|
|
179
|
+
*
|
|
180
|
+
* @returns A GateDefinition representing the implicit HOLD gate
|
|
181
|
+
*/
|
|
182
|
+
export function createImplicitHoldGate() {
|
|
183
|
+
return {
|
|
184
|
+
name: IMPLICIT_HOLD_GATE_NAME,
|
|
185
|
+
description: 'Implicit gate created for backward-compatible HOLD directive',
|
|
186
|
+
type: 'signal',
|
|
187
|
+
trigger: {
|
|
188
|
+
source: 'directive',
|
|
189
|
+
match: '^hold(?:\\s*[\\u2014\\u2013-]\\s*(.+))?$',
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Create an implicit signal gate definition that matches the RESUME directive.
|
|
195
|
+
*
|
|
196
|
+
* This is the counterpart to the implicit HOLD gate. When a workflow is
|
|
197
|
+
* paused by a HOLD directive, this gate defines the condition that will
|
|
198
|
+
* release the hold — receiving a RESUME directive.
|
|
199
|
+
*
|
|
200
|
+
* The created gate matches:
|
|
201
|
+
* - Directive source: first line of comment
|
|
202
|
+
* - Pattern: `^resume$` (exact match for RESUME directive)
|
|
203
|
+
*
|
|
204
|
+
* @returns A GateDefinition representing the implicit RESUME gate
|
|
205
|
+
*/
|
|
206
|
+
export function createImplicitResumeGate() {
|
|
207
|
+
return {
|
|
208
|
+
name: IMPLICIT_RESUME_GATE_NAME,
|
|
209
|
+
description: 'Implicit gate created for backward-compatible RESUME directive',
|
|
210
|
+
type: 'signal',
|
|
211
|
+
trigger: {
|
|
212
|
+
source: 'directive',
|
|
213
|
+
match: '^resume$',
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal-gate.test.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/signal-gate.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { evaluateSignalGate, isSignalGateTrigger, getApplicableSignalGates, createImplicitHoldGate, createImplicitResumeGate, IMPLICIT_HOLD_GATE_NAME, IMPLICIT_RESUME_GATE_NAME, } from './signal-gate.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function makeGateDefinition(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
name: 'test-gate',
|
|
9
|
+
type: 'signal',
|
|
10
|
+
trigger: { source: 'comment', match: 'APPROVE' },
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function makeWorkflow(gates = []) {
|
|
15
|
+
return {
|
|
16
|
+
apiVersion: 'v1.1',
|
|
17
|
+
kind: 'WorkflowDefinition',
|
|
18
|
+
metadata: { name: 'test-workflow' },
|
|
19
|
+
phases: [{ name: 'development', template: 'dev' }],
|
|
20
|
+
transitions: [{ from: 'Backlog', to: 'development' }],
|
|
21
|
+
gates,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// evaluateSignalGate
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
describe('evaluateSignalGate', () => {
|
|
28
|
+
it('matches exact comment text (case-insensitive)', () => {
|
|
29
|
+
const gate = makeGateDefinition({ trigger: { source: 'comment', match: 'APPROVE' } });
|
|
30
|
+
const result = evaluateSignalGate(gate, 'approve', false);
|
|
31
|
+
expect(result.matched).toBe(true);
|
|
32
|
+
expect(result.source).toBe('approve');
|
|
33
|
+
});
|
|
34
|
+
it('matches exact comment text (exact case)', () => {
|
|
35
|
+
const gate = makeGateDefinition({ trigger: { source: 'comment', match: 'APPROVE' } });
|
|
36
|
+
const result = evaluateSignalGate(gate, 'APPROVE', false);
|
|
37
|
+
expect(result.matched).toBe(true);
|
|
38
|
+
expect(result.source).toBe('APPROVE');
|
|
39
|
+
});
|
|
40
|
+
it('matches regex pattern', () => {
|
|
41
|
+
const gate = makeGateDefinition({ trigger: { source: 'comment', match: '^LGTM.*$' } });
|
|
42
|
+
const result = evaluateSignalGate(gate, 'LGTM looks good', false);
|
|
43
|
+
expect(result.matched).toBe(true);
|
|
44
|
+
expect(result.source).toBe('LGTM looks good');
|
|
45
|
+
});
|
|
46
|
+
it('handles directive-only mode (first non-empty line)', () => {
|
|
47
|
+
const gate = makeGateDefinition({ trigger: { source: 'directive', match: 'APPROVE' } });
|
|
48
|
+
const result = evaluateSignalGate(gate, 'APPROVE\nsome other text', false);
|
|
49
|
+
expect(result.matched).toBe(true);
|
|
50
|
+
expect(result.source).toBe('APPROVE');
|
|
51
|
+
});
|
|
52
|
+
it('skips bot comments', () => {
|
|
53
|
+
const gate = makeGateDefinition({ trigger: { source: 'comment', match: 'APPROVE' } });
|
|
54
|
+
const result = evaluateSignalGate(gate, 'APPROVE', true);
|
|
55
|
+
expect(result.matched).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
it('handles invalid regex gracefully (falls back to exact match only)', () => {
|
|
58
|
+
const gate = makeGateDefinition({ trigger: { source: 'comment', match: '[invalid(' } });
|
|
59
|
+
const result = evaluateSignalGate(gate, 'something else', false);
|
|
60
|
+
expect(result.matched).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it('is case-insensitive for regex matching', () => {
|
|
63
|
+
const gate = makeGateDefinition({ trigger: { source: 'comment', match: '^approve$' } });
|
|
64
|
+
const result = evaluateSignalGate(gate, 'APPROVE', false);
|
|
65
|
+
expect(result.matched).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it('returns not matched for empty comment with directive source', () => {
|
|
68
|
+
const gate = makeGateDefinition({ trigger: { source: 'directive', match: 'APPROVE' } });
|
|
69
|
+
const result = evaluateSignalGate(gate, '', false);
|
|
70
|
+
expect(result.matched).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it('returns not matched for non-matching comment', () => {
|
|
73
|
+
const gate = makeGateDefinition({ trigger: { source: 'comment', match: 'APPROVE' } });
|
|
74
|
+
const result = evaluateSignalGate(gate, 'I reject this', false);
|
|
75
|
+
expect(result.matched).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
it('returns not matched for invalid trigger configuration', () => {
|
|
78
|
+
const gate = makeGateDefinition({ trigger: { invalid: true } });
|
|
79
|
+
const result = evaluateSignalGate(gate, 'APPROVE', false);
|
|
80
|
+
expect(result.matched).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
it('trims whitespace from comment when source is comment', () => {
|
|
83
|
+
const gate = makeGateDefinition({ trigger: { source: 'comment', match: 'APPROVE' } });
|
|
84
|
+
const result = evaluateSignalGate(gate, ' APPROVE ', false);
|
|
85
|
+
expect(result.matched).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// isSignalGateTrigger
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
describe('isSignalGateTrigger', () => {
|
|
92
|
+
it('returns true for valid signal trigger', () => {
|
|
93
|
+
expect(isSignalGateTrigger({ source: 'comment', match: 'APPROVE' })).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
it('returns true for directive source', () => {
|
|
96
|
+
expect(isSignalGateTrigger({ source: 'directive', match: 'HOLD' })).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it('returns false for missing source', () => {
|
|
99
|
+
expect(isSignalGateTrigger({ match: 'APPROVE' })).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
it('returns false for invalid source type', () => {
|
|
102
|
+
expect(isSignalGateTrigger({ source: 'webhook', match: 'APPROVE' })).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
it('returns false for missing match', () => {
|
|
105
|
+
expect(isSignalGateTrigger({ source: 'comment' })).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
it('returns false for empty match', () => {
|
|
108
|
+
expect(isSignalGateTrigger({ source: 'comment', match: '' })).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// getApplicableSignalGates
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
describe('getApplicableSignalGates', () => {
|
|
115
|
+
it('filters by type=signal and appliesTo', () => {
|
|
116
|
+
const gates = [
|
|
117
|
+
makeGateDefinition({ name: 'signal-1', type: 'signal', appliesTo: ['development'] }),
|
|
118
|
+
makeGateDefinition({ name: 'timer-1', type: 'timer', appliesTo: ['development'] }),
|
|
119
|
+
makeGateDefinition({ name: 'signal-2', type: 'signal', appliesTo: ['qa'] }),
|
|
120
|
+
];
|
|
121
|
+
const workflow = makeWorkflow(gates);
|
|
122
|
+
const result = getApplicableSignalGates(workflow, 'development');
|
|
123
|
+
expect(result).toHaveLength(1);
|
|
124
|
+
expect(result[0].name).toBe('signal-1');
|
|
125
|
+
});
|
|
126
|
+
it('handles no gates', () => {
|
|
127
|
+
const workflow = makeWorkflow([]);
|
|
128
|
+
const result = getApplicableSignalGates(workflow, 'development');
|
|
129
|
+
expect(result).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
it('handles undefined gates', () => {
|
|
132
|
+
const workflow = makeWorkflow();
|
|
133
|
+
delete workflow.gates;
|
|
134
|
+
const result = getApplicableSignalGates(workflow, 'development');
|
|
135
|
+
expect(result).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
it('returns gates with no appliesTo (applies to all phases)', () => {
|
|
138
|
+
const gates = [
|
|
139
|
+
makeGateDefinition({ name: 'global-gate', type: 'signal' }),
|
|
140
|
+
];
|
|
141
|
+
const workflow = makeWorkflow(gates);
|
|
142
|
+
const result = getApplicableSignalGates(workflow, 'any-phase');
|
|
143
|
+
expect(result).toHaveLength(1);
|
|
144
|
+
expect(result[0].name).toBe('global-gate');
|
|
145
|
+
});
|
|
146
|
+
it('returns gates with empty appliesTo array (applies to all phases)', () => {
|
|
147
|
+
const gates = [
|
|
148
|
+
makeGateDefinition({ name: 'global-gate', type: 'signal', appliesTo: [] }),
|
|
149
|
+
];
|
|
150
|
+
const workflow = makeWorkflow(gates);
|
|
151
|
+
const result = getApplicableSignalGates(workflow, 'any-phase');
|
|
152
|
+
expect(result).toHaveLength(1);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// createImplicitHoldGate
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
describe('createImplicitHoldGate', () => {
|
|
159
|
+
it('creates a gate matching HOLD directive', () => {
|
|
160
|
+
const gate = createImplicitHoldGate();
|
|
161
|
+
expect(gate.name).toBe(IMPLICIT_HOLD_GATE_NAME);
|
|
162
|
+
expect(gate.type).toBe('signal');
|
|
163
|
+
// Should match "HOLD"
|
|
164
|
+
const result = evaluateSignalGate(gate, 'HOLD', false);
|
|
165
|
+
expect(result.matched).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
it('matches HOLD with reason (HOLD -- reason)', () => {
|
|
168
|
+
const gate = createImplicitHoldGate();
|
|
169
|
+
const result = evaluateSignalGate(gate, 'HOLD -- waiting for design review', false);
|
|
170
|
+
expect(result.matched).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
it('matches HOLD with em-dash', () => {
|
|
173
|
+
const gate = createImplicitHoldGate();
|
|
174
|
+
const result = evaluateSignalGate(gate, 'HOLD \u2014 some reason', false);
|
|
175
|
+
expect(result.matched).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// createImplicitResumeGate
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
describe('createImplicitResumeGate', () => {
|
|
182
|
+
it('creates a gate matching RESUME directive', () => {
|
|
183
|
+
const gate = createImplicitResumeGate();
|
|
184
|
+
expect(gate.name).toBe(IMPLICIT_RESUME_GATE_NAME);
|
|
185
|
+
expect(gate.type).toBe('signal');
|
|
186
|
+
const result = evaluateSignalGate(gate, 'RESUME', false);
|
|
187
|
+
expect(result.matched).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
it('matches case-insensitively', () => {
|
|
190
|
+
const gate = createImplicitResumeGate();
|
|
191
|
+
const result = evaluateSignalGate(gate, 'resume', false);
|
|
192
|
+
expect(result.matched).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
it('does not match RESUME with extra text', () => {
|
|
195
|
+
const gate = createImplicitResumeGate();
|
|
196
|
+
const result = evaluateSignalGate(gate, 'RESUME now please', false);
|
|
197
|
+
expect(result.matched).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Timeout Engine
|
|
3
|
+
*
|
|
4
|
+
* Cross-cutting timeout engine that monitors active gates and determines
|
|
5
|
+
* what action to take when deadlines expire. Every gate type (signal, timer,
|
|
6
|
+
* webhook) can have an optional timeout with an action of 'escalate', 'skip',
|
|
7
|
+
* or 'fail'. This engine is the shared component that checks deadlines and
|
|
8
|
+
* returns the action to take.
|
|
9
|
+
*
|
|
10
|
+
* The main entry point is `processGateTimeouts()`, which is called by the
|
|
11
|
+
* governor on each poll cycle. The lower-level functions (`checkGateTimeout`,
|
|
12
|
+
* `checkAllGateTimeouts`, `resolveTimeoutAction`) are pure and exported for
|
|
13
|
+
* direct use and testability.
|
|
14
|
+
*/
|
|
15
|
+
import type { GateState, GateStorage } from '../gate-state.js';
|
|
16
|
+
/**
|
|
17
|
+
* Result of checking a single gate for timeout.
|
|
18
|
+
* If `timedOut` is false, no action field is present.
|
|
19
|
+
* If `timedOut` is true, `action` indicates what should happen.
|
|
20
|
+
*/
|
|
21
|
+
export interface TimeoutCheckResult {
|
|
22
|
+
timedOut: boolean;
|
|
23
|
+
action?: 'escalate' | 'skip' | 'fail';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A gate that has been determined to have timed out, paired with
|
|
27
|
+
* the action that should be taken.
|
|
28
|
+
*/
|
|
29
|
+
export interface TimedOutGate {
|
|
30
|
+
gateState: GateState;
|
|
31
|
+
action: 'escalate' | 'skip' | 'fail';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The resolution produced for a timed-out gate, containing all the
|
|
35
|
+
* information the governor needs to act on the timeout.
|
|
36
|
+
*/
|
|
37
|
+
export interface TimeoutResolution {
|
|
38
|
+
type: 'escalate' | 'skip' | 'fail';
|
|
39
|
+
issueId: string;
|
|
40
|
+
gateName: string;
|
|
41
|
+
reason: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check whether a single gate has timed out.
|
|
45
|
+
*
|
|
46
|
+
* Pure function that compares the current time against the gate's
|
|
47
|
+
* `timeoutDeadline`. Returns `{ timedOut: false }` if no deadline
|
|
48
|
+
* exists or the deadline has not yet passed.
|
|
49
|
+
*
|
|
50
|
+
* @param gateState - The gate state to check
|
|
51
|
+
* @param now - Current time in epoch ms (defaults to Date.now() for testability)
|
|
52
|
+
* @returns The timeout check result
|
|
53
|
+
*/
|
|
54
|
+
export declare function checkGateTimeout(gateState: GateState, now?: number): TimeoutCheckResult;
|
|
55
|
+
/**
|
|
56
|
+
* Check multiple gates for timeouts.
|
|
57
|
+
*
|
|
58
|
+
* Pure function that filters a list of gate states down to only those
|
|
59
|
+
* that have timed out, returning each with its configured timeout action.
|
|
60
|
+
*
|
|
61
|
+
* @param gates - Array of gate states to check
|
|
62
|
+
* @param now - Current time in epoch ms (defaults to Date.now() for testability)
|
|
63
|
+
* @returns Array of gates that have timed out, with their actions
|
|
64
|
+
*/
|
|
65
|
+
export declare function checkAllGateTimeouts(gates: GateState[], now?: number): TimedOutGate[];
|
|
66
|
+
/**
|
|
67
|
+
* Determine the resolution for a timeout action.
|
|
68
|
+
*
|
|
69
|
+
* Pure function that maps a timeout action to a structured resolution
|
|
70
|
+
* containing the action type, issue context, and a human-readable reason.
|
|
71
|
+
*
|
|
72
|
+
* - `escalate` - The governor should advance the escalation strategy
|
|
73
|
+
* - `skip` - The governor should skip the gate and continue the workflow
|
|
74
|
+
* - `fail` - The governor should fail the workflow
|
|
75
|
+
*
|
|
76
|
+
* @param action - The timeout action to resolve
|
|
77
|
+
* @param issueId - The issue identifier associated with the gate
|
|
78
|
+
* @param gateName - The name of the gate that timed out
|
|
79
|
+
* @returns The structured timeout resolution
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveTimeoutAction(action: 'escalate' | 'skip' | 'fail', issueId: string, gateName: string): TimeoutResolution;
|
|
82
|
+
/**
|
|
83
|
+
* Process gate timeouts for a set of active gates.
|
|
84
|
+
*
|
|
85
|
+
* This is the main entry point called by the governor on each poll cycle.
|
|
86
|
+
* It checks all provided gates for timeouts, marks timed-out gates via
|
|
87
|
+
* the storage adapter (using `timeoutGate()` from gate-state.ts), and
|
|
88
|
+
* returns an array of timeout resolutions for the caller to act on.
|
|
89
|
+
*
|
|
90
|
+
* @param activeGates - Array of currently active gate states to check
|
|
91
|
+
* @param storage - The gate storage adapter for persisting state changes
|
|
92
|
+
* @param now - Current time in epoch ms (defaults to Date.now() for testability)
|
|
93
|
+
* @returns Array of timeout resolutions for the governor to process
|
|
94
|
+
*/
|
|
95
|
+
export declare function processGateTimeouts(activeGates: GateState[], storage: GateStorage, now?: number): Promise<TimeoutResolution[]>;
|
|
96
|
+
//# sourceMappingURL=timeout-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout-engine.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/timeout-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAqB9D;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAA;CACtC;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAA;CACrC;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAA;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAkBvF;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,YAAY,EAAE,CAYrF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,EACpC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,iBAAiB,CAwBnB;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,SAAS,EAAE,EACxB,OAAO,EAAE,WAAW,EACpB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAsC9B"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Timeout Engine
|
|
3
|
+
*
|
|
4
|
+
* Cross-cutting timeout engine that monitors active gates and determines
|
|
5
|
+
* what action to take when deadlines expire. Every gate type (signal, timer,
|
|
6
|
+
* webhook) can have an optional timeout with an action of 'escalate', 'skip',
|
|
7
|
+
* or 'fail'. This engine is the shared component that checks deadlines and
|
|
8
|
+
* returns the action to take.
|
|
9
|
+
*
|
|
10
|
+
* The main entry point is `processGateTimeouts()`, which is called by the
|
|
11
|
+
* governor on each poll cycle. The lower-level functions (`checkGateTimeout`,
|
|
12
|
+
* `checkAllGateTimeouts`, `resolveTimeoutAction`) are pure and exported for
|
|
13
|
+
* direct use and testability.
|
|
14
|
+
*/
|
|
15
|
+
import { timeoutGate } from '../gate-state.js';
|
|
16
|
+
// ============================================
|
|
17
|
+
// Logger
|
|
18
|
+
// ============================================
|
|
19
|
+
const log = {
|
|
20
|
+
info: (msg, data) => console.log(`[timeout-engine] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
21
|
+
warn: (msg, data) => console.warn(`[timeout-engine] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
22
|
+
error: (msg, data) => console.error(`[timeout-engine] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
23
|
+
debug: (_msg, _data) => { },
|
|
24
|
+
};
|
|
25
|
+
// ============================================
|
|
26
|
+
// Pure Functions
|
|
27
|
+
// ============================================
|
|
28
|
+
/**
|
|
29
|
+
* Check whether a single gate has timed out.
|
|
30
|
+
*
|
|
31
|
+
* Pure function that compares the current time against the gate's
|
|
32
|
+
* `timeoutDeadline`. Returns `{ timedOut: false }` if no deadline
|
|
33
|
+
* exists or the deadline has not yet passed.
|
|
34
|
+
*
|
|
35
|
+
* @param gateState - The gate state to check
|
|
36
|
+
* @param now - Current time in epoch ms (defaults to Date.now() for testability)
|
|
37
|
+
* @returns The timeout check result
|
|
38
|
+
*/
|
|
39
|
+
export function checkGateTimeout(gateState, now) {
|
|
40
|
+
const currentTime = now ?? Date.now();
|
|
41
|
+
// No deadline configured — cannot time out
|
|
42
|
+
if (gateState.timeoutDeadline == null) {
|
|
43
|
+
return { timedOut: false };
|
|
44
|
+
}
|
|
45
|
+
// Deadline has not passed yet
|
|
46
|
+
if (currentTime < gateState.timeoutDeadline) {
|
|
47
|
+
return { timedOut: false };
|
|
48
|
+
}
|
|
49
|
+
// Deadline has expired — return the configured action (default to 'fail')
|
|
50
|
+
return {
|
|
51
|
+
timedOut: true,
|
|
52
|
+
action: gateState.timeoutAction ?? 'fail',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check multiple gates for timeouts.
|
|
57
|
+
*
|
|
58
|
+
* Pure function that filters a list of gate states down to only those
|
|
59
|
+
* that have timed out, returning each with its configured timeout action.
|
|
60
|
+
*
|
|
61
|
+
* @param gates - Array of gate states to check
|
|
62
|
+
* @param now - Current time in epoch ms (defaults to Date.now() for testability)
|
|
63
|
+
* @returns Array of gates that have timed out, with their actions
|
|
64
|
+
*/
|
|
65
|
+
export function checkAllGateTimeouts(gates, now) {
|
|
66
|
+
const currentTime = now ?? Date.now();
|
|
67
|
+
const timedOut = [];
|
|
68
|
+
for (const gateState of gates) {
|
|
69
|
+
const result = checkGateTimeout(gateState, currentTime);
|
|
70
|
+
if (result.timedOut && result.action) {
|
|
71
|
+
timedOut.push({ gateState, action: result.action });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return timedOut;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Determine the resolution for a timeout action.
|
|
78
|
+
*
|
|
79
|
+
* Pure function that maps a timeout action to a structured resolution
|
|
80
|
+
* containing the action type, issue context, and a human-readable reason.
|
|
81
|
+
*
|
|
82
|
+
* - `escalate` - The governor should advance the escalation strategy
|
|
83
|
+
* - `skip` - The governor should skip the gate and continue the workflow
|
|
84
|
+
* - `fail` - The governor should fail the workflow
|
|
85
|
+
*
|
|
86
|
+
* @param action - The timeout action to resolve
|
|
87
|
+
* @param issueId - The issue identifier associated with the gate
|
|
88
|
+
* @param gateName - The name of the gate that timed out
|
|
89
|
+
* @returns The structured timeout resolution
|
|
90
|
+
*/
|
|
91
|
+
export function resolveTimeoutAction(action, issueId, gateName) {
|
|
92
|
+
switch (action) {
|
|
93
|
+
case 'escalate':
|
|
94
|
+
return {
|
|
95
|
+
type: 'escalate',
|
|
96
|
+
issueId,
|
|
97
|
+
gateName,
|
|
98
|
+
reason: `Gate ${gateName} timed out — escalating`,
|
|
99
|
+
};
|
|
100
|
+
case 'skip':
|
|
101
|
+
return {
|
|
102
|
+
type: 'skip',
|
|
103
|
+
issueId,
|
|
104
|
+
gateName,
|
|
105
|
+
reason: `Gate ${gateName} timed out — skipping gate`,
|
|
106
|
+
};
|
|
107
|
+
case 'fail':
|
|
108
|
+
return {
|
|
109
|
+
type: 'fail',
|
|
110
|
+
issueId,
|
|
111
|
+
gateName,
|
|
112
|
+
reason: `Gate ${gateName} timed out — failing workflow`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ============================================
|
|
117
|
+
// I/O Function
|
|
118
|
+
// ============================================
|
|
119
|
+
/**
|
|
120
|
+
* Process gate timeouts for a set of active gates.
|
|
121
|
+
*
|
|
122
|
+
* This is the main entry point called by the governor on each poll cycle.
|
|
123
|
+
* It checks all provided gates for timeouts, marks timed-out gates via
|
|
124
|
+
* the storage adapter (using `timeoutGate()` from gate-state.ts), and
|
|
125
|
+
* returns an array of timeout resolutions for the caller to act on.
|
|
126
|
+
*
|
|
127
|
+
* @param activeGates - Array of currently active gate states to check
|
|
128
|
+
* @param storage - The gate storage adapter for persisting state changes
|
|
129
|
+
* @param now - Current time in epoch ms (defaults to Date.now() for testability)
|
|
130
|
+
* @returns Array of timeout resolutions for the governor to process
|
|
131
|
+
*/
|
|
132
|
+
export async function processGateTimeouts(activeGates, storage, now) {
|
|
133
|
+
const timedOutGates = checkAllGateTimeouts(activeGates, now);
|
|
134
|
+
if (timedOutGates.length === 0) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
log.info('Processing gate timeouts', {
|
|
138
|
+
count: timedOutGates.length,
|
|
139
|
+
gates: timedOutGates.map(g => g.gateState.gateName),
|
|
140
|
+
});
|
|
141
|
+
const resolutions = [];
|
|
142
|
+
for (const { gateState, action } of timedOutGates) {
|
|
143
|
+
// Mark the gate as timed-out in storage
|
|
144
|
+
const updated = await timeoutGate(gateState.issueId, gateState.gateName, storage);
|
|
145
|
+
if (!updated) {
|
|
146
|
+
log.warn('Failed to mark gate as timed-out (gate not found or not active)', {
|
|
147
|
+
issueId: gateState.issueId,
|
|
148
|
+
gateName: gateState.gateName,
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const resolution = resolveTimeoutAction(action, gateState.issueId, gateState.gateName);
|
|
153
|
+
resolutions.push(resolution);
|
|
154
|
+
log.info('Gate timeout resolved', {
|
|
155
|
+
issueId: gateState.issueId,
|
|
156
|
+
gateName: gateState.gateName,
|
|
157
|
+
action,
|
|
158
|
+
reason: resolution.reason,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return resolutions;
|
|
162
|
+
}
|