agents-relay 2.0.141 → 2.0.143
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/cli-workflow-action.js +15 -0
- package/dist/cli-workflow.js +88 -0
- package/dist/cli.js +10 -0
- package/dist/dashboard-html.js +5 -5
- package/dist/dashboard-style.js +1 -0
- package/dist/escalation-notifier.js +92 -0
- package/dist/local-authority-store.js +29 -0
- package/dist/local-merge-delivery.js +33 -10
- package/dist/planner-child-recovery.js +44 -0
- package/dist/planner-recovery.js +30 -0
- package/dist/reconciler-review.js +9 -0
- package/dist/reconciler-runtime.js +7 -1
- package/dist/reconciler-workflow-outbox.js +11 -0
- package/dist/reconciler.js +31 -45
- package/dist/relay-job-snapshot-reads.js +11 -0
- package/dist/relay-store.js +28 -37
- package/dist/relay-workflow-operations.js +104 -0
- package/dist/relayd.js +2 -1
- package/dist/task-recovery.js +65 -15
- package/dist/troubleshooter-fallback.js +13 -5
- package/dist/workflow/action-ledger.js +75 -0
- package/dist/workflow/authoritative-conditions.js +38 -0
- package/dist/workflow/conditions.js +32 -0
- package/dist/workflow/definitions-v2.js +6 -1
- package/dist/workflow/dispatcher.js +53 -3
- package/dist/workflow/durable-publication.js +85 -0
- package/dist/workflow/durable-transition.js +9 -0
- package/dist/workflow/planner-orchestration-v2.js +15 -2
- package/dist/workflow/usage-index.js +35 -0
- package/package.json +9 -5
- package/workflow/main.mmd +54 -27
- package/workflow/planner-orchestration.mmd +21 -1
- package/workflow/review-lifecycle.mmd +4 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class ConditionRegistry {
|
|
2
|
+
conditions = new Map();
|
|
3
|
+
constructor(specs) {
|
|
4
|
+
for (const spec of specs) {
|
|
5
|
+
if (this.conditions.has(spec.id))
|
|
6
|
+
throw new Error(`duplicate condition: ${spec.id}`);
|
|
7
|
+
this.conditions.set(spec.id, spec);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
discover() { return [...this.conditions.keys()].sort(); }
|
|
11
|
+
inspect(id) {
|
|
12
|
+
const result = this.conditions.get(id);
|
|
13
|
+
if (!result)
|
|
14
|
+
throw new Error(`unknown condition: ${id}`);
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
check(id, facts) {
|
|
18
|
+
return this.inspect(id).check(facts);
|
|
19
|
+
}
|
|
20
|
+
guard(id) {
|
|
21
|
+
return ({ durableFacts }) => this.check(id, durableFacts) === 'Verified';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function booleanCondition(id) {
|
|
25
|
+
return { id, check: facts => facts[id] === true ? 'Verified' : facts[id] === false ? 'Failed' : 'Unknown' };
|
|
26
|
+
}
|
|
27
|
+
export function verifyActionEffect(registry, actionId, conditionId, facts) {
|
|
28
|
+
return { actionId, conditionId, outcome: registry.check(conditionId, facts) };
|
|
29
|
+
}
|
|
30
|
+
export function safeRetryAllowed(outcome) {
|
|
31
|
+
return outcome === 'Failed';
|
|
32
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { ConditionRegistry, booleanCondition } from './conditions.js';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { GraphRuntime } from './graph-v2.js';
|
|
3
4
|
import { WORKFLOW_EFFECT_NAMES, WORKFLOW_EVENT_NAMES, WORKFLOW_MUTATION_NAMES, } from './contracts.js';
|
|
4
5
|
const TASK_STATES = ['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED'];
|
|
5
6
|
const JOB_STATES = ['DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED'];
|
|
6
|
-
const
|
|
7
|
+
const conditionRegistry = new ConditionRegistry([
|
|
8
|
+
...new Set(['dependenciesSatisfied', 'dependenciesUnsatisfied', 'runnable', 'dispatchable', 'recoverableExecution', 'troubleshooterRecovery', 'retryAvailable', 'autonomousJob', 'platformFallbackSafe', 'notMerged', 'autoMerge', 'prOpen', 'reviewDisabled', 'implementationComplete', 'headAvailable', 'mergeCapable', 'localCompletionAllowed', 'localMergeReady'])
|
|
9
|
+
].map(booleanCondition));
|
|
10
|
+
const boolGuard = (name) => conditionRegistry.guard(name);
|
|
11
|
+
export const lifecycleConditions = conditionRegistry;
|
|
7
12
|
function eventRegistry() {
|
|
8
13
|
return Object.fromEntries(WORKFLOW_EVENT_NAMES.map(id => [id, { id }]));
|
|
9
14
|
}
|
|
@@ -58,16 +58,66 @@ export async function dispatchWorkflowGraph(runtime, options) {
|
|
|
58
58
|
let current = options.initialState;
|
|
59
59
|
const maxSteps = options.maxSteps ?? 1;
|
|
60
60
|
for (let step = 0; step < maxSteps; step += 1) {
|
|
61
|
+
let deferredTransition = null;
|
|
62
|
+
const deferredEvents = [];
|
|
61
63
|
const dispatcher = new GraphDispatcher(runtime, {
|
|
62
|
-
persistTransition: async (transition) => {
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
persistTransition: async (transition) => {
|
|
65
|
+
if (options.actionVerification)
|
|
66
|
+
deferredTransition = transition;
|
|
67
|
+
else
|
|
68
|
+
await options.persistTransition?.(transition.source, transition.target);
|
|
69
|
+
},
|
|
70
|
+
executeAction: options.executeAction ? async (actionId) => {
|
|
71
|
+
const verification = options.actionVerification;
|
|
72
|
+
if (verification) {
|
|
73
|
+
const identity = verification.identity(actionId);
|
|
74
|
+
const decision = runtime.evaluateSync({ currentState: current, input: options.input, durableFacts: options.durableFacts ?? {} });
|
|
75
|
+
if (!verification.ledger.reserveForRunningTask(identity, decision, verification.conditionId(actionId))) {
|
|
76
|
+
throw new Error('action attempt already reserved: reconcile outcome; never repeat side effect');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const receipt = await options.executeAction(actionId);
|
|
80
|
+
if (receipt.ok === false)
|
|
81
|
+
throw new Error('action returned failure receipt: reconciliation required');
|
|
82
|
+
if (verification) {
|
|
83
|
+
const identity = verification.identity(actionId);
|
|
84
|
+
const trace = verification.ledger.verify(identity.attemptKey, verification.conditions, await verification.observedFacts(actionId));
|
|
85
|
+
if (trace.outcome !== 'Verified')
|
|
86
|
+
throw new Error('action postcondition ' + trace.outcome + ': reconciliation required');
|
|
87
|
+
}
|
|
88
|
+
return receipt;
|
|
89
|
+
} : undefined,
|
|
90
|
+
publishEvent: async (event) => {
|
|
91
|
+
if (options.actionVerification)
|
|
92
|
+
deferredEvents.push(event);
|
|
93
|
+
else
|
|
94
|
+
await options.publishEvent?.(event.id, event.payload);
|
|
95
|
+
},
|
|
65
96
|
});
|
|
66
97
|
const result = await dispatcher.dispatch({
|
|
67
98
|
currentState: current,
|
|
68
99
|
input: options.input,
|
|
69
100
|
durableFacts: options.durableFacts ?? {},
|
|
70
101
|
});
|
|
102
|
+
if (options.actionVerification && result.decision.accepted) {
|
|
103
|
+
if (result.receipts.some(receipt => receipt.ok === false))
|
|
104
|
+
throw new Error('action failed: reconciliation required; transition withheld');
|
|
105
|
+
const transition = deferredTransition;
|
|
106
|
+
const publication = options.actionVerification.publication;
|
|
107
|
+
const persist = options.actionVerification.persistSqliteTransition;
|
|
108
|
+
if (!publication || !persist || transition === null)
|
|
109
|
+
throw new Error('atomic SQLite transition and outbox required');
|
|
110
|
+
const events = deferredEvents.map((event, index) => {
|
|
111
|
+
const action = result.decision.accepted ? result.decision.actions[0] : undefined;
|
|
112
|
+
if (!action)
|
|
113
|
+
throw new Error('missing durable action event identity');
|
|
114
|
+
const identity = options.actionVerification.identity(action.id);
|
|
115
|
+
return { key: `${identity.attemptKey}/emit/${index}`, eventId: event.id, payload: event.payload };
|
|
116
|
+
});
|
|
117
|
+
publication.commitTransitionAndEvents(() => persist(transition.source, transition.target), events);
|
|
118
|
+
if (options.publishEvent)
|
|
119
|
+
await publication.replay(async (event) => options.publishEvent(event.eventId, event.payload));
|
|
120
|
+
}
|
|
71
121
|
if (options.observe !== false)
|
|
72
122
|
observeDecision(options.graphName, options.input, result.decision);
|
|
73
123
|
if (!result.decision.accepted)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export class DurableWorkflowPublication {
|
|
2
|
+
db;
|
|
3
|
+
constructor(db) {
|
|
4
|
+
this.db = db;
|
|
5
|
+
db.exec(`CREATE TABLE IF NOT EXISTS workflow_event_outbox (
|
|
6
|
+
event_key TEXT PRIMARY KEY, event_id TEXT NOT NULL, payload_json TEXT NOT NULL,
|
|
7
|
+
state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending','published')),
|
|
8
|
+
created_at TEXT NOT NULL, published_at TEXT
|
|
9
|
+
)`);
|
|
10
|
+
}
|
|
11
|
+
/** State mutation callback MUST use this.db synchronously; rollback covers both writes. */
|
|
12
|
+
commitTransitionAndEvents(transition, events) {
|
|
13
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
14
|
+
try {
|
|
15
|
+
transition();
|
|
16
|
+
for (const event of (typeof events === 'function' ? events() : events))
|
|
17
|
+
this.enqueue(event.key, event.eventId, event.payload);
|
|
18
|
+
this.db.exec('COMMIT');
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
this.db.exec('ROLLBACK');
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
enqueue(key, eventId, payload) {
|
|
26
|
+
if (!key || !eventId)
|
|
27
|
+
throw new Error('event identity required');
|
|
28
|
+
const encoded = JSON.stringify(payload ?? null);
|
|
29
|
+
this.db.prepare(`INSERT OR IGNORE INTO workflow_event_outbox(event_key,event_id,payload_json,created_at) VALUES(?,?,?,?)`)
|
|
30
|
+
.run(key, eventId, encoded, new Date().toISOString());
|
|
31
|
+
const existing = this.db.prepare('SELECT event_id,payload_json FROM workflow_event_outbox WHERE event_key=?').get(key);
|
|
32
|
+
if (existing.event_id !== eventId || existing.payload_json !== encoded)
|
|
33
|
+
throw new Error('workflow event key collision');
|
|
34
|
+
}
|
|
35
|
+
pending() {
|
|
36
|
+
return this.db.prepare("SELECT event_key,event_id,payload_json FROM workflow_event_outbox WHERE state='pending' ORDER BY created_at,event_key").all().map(row => ({ key: row.event_key, eventId: row.event_id, payload: JSON.parse(row.payload_json) }));
|
|
37
|
+
}
|
|
38
|
+
async replay(publish) {
|
|
39
|
+
let delivered = 0;
|
|
40
|
+
for (const event of this.pending()) {
|
|
41
|
+
await publish(event);
|
|
42
|
+
this.db.prepare("UPDATE workflow_event_outbox SET state='published',published_at=? WHERE event_key=? AND state='pending'").run(new Date().toISOString(), event.key);
|
|
43
|
+
delivered++;
|
|
44
|
+
}
|
|
45
|
+
return delivered;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Atomic SQLite inbox for event consumers. The callback MUST mutate the same DB
|
|
49
|
+
* synchronously; duplicate deliveries return false without repeating the mutation.
|
|
50
|
+
* Consumers with external side effects must use their own idempotency protocol. */
|
|
51
|
+
export class DurableWorkflowConsumer {
|
|
52
|
+
db;
|
|
53
|
+
constructor(db) {
|
|
54
|
+
this.db = db;
|
|
55
|
+
db.exec(`CREATE TABLE IF NOT EXISTS workflow_event_inbox (
|
|
56
|
+
consumer_id TEXT NOT NULL, event_key TEXT NOT NULL, event_id TEXT NOT NULL,
|
|
57
|
+
payload_json TEXT NOT NULL, consumed_at TEXT NOT NULL,
|
|
58
|
+
PRIMARY KEY(consumer_id,event_key)
|
|
59
|
+
)`);
|
|
60
|
+
}
|
|
61
|
+
consume(consumerId, event, apply) {
|
|
62
|
+
if (!consumerId || !event.key || !event.eventId)
|
|
63
|
+
throw new Error('consumer and event identity required');
|
|
64
|
+
const payload = JSON.stringify(event.payload ?? null);
|
|
65
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
66
|
+
try {
|
|
67
|
+
const previous = this.db.prepare('SELECT event_id,payload_json FROM workflow_event_inbox WHERE consumer_id=? AND event_key=?').get(consumerId, event.key);
|
|
68
|
+
if (previous) {
|
|
69
|
+
if (previous.event_id !== event.eventId || previous.payload_json !== payload)
|
|
70
|
+
throw new Error('consumer event key collision');
|
|
71
|
+
this.db.exec('COMMIT');
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
apply();
|
|
75
|
+
this.db.prepare('INSERT INTO workflow_event_inbox(consumer_id,event_key,event_id,payload_json,consumed_at) VALUES(?,?,?,?,?)')
|
|
76
|
+
.run(consumerId, event.key, event.eventId, payload, new Date().toISOString());
|
|
77
|
+
this.db.exec('COMMIT');
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.db.exec('ROLLBACK');
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -26,11 +26,20 @@ export class DurableTaskTransitionRuntime {
|
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
28
|
const from = task.state;
|
|
29
|
+
const previousUpdatedAt = task.updatedAt;
|
|
29
30
|
const decision = tryTransitionTaskEvent(task, event, options.facts?.(job, task) ?? {});
|
|
30
31
|
if (!decision.accepted || !decision.target) {
|
|
31
32
|
return { job, task, from, decision };
|
|
32
33
|
}
|
|
33
34
|
await options.mutate?.(job, task, decision);
|
|
35
|
+
// The local SQLite writer rejects stale snapshots. Wall-clock timestamps may
|
|
36
|
+
// collide or move backwards across rapid start/terminal events; ensure the
|
|
37
|
+
// graph-selected transition has a strictly newer version before saving.
|
|
38
|
+
const previous = Date.parse(previousUpdatedAt);
|
|
39
|
+
const proposed = Date.parse(task.updatedAt);
|
|
40
|
+
if (Number.isFinite(previous) && (!Number.isFinite(proposed) || proposed <= previous)) {
|
|
41
|
+
task.updatedAt = new Date(previous + 1).toISOString();
|
|
42
|
+
}
|
|
34
43
|
await this.store.saveTask(task);
|
|
35
44
|
observability().record({
|
|
36
45
|
stage: 'transition',
|
|
@@ -6,13 +6,16 @@ const source = readFileSync(new URL('../../workflow/planner-orchestration.mmd',
|
|
|
6
6
|
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
7
7
|
export const plannerOrchestrationV2 = new GraphRuntime(source, {
|
|
8
8
|
states: new Set([
|
|
9
|
-
'idle', 'check_legacy_pending', 'check_recover', 'check_seed', 'check_planner', 'check_resolution_ownership',
|
|
9
|
+
'idle', 'check_legacy_pending', 'check_recover', 'check_plan_failure', 'check_plan_recovery', 'check_plan_recovery_exists', 'check_seed', 'check_planner', 'check_resolution_ownership',
|
|
10
10
|
'check_blocked', 'check_work', 'check_materialized', 'check_objective', 'wait', 'recovered', 'created',
|
|
11
11
|
]),
|
|
12
12
|
guards: {
|
|
13
13
|
autonomousJob: boolGuard('autonomousJob'),
|
|
14
14
|
legacyPlanningPending: boolGuard('legacyPlanningPending'),
|
|
15
15
|
recoverablePendingPlan: boolGuard('recoverablePendingPlan'),
|
|
16
|
+
terminalPlanFailure: boolGuard('terminalPlanFailure'),
|
|
17
|
+
planRecoveryExists: boolGuard('planRecoveryExists'),
|
|
18
|
+
planRecoveryVerified: boolGuard('planRecoveryVerified'),
|
|
16
19
|
hasSeedWork: boolGuard('hasSeedWork'),
|
|
17
20
|
noActivePlanner: boolGuard('noActivePlanner'),
|
|
18
21
|
resolutionOwnedByTroubleshooter: boolGuard('resolutionOwnedByTroubleshooter'),
|
|
@@ -22,6 +25,7 @@ export const plannerOrchestrationV2 = new GraphRuntime(source, {
|
|
|
22
25
|
objectiveUnsatisfied: boolGuard('objectiveUnsatisfied'),
|
|
23
26
|
},
|
|
24
27
|
actions: {
|
|
28
|
+
'planner.createRecovery': { id: 'planner.createRecovery' },
|
|
25
29
|
'planner.recoverPending': { id: 'planner.recoverPending' },
|
|
26
30
|
'planner.createRound': { id: 'planner.createRound' },
|
|
27
31
|
},
|
|
@@ -39,6 +43,12 @@ export function plannerOrchestrationFacts(job, recoverablePendingPlan) {
|
|
|
39
43
|
const scopedPlans = (job.plans ?? []).filter(plan => !Number.isFinite(slot) || Date.parse(plan.createdAt) >= slot)
|
|
40
44
|
.sort((a, b) => a.round - b.round || a.createdAt.localeCompare(b.createdAt));
|
|
41
45
|
const latestPlan = scopedPlans.at(-1);
|
|
46
|
+
const failedPlan = latestPlan?.status === 'FAILED' ? latestPlan : null;
|
|
47
|
+
const recovery = failedPlan ? job.tasks.find(task => task.agentName === 'troubleshooter' && task.recoveryPlanId === failedPlan.id) : undefined;
|
|
48
|
+
const recoveryVerified = recovery?.state === 'SUCCEEDED'
|
|
49
|
+
&& recovery.result?.data?.verified === true
|
|
50
|
+
&& typeof recovery.result?.data?.evidence === 'string'
|
|
51
|
+
&& recovery.result.data.evidence.trim().length > 0;
|
|
42
52
|
const legacyLatest = scopedPlans.length === 0
|
|
43
53
|
? scopedTasks.filter(task => task.kind === 'planner' && task.state === 'SUCCEEDED' && task.plannerResult)
|
|
44
54
|
.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)).at(-1)
|
|
@@ -49,8 +59,11 @@ export function plannerOrchestrationFacts(job, recoverablePendingPlan) {
|
|
|
49
59
|
autonomousJob: job.executionMode === 'autonomous',
|
|
50
60
|
legacyPlanningPending: job.initialPlanning?.status === 'pending',
|
|
51
61
|
recoverablePendingPlan,
|
|
62
|
+
terminalPlanFailure: Boolean(failedPlan && !failedPlan.manualPlanning && failedPlan.trigger.source !== 'unblock'),
|
|
63
|
+
planRecoveryExists: Boolean(recovery),
|
|
64
|
+
planRecoveryVerified: recoveryVerified,
|
|
52
65
|
hasSeedWork: !((job.plans?.length ?? 0) === 0 && job.tasks.filter(task => task.kind !== 'planner').length === 0),
|
|
53
|
-
noActivePlanner: unblockNeedsNewPlan(job) || !unconsumedUnblockTrigger(job) ? !scopedPlans.some(plan => plan.status === 'PLANNING') : false,
|
|
66
|
+
noActivePlanner: failedPlan?.manualPlanning ? false : unblockNeedsNewPlan(job) || !unconsumedUnblockTrigger(job) ? !scopedPlans.some(plan => plan.status === 'PLANNING') : false,
|
|
54
67
|
noBlockedWork: Boolean(unconsumedUnblockTrigger(job)) || !scopedTasks.some(task => task.kind !== 'planner' && isEffectivelyBlocked(task)),
|
|
55
68
|
noActiveImplementation: !scopedTasks.some(task => task.kind !== 'planner' && (['QUEUED', 'RUNNING'].includes(task.state) || (task.state === 'DRAFT' && !task.unblock))),
|
|
56
69
|
resolutionOwnedByTroubleshooter: scopedTasks.some(task => task.kind !== 'planner' && task.resolution?.owner === 'troubleshooter' && ['active', 'waiting'].includes(task.resolution.state)),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Derived, read-only reverse usage index. Graph topology remains the only transition authority. */
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { parseGraphV2 } from './graph-v2.js';
|
|
6
|
+
const EXECUTABLE_GRAPH = /^[a-z][a-z0-9-]*\.mmd$/;
|
|
7
|
+
export function executableGraphs(directory) {
|
|
8
|
+
const output = new Map();
|
|
9
|
+
for (const filename of readdirSync(directory).sort()) {
|
|
10
|
+
if (!EXECUTABLE_GRAPH.test(filename) || filename === 'main.mmd')
|
|
11
|
+
continue;
|
|
12
|
+
output.set(filename, parseGraphV2(readFileSync(join(directory, filename), 'utf8')));
|
|
13
|
+
}
|
|
14
|
+
return output;
|
|
15
|
+
}
|
|
16
|
+
export function buildWorkflowUsageIndex(graphs) {
|
|
17
|
+
const usages = [];
|
|
18
|
+
for (const [graphName, graph] of graphs) {
|
|
19
|
+
for (const node of graph.nodes.values()) {
|
|
20
|
+
usages.push({ graph: graphName, kind: node.kind, id: node.semanticId, line: node.line, node: node.id });
|
|
21
|
+
}
|
|
22
|
+
for (const edge of graph.edges) {
|
|
23
|
+
if ((edge.kind === 'on' || edge.kind === 'emit' || edge.kind === 'request') && edge.semanticId) {
|
|
24
|
+
usages.push({ graph: graphName, kind: edge.kind === 'request' ? 'request' : 'event', id: edge.semanticId, line: edge.line });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return usages.sort((a, b) => a.id.localeCompare(b.id) || a.graph.localeCompare(b.graph) || a.line - b.line);
|
|
29
|
+
}
|
|
30
|
+
export function whereWorkflowSymbol(index, id, kind) {
|
|
31
|
+
return index.filter(item => item.id === id && (!kind || item.kind === kind));
|
|
32
|
+
}
|
|
33
|
+
export function defaultWorkflowDirectory() {
|
|
34
|
+
return resolve(fileURLToPath(new URL('../../workflow/', import.meta.url)));
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-relay",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.143",
|
|
4
4
|
"description": "Durable async agent jobs coordinated through GitHub pull requests",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,10 +19,14 @@
|
|
|
19
19
|
"test:publish": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 --test-force-exit test/*.test.js",
|
|
20
20
|
"test:fast": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=2 test/workflow-lifecycle.test.js test/workflow-v2-lifecycle-parity.test.js test/workflow-effects.test.js test/relay-store.test.js test/local-authority.test.js test/local-jobid-routing.test.js test/local-sqlite-jobid.test.js test/no-github.test.js test/scheduled-jobs.test.js test/reconcile-coordinator.test.js test/service-publication.test.js test/github-webhook.test.js",
|
|
21
21
|
"test:full": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 test/*.test.js && npm run test:verification:l3",
|
|
22
|
-
"test:verification:l1": "node --test test/verification/harness.test.mjs",
|
|
23
|
-
"test:verification:l3": "npm run build && node --test test/verification/harness.test.mjs test/verification/l3-sqlite.test.mjs test/verification/l3-ingress.test.mjs test/verification/l3-replay.test.mjs",
|
|
24
|
-
"test:verification": "npm run test:verification:l1 && npm run test:verification:l3",
|
|
25
|
-
"e2e": "node e2e/runner/run.mjs"
|
|
22
|
+
"test:verification:l1": "npm run build && node --test test/verification/harness.test.mjs test/verification/contract.test.mjs test/verification/graph-inventory.test.mjs test/verification/dashboard-drilldown.test.mjs test/verification/evidence-matrix.test.mjs",
|
|
23
|
+
"test:verification:l3": "npm run build && node --test test/verification/harness.test.mjs test/verification/l3-sqlite.test.mjs test/verification/l3-ingress.test.mjs test/verification/l3-replay.test.mjs test/verification/objective-authority.test.mjs test/verification/l3-convergence.test.mjs test/verification/l3-review-convergence.test.mjs test/verification/l3-human-escalation.test.mjs test/verification/l3-escalation-notifier.test.mjs test/verification/l3-unknown-effect.test.mjs test/verification/l3-planner-round.test.mjs test/verification/full-recovery-gates.test.mjs test/verification/production-wiring.test.mjs",
|
|
24
|
+
"test:verification": "npm run test:verification:l1 && npm run test:verification:l2 && npm run test:verification:l3 && npm run test:verification:cross-a && node --test test/workflow-production-outbox.test.js && npm run test:verification:matrix",
|
|
25
|
+
"e2e": "node e2e/runner/run.mjs",
|
|
26
|
+
"test:verification:cross-a": "npm run build && RELAY_A_DIST=$PWD/dist node --test test/verification/action-ledger-cross-branch.test.mjs test/verification/outbox-cross-a.test.mjs",
|
|
27
|
+
"test:verification:matrix": "node test/verification/recovery-report.mjs /tmp/agents-relay-job481-evidence",
|
|
28
|
+
"test:verification:l2": "npm run build && node --test test/verification/l2/*.test.mjs",
|
|
29
|
+
"test:verification:l2:live": "node test/verification/l2/live-provider.mjs"
|
|
26
30
|
},
|
|
27
31
|
"devDependencies": {
|
|
28
32
|
"@types/node": "^22.10.2",
|
package/workflow/main.mmd
CHANGED
|
@@ -1,29 +1,56 @@
|
|
|
1
|
-
%% dashboard: presentation-only
|
|
1
|
+
%% dashboard: presentation-only — not registered with GraphRuntime or the executable Graph DSL.
|
|
2
|
+
%% Nodes link to the canonical executable workflow graphs. SQLite remains authoritative.
|
|
2
3
|
flowchart LR
|
|
3
|
-
objective["START: Job Objective"]
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
resolution -->|Needs human action| escalator["Escalator workflow"] -->|Resolved| resolution
|
|
15
|
-
classDef failure fill:#ffe4e4,stroke:#be3030,color:#7c1818
|
|
16
|
-
class troubleshooter,resolution,escalator failure
|
|
17
|
-
linkStyle 8,9,10,11,12,13,14,15,16,17,18 stroke:#be3030,stroke-width:2px
|
|
4
|
+
objective["START: Job Objective"]
|
|
5
|
+
planner["Planner"]
|
|
6
|
+
worker["Worker"]
|
|
7
|
+
reviewer["Reviewer"]
|
|
8
|
+
delivery["Merge / Delivery"]
|
|
9
|
+
verify["Objective Verification"]
|
|
10
|
+
completed["Completed"]
|
|
11
|
+
trouble["Troubleshooter"]
|
|
12
|
+
recover{"Recovery conditions verified?"}
|
|
13
|
+
escalation["Escalation: human action only"]
|
|
14
|
+
fix["Changes Required: fix and review again"]
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
objective --> planner
|
|
17
|
+
planner --> worker
|
|
18
|
+
worker --> reviewer
|
|
19
|
+
reviewer -->|approved| delivery
|
|
20
|
+
reviewer -->|changes required| fix
|
|
21
|
+
fix --> worker
|
|
22
|
+
delivery --> verify
|
|
23
|
+
verify -->|verified| completed
|
|
24
|
+
|
|
25
|
+
planner -->|failure| trouble
|
|
26
|
+
worker -->|failure| trouble
|
|
27
|
+
reviewer -->|failure| trouble
|
|
28
|
+
delivery -->|failure| trouble
|
|
29
|
+
verify -->|unknown / failed| trouble
|
|
30
|
+
trouble --> recover
|
|
31
|
+
recover -->|guarded retry to exact failed step| planner
|
|
32
|
+
recover -->|guarded retry to exact failed step| worker
|
|
33
|
+
recover -->|guarded retry to exact failed step| reviewer
|
|
34
|
+
recover -->|guarded retry to exact failed step| delivery
|
|
35
|
+
recover -->|unknown: reconcile first| trouble
|
|
36
|
+
recover -->|human action required| escalation
|
|
37
|
+
escalation -->|resolved: reconcile and resume| trouble
|
|
38
|
+
|
|
39
|
+
click objective href "job-lifecycle.mmd" "Job lifecycle"
|
|
40
|
+
click planner href "planner-orchestration.mmd" "Planner orchestration"
|
|
41
|
+
click worker href "task-lifecycle.mmd" "Task lifecycle"
|
|
42
|
+
click reviewer href "review-lifecycle.mmd" "Review lifecycle"
|
|
43
|
+
click delivery href "job-lifecycle.mmd" "Job completion"
|
|
44
|
+
click verify href "job-lifecycle.mmd" "Objective completion gate"
|
|
45
|
+
click trouble href "troubleshooter.mmd" "Troubleshooter"
|
|
46
|
+
click recover href "task-resolution.mmd" "Task resolution"
|
|
47
|
+
click escalation href "escalation.mmd" "Escalation"
|
|
48
|
+
click fix href "review-lifecycle.mmd" "Review fixes"
|
|
49
|
+
|
|
50
|
+
classDef exception fill:#fee2e2,stroke:#b91c1c,color:#991b1b
|
|
51
|
+
classDef normal fill:#eff6ff,stroke:#2563eb,color:#1e40af
|
|
52
|
+
class trouble,recover,escalation exception
|
|
53
|
+
class objective,planner,worker,reviewer,delivery,verify,completed,fix normal
|
|
54
|
+
|
|
55
|
+
%% Failed/unknown transitions are red; verified retries remain blue.
|
|
56
|
+
linkStyle 8,9,10,11,12,18 stroke:#be3030,stroke-width:2px
|
|
@@ -3,6 +3,9 @@ flowchart LR
|
|
|
3
3
|
idle["state: idle"]
|
|
4
4
|
checkLegacy["state: check_legacy_pending"]
|
|
5
5
|
checkRecover["state: check_recover"]
|
|
6
|
+
checkPlanFailure["state: check_plan_failure"]
|
|
7
|
+
checkPlanRecovery["state: check_plan_recovery"]
|
|
8
|
+
checkPlanRecoveryExists["state: check_plan_recovery_exists"]
|
|
6
9
|
checkSeed["state: check_seed"]
|
|
7
10
|
checkPlanner["state: check_planner"]
|
|
8
11
|
checkResolution["state: check_resolution_ownership"]
|
|
@@ -17,6 +20,9 @@ created["state: created"]
|
|
|
17
20
|
autonomous["guard: autonomousJob"]
|
|
18
21
|
legacyPending["guard: legacyPlanningPending"]
|
|
19
22
|
recoverablePending["guard: recoverablePendingPlan"]
|
|
23
|
+
terminalFailure["guard: terminalPlanFailure"]
|
|
24
|
+
recoveryVerified["guard: planRecoveryVerified"]
|
|
25
|
+
recoveryExists["guard: planRecoveryExists"]
|
|
20
26
|
hasSeed["guard: hasSeedWork"]
|
|
21
27
|
noActivePlanner["guard: noActivePlanner"]
|
|
22
28
|
resolutionOwned["guard: resolutionOwnedByTroubleshooter"]
|
|
@@ -26,6 +32,7 @@ materialized["guard: continuationMaterialized"]
|
|
|
26
32
|
unsatisfied["guard: objectiveUnsatisfied"]
|
|
27
33
|
|
|
28
34
|
recoverAction["action: planner.recoverPending"]
|
|
35
|
+
createRecovery["action: planner.createRecovery"]
|
|
29
36
|
createAction["action: planner.createRound"]
|
|
30
37
|
|
|
31
38
|
idle -->|on: planner.reconcile| autonomous
|
|
@@ -39,7 +46,20 @@ legacyPending -->|false| checkRecover
|
|
|
39
46
|
checkRecover -->|on: planner.reconcile| recoverablePending
|
|
40
47
|
recoverablePending -->|true| recoverAction
|
|
41
48
|
recoverAction --> recovered
|
|
42
|
-
recoverablePending -->|false|
|
|
49
|
+
recoverablePending -->|false| checkPlanFailure
|
|
50
|
+
|
|
51
|
+
checkPlanFailure -->|on: planner.reconcile| terminalFailure
|
|
52
|
+
terminalFailure -->|false| checkSeed
|
|
53
|
+
terminalFailure -->|true| checkPlanRecovery
|
|
54
|
+
|
|
55
|
+
checkPlanRecovery -->|on: planner.reconcile| recoveryVerified
|
|
56
|
+
recoveryVerified -->|true| checkPlanner
|
|
57
|
+
recoveryVerified -->|false| checkPlanRecoveryExists
|
|
58
|
+
|
|
59
|
+
checkPlanRecoveryExists -->|on: planner.reconcile| recoveryExists
|
|
60
|
+
recoveryExists -->|true| wait
|
|
61
|
+
recoveryExists -->|false| createRecovery
|
|
62
|
+
createRecovery --> created
|
|
43
63
|
|
|
44
64
|
checkSeed -->|on: planner.reconcile| hasSeed
|
|
45
65
|
hasSeed -->|false| wait
|
|
@@ -101,6 +101,10 @@ autoMergePending -->|false| approved
|
|
|
101
101
|
pending -->|on: review.changes_required| fixPending
|
|
102
102
|
fixPending --> changes
|
|
103
103
|
|
|
104
|
+
%% A failed Reviewer execution may recover with a new Execution ID while
|
|
105
|
+
%% the Review lifecycle remains STARTED. This is a legal re-start event;
|
|
106
|
+
%% Reconciler still fences stale execution generations and immutable head SHA.
|
|
107
|
+
started -->|on: review.started| started
|
|
104
108
|
started -->|on: review.approved| autoMergeStarted
|
|
105
109
|
autoMergeStarted -->|true| mergeStarted
|
|
106
110
|
mergeStarted --> approved
|