agents-relay 1.0.177 → 2.0.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/README.md +4 -2
- package/dist/adapters.js +26 -2
- package/dist/blockers.js +60 -65
- package/dist/cli-support.js +33 -1
- package/dist/cli.js +72 -86
- package/dist/dashboard-actions.js +13 -35
- package/dist/dashboard-html.js +1 -1
- package/dist/dashboard.js +39 -0
- package/dist/execution-recovery.js +37 -0
- package/dist/graph-runtime.js +1 -0
- package/dist/job-actions.js +53 -0
- package/dist/manual-plan.js +15 -17
- package/dist/planner-runtime.js +135 -0
- package/dist/planner.js +76 -60
- package/dist/pool.js +54 -10
- package/dist/projected-store.js +314 -0
- package/dist/reconciler-review.js +185 -110
- package/dist/reconciler-runtime.js +67 -7
- package/dist/reconciler.js +134 -282
- package/dist/relay-store.js +380 -0
- package/dist/relayd.js +122 -29
- package/dist/runtime-status.js +2 -1
- package/dist/scheduled-jobs.js +40 -31
- package/dist/scheduler.js +19 -18
- package/dist/store.js +1 -1
- package/dist/task-actions.js +139 -0
- package/dist/troubleshooter.js +14 -8
- package/dist/types.js +3 -3
- package/dist/unblock.js +31 -21
- package/dist/workflow/authoritative-unblock-v2.js +20 -0
- package/dist/workflow/blocker-resolution-v2.js +33 -0
- package/dist/workflow/contracts.js +21 -0
- package/dist/workflow/definitions-v2.js +75 -0
- package/dist/workflow/dispatcher.js +56 -0
- package/dist/workflow/entity-creation.js +32 -0
- package/dist/workflow/graph-v2.js +391 -0
- package/dist/workflow/job-reconcile-v2.js +19 -0
- package/dist/workflow/lifecycle.js +82 -4
- package/dist/workflow/planner-orchestration-v2.js +58 -0
- package/dist/workflow/planner-trigger-v2.js +24 -0
- package/dist/workflow/review-v2.js +42 -0
- package/dist/workflow/runtime.js +15 -1
- package/dist/workflow/schedule-cycle-v2.js +25 -0
- package/dist/workflow/scheduled-run-v2.js +30 -0
- package/dist/workflow/task-action-policy.js +19 -0
- package/dist/workflow/task-reconcile.js +6 -6
- package/dist/workflow/task-related-action-v2.js +42 -0
- package/dist/workflow/troubleshooter-v2.js +29 -0
- package/dist/workspace.js +6 -1
- package/package.json +1 -1
- package/skills/agents-relay/agents/planner.agent.md +5 -5
- package/skills/agents-relay/contracts/task-job-contract.md +2 -51
- package/workflow/authoritative-unblock-v2.mmd +15 -0
- package/workflow/blocker-resolution-v2.mmd +73 -0
- package/workflow/job-lifecycle-v2.mmd +70 -0
- package/workflow/job-lifecycle.mmd +2 -2
- package/workflow/planner-orchestration-v2.mmd +65 -0
- package/workflow/planner-trigger-v2.mmd +32 -0
- package/workflow/review-lifecycle-v2.mmd +124 -0
- package/workflow/schedule-cycle-v2.mmd +19 -0
- package/workflow/scheduled-run-v2.mmd +55 -0
- package/workflow/task-lifecycle-v2.mmd +201 -0
- package/workflow/task-lifecycle.mmd +3 -0
- package/workflow/task-related-action-v2.mmd +28 -0
- package/workflow/troubleshooter-v2.mmd +43 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GraphRuntime } from './graph-v2.js';
|
|
3
|
+
import { dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
4
|
+
const source = readFileSync(new URL('../../workflow/planner-trigger-v2.mmd', import.meta.url), 'utf8');
|
|
5
|
+
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
6
|
+
export const plannerTriggerV2 = new GraphRuntime(source, {
|
|
7
|
+
states: new Set(['idle', 'check_unblock', 'check_schedule', 'check_initial', 'manual', 'unblock', 'schedule', 'initial', 'continuation']),
|
|
8
|
+
guards: {
|
|
9
|
+
manualPlan: boolGuard('manualPlan'),
|
|
10
|
+
hasUnblock: boolGuard('hasUnblock'),
|
|
11
|
+
scheduledRun: boolGuard('scheduledRun'),
|
|
12
|
+
initialPlan: boolGuard('initialPlan'),
|
|
13
|
+
},
|
|
14
|
+
actions: {},
|
|
15
|
+
events: { 'plan.requested': { id: 'plan.requested' } },
|
|
16
|
+
});
|
|
17
|
+
export function dispatchPlannerTrigger(facts) {
|
|
18
|
+
return dispatchWorkflowGraphSync(plannerTriggerV2, {
|
|
19
|
+
initialState: 'idle', input: { transport: 'event', id: 'plan.requested', payload: {} }, durableFacts: facts,
|
|
20
|
+
isTransientState: state => state.startsWith('check_'), maxSteps: 4,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Compatibility/testing alias. Production callers use dispatchPlannerTrigger. */
|
|
24
|
+
export const decidePlannerTrigger = dispatchPlannerTrigger;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GraphRuntime } from './graph-v2.js';
|
|
3
|
+
import { dispatchWorkflowGraph, dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
4
|
+
const source = readFileSync(new URL('../../workflow/review-lifecycle-v2.mmd', import.meta.url), 'utf8');
|
|
5
|
+
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
6
|
+
export const reviewLifecycleV2 = new GraphRuntime(source, {
|
|
7
|
+
states: new Set(['NONE', 'PENDING', 'STARTED', 'APPROVED', 'CHANGES_REQUIRED', 'BLOCKED', 'STALE']),
|
|
8
|
+
guards: {
|
|
9
|
+
unscheduled: boolGuard('unscheduled'),
|
|
10
|
+
reviewEnabled: boolGuard('reviewEnabled'),
|
|
11
|
+
prOpen: boolGuard('prOpen'),
|
|
12
|
+
jobActive: boolGuard('jobActive'),
|
|
13
|
+
implementationComplete: boolGuard('implementationComplete'),
|
|
14
|
+
headAvailable: boolGuard('headAvailable'),
|
|
15
|
+
completionGateSatisfied: boolGuard('completionGateSatisfied'),
|
|
16
|
+
noActiveReviewTask: boolGuard('noActiveReviewTask'),
|
|
17
|
+
currentReviewCancelled: boolGuard('currentReviewCancelled'),
|
|
18
|
+
noApprovedCurrentReview: boolGuard('noApprovedCurrentReview'),
|
|
19
|
+
autoMerge: boolGuard('autoMerge'),
|
|
20
|
+
},
|
|
21
|
+
actions: Object.fromEntries(['review.create', 'review.cancelStale', 'review.createFix', 'review.requestMerge'].map(id => [id, { id }])),
|
|
22
|
+
events: Object.fromEntries(['reconcile', 'review.started', 'review.approved', 'review.changes_required', 'review.blocked', 'head.changed'].map(id => [id, { id }])),
|
|
23
|
+
});
|
|
24
|
+
export function reviewGraphState(outcome) {
|
|
25
|
+
if (!outcome)
|
|
26
|
+
return 'NONE';
|
|
27
|
+
return outcome.toUpperCase();
|
|
28
|
+
}
|
|
29
|
+
export function reviewOutcome(state) {
|
|
30
|
+
if (state === 'NONE')
|
|
31
|
+
return undefined;
|
|
32
|
+
if (!['PENDING', 'STARTED', 'APPROVED', 'CHANGES_REQUIRED', 'BLOCKED', 'STALE'].includes(state)) {
|
|
33
|
+
throw new Error('Unknown review graph state ' + state);
|
|
34
|
+
}
|
|
35
|
+
return state.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
export async function dispatchReviewLifecycle(outcome, event, facts = {}, executeAction) {
|
|
38
|
+
return dispatchWorkflowGraph(reviewLifecycleV2, { initialState: reviewGraphState(outcome), input: { transport: 'event', id: event, payload: {} }, durableFacts: facts, executeAction });
|
|
39
|
+
}
|
|
40
|
+
export function decideReviewLifecycle(outcome, event, facts = {}) {
|
|
41
|
+
return dispatchWorkflowGraphSync(reviewLifecycleV2, { initialState: reviewGraphState(outcome), input: { transport: 'event', id: event, payload: {} }, durableFacts: facts });
|
|
42
|
+
}
|
package/dist/workflow/runtime.js
CHANGED
|
@@ -17,12 +17,26 @@ export function graphWorkflowDecision(engine, from, event, facts = {}) {
|
|
|
17
17
|
reason: 'accepted',
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
function applyTaskMutation(task, mutation) {
|
|
20
|
+
export function applyTaskMutation(task, mutation) {
|
|
21
21
|
if (mutation === 'mutation.clearError')
|
|
22
22
|
task.error = null;
|
|
23
23
|
if (mutation === 'mutation.clearFailureReason')
|
|
24
24
|
task.failureReason = undefined;
|
|
25
|
+
if (mutation === 'mutation.clearLease') {
|
|
26
|
+
task.leaseOwner = null;
|
|
27
|
+
task.leaseExpiresAt = null;
|
|
28
|
+
}
|
|
29
|
+
if (mutation === 'mutation.clearExecution')
|
|
30
|
+
task.executionId = null;
|
|
31
|
+
if (mutation === 'mutation.markLeaseExpired')
|
|
32
|
+
task.error = 'Lease expired after maximum attempts';
|
|
33
|
+
if (mutation === 'mutation.markStartupOrphanedExhausted')
|
|
34
|
+
task.error = 'Orphaned after relayd restart after maximum attempts';
|
|
35
|
+
if (mutation === 'mutation.markPlannerUnavailable')
|
|
36
|
+
task.error = 'No objective planner configured';
|
|
25
37
|
}
|
|
38
|
+
export function applyTaskGraphTarget(task, target, timestamp = new Date().toISOString()) { task.state = target; task.updatedAt = timestamp; }
|
|
39
|
+
export function applyJobGraphTarget(job, target, timestamp = new Date().toISOString()) { job.state = target; job.updatedAt = timestamp; }
|
|
26
40
|
export function applyTaskWorkflowDecision(task, decision, timestamp = new Date().toISOString()) {
|
|
27
41
|
if (!decision.accepted || !decision.target)
|
|
28
42
|
throw new Error(`Cannot apply rejected workflow decision to task ${task.id}`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GraphRuntime } from './graph-v2.js';
|
|
3
|
+
import { dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
4
|
+
const source = readFileSync(new URL('../../workflow/schedule-cycle-v2.mmd', import.meta.url), 'utf8');
|
|
5
|
+
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
6
|
+
export const scheduleCycleV2 = new GraphRuntime(source, {
|
|
7
|
+
states: new Set(['idle', 'check_satisfied', 'waiting', 'finished']),
|
|
8
|
+
guards: {
|
|
9
|
+
hasCurrentSlot: boolGuard('hasCurrentSlot'),
|
|
10
|
+
plannerSatisfied: boolGuard('plannerSatisfied'),
|
|
11
|
+
},
|
|
12
|
+
actions: { 'schedule.finishSlot': { id: 'schedule.finishSlot' } },
|
|
13
|
+
events: { 'schedule.reconcile': { id: 'schedule.reconcile' } },
|
|
14
|
+
});
|
|
15
|
+
export function dispatchScheduleCycle(facts, onFinish) {
|
|
16
|
+
return dispatchWorkflowGraphSync(scheduleCycleV2, {
|
|
17
|
+
initialState: 'idle', input: { transport: 'event', id: 'schedule.reconcile', payload: {} }, durableFacts: facts,
|
|
18
|
+
isTransientState: state => state.startsWith('check_'), maxSteps: 2,
|
|
19
|
+
executeAction: actionId => { if (actionId === 'schedule.finishSlot')
|
|
20
|
+
onFinish();
|
|
21
|
+
else
|
|
22
|
+
throw new Error('Unsupported schedule cycle action ' + actionId); return {}; },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function decideScheduleCycle(facts) { return dispatchScheduleCycle(facts, () => { }); }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GraphRuntime } from './graph-v2.js';
|
|
3
|
+
import { dispatchWorkflowGraph, dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
4
|
+
const source = readFileSync(new URL('../../workflow/scheduled-run-v2.mmd', import.meta.url), 'utf8');
|
|
5
|
+
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
6
|
+
export const scheduledRunV2 = new GraphRuntime(source, {
|
|
7
|
+
states: new Set([
|
|
8
|
+
'idle', 'check_unclaimed', 'check_job', 'check_active', 'check_concurrency',
|
|
9
|
+
'check_queue', 'check_missed', 'skip_idempotent', 'skip_active', 'claimed',
|
|
10
|
+
]),
|
|
11
|
+
guards: {
|
|
12
|
+
slotDue: boolGuard('slotDue'),
|
|
13
|
+
slotUnclaimed: boolGuard('slotUnclaimed'),
|
|
14
|
+
jobActive: boolGuard('jobActive'),
|
|
15
|
+
hasActiveTasks: boolGuard('hasActiveTasks'),
|
|
16
|
+
concurrencySkip: boolGuard('concurrencySkip'),
|
|
17
|
+
concurrencyQueue: boolGuard('concurrencyQueue'),
|
|
18
|
+
runMissed: boolGuard('runMissed'),
|
|
19
|
+
},
|
|
20
|
+
actions: Object.fromEntries([
|
|
21
|
+
'schedule.skipActiveAdvance', 'schedule.claimFromSlot', 'schedule.claimFromNow',
|
|
22
|
+
].map(id => [id, { id }])),
|
|
23
|
+
events: { 'schedule.tick': { id: 'schedule.tick' } },
|
|
24
|
+
});
|
|
25
|
+
export async function dispatchScheduledRun(facts, executeAction) {
|
|
26
|
+
return dispatchWorkflowGraph(scheduledRunV2, { initialState: 'idle', input: { transport: 'event', id: 'schedule.tick', payload: {} }, durableFacts: facts, isTransientState: state => state.startsWith('check_'), maxSteps: 8, executeAction });
|
|
27
|
+
}
|
|
28
|
+
export function decideScheduledRun(facts) {
|
|
29
|
+
return dispatchWorkflowGraphSync(scheduledRunV2, { initialState: 'idle', input: { transport: 'event', id: 'schedule.tick', payload: {} }, durableFacts: facts, isTransientState: state => state.startsWith('check_'), maxSteps: 8 });
|
|
30
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { decideRelatedTaskAction } from './task-related-action-v2.js';
|
|
2
|
+
/**
|
|
3
|
+
* Graph-driven related-task scope for explicit task actions.
|
|
4
|
+
*
|
|
5
|
+
* TypeScript provides relationship/lifecycle facts; the executable workflow
|
|
6
|
+
* decides whether each candidate receives a related mutation.
|
|
7
|
+
*/
|
|
8
|
+
export function selectRelatedTaskMutations(job, task, actionId) {
|
|
9
|
+
return job.tasks.flatMap(candidate => {
|
|
10
|
+
const decision = decideRelatedTaskAction(task, candidate, actionId);
|
|
11
|
+
if (!decision.accepted || decision.target !== 'selected')
|
|
12
|
+
return [];
|
|
13
|
+
return [{
|
|
14
|
+
taskId: candidate.id,
|
|
15
|
+
mutation: 'cancel',
|
|
16
|
+
reason: `Superseded by manual retry of ${task.id}`,
|
|
17
|
+
}];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { applyTaskWorkflowDecision } from './runtime.js';
|
|
1
|
+
import { dispatchTaskLifecycleSync } from './lifecycle.js';
|
|
3
2
|
export function dependencyReady(task, tasks) {
|
|
4
3
|
return task.dependencies.every(id => tasks.get(id)?.state === 'SUCCEEDED');
|
|
5
4
|
}
|
|
@@ -32,17 +31,18 @@ export function reconcileTaskDependencies(job) {
|
|
|
32
31
|
const scopedTasks = scheduledCycleTasks(job);
|
|
33
32
|
const tasks = new Map(job.tasks.map(task => [task.id, task]));
|
|
34
33
|
for (const task of scopedTasks) {
|
|
34
|
+
if (task.kind === 'planner')
|
|
35
|
+
continue;
|
|
35
36
|
const ready = dependencyReady(task, tasks);
|
|
36
37
|
const facts = {
|
|
37
38
|
dependenciesSatisfied: task.dependencies.length > 0 && ready,
|
|
38
39
|
dependenciesUnsatisfied: task.dependencies.length > 0 && !ready,
|
|
39
40
|
runnable: ready,
|
|
40
41
|
};
|
|
41
|
-
const
|
|
42
|
+
const from = task.state;
|
|
43
|
+
const decision = dispatchTaskLifecycleSync(task, 'reconcile', facts);
|
|
42
44
|
if (decision.reason === 'ambiguous')
|
|
43
|
-
throw new Error(`Ambiguous automatic task transition from ${
|
|
44
|
-
if (decision.accepted && decision.target)
|
|
45
|
-
applyTaskWorkflowDecision(task, decision);
|
|
45
|
+
throw new Error(`Ambiguous automatic task transition from ${from}`);
|
|
46
46
|
}
|
|
47
47
|
return job;
|
|
48
48
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { decideTaskLifecycle } from './lifecycle.js';
|
|
3
|
+
import { GraphRuntime } from './graph-v2.js';
|
|
4
|
+
import { dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
5
|
+
const source = readFileSync(new URL('../../workflow/task-related-action-v2.mmd', import.meta.url), 'utf8');
|
|
6
|
+
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
7
|
+
export const taskRelatedActionV2 = new GraphRuntime(source, {
|
|
8
|
+
states: new Set(['evaluate', 'check_other', 'check_recovery', 'skip', 'selected']),
|
|
9
|
+
guards: {
|
|
10
|
+
otherTask: boolGuard('otherTask'),
|
|
11
|
+
recoveryForTask: boolGuard('recoveryForTask'),
|
|
12
|
+
candidateCancellable: boolGuard('candidateCancellable'),
|
|
13
|
+
},
|
|
14
|
+
actions: {
|
|
15
|
+
'task.retry': { id: 'task.retry' },
|
|
16
|
+
'task.cancel': { id: 'task.cancel' },
|
|
17
|
+
'task.unblock.retry': { id: 'task.unblock.retry' },
|
|
18
|
+
'task.queue': { id: 'task.queue' },
|
|
19
|
+
'task.release': { id: 'task.release' },
|
|
20
|
+
'task.hold': { id: 'task.hold' },
|
|
21
|
+
'related.cancel': { id: 'related.cancel' },
|
|
22
|
+
},
|
|
23
|
+
events: {},
|
|
24
|
+
});
|
|
25
|
+
export function relatedTaskActionFacts(task, candidate) {
|
|
26
|
+
return {
|
|
27
|
+
otherTask: candidate.id !== task.id,
|
|
28
|
+
recoveryForTask: candidate.agentName === 'troubleshooter'
|
|
29
|
+
&& (candidate.parentTaskId === task.id
|
|
30
|
+
|| (!candidate.parentTaskId && candidate.id.startsWith(`troubleshoot-${task.id}-a`))),
|
|
31
|
+
candidateCancellable: decideTaskLifecycle(candidate, 'cancel').accepted,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function decideRelatedTaskAction(task, candidate, actionId) {
|
|
35
|
+
return dispatchWorkflowGraphSync(taskRelatedActionV2, {
|
|
36
|
+
initialState: 'evaluate',
|
|
37
|
+
input: { transport: 'action', id: actionId, payload: {} },
|
|
38
|
+
durableFacts: relatedTaskActionFacts(task, candidate),
|
|
39
|
+
isTransientState: state => state === 'check_other' || state === 'check_recovery',
|
|
40
|
+
maxSteps: 3,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GraphRuntime } from './graph-v2.js';
|
|
3
|
+
import { dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
4
|
+
const source = readFileSync(new URL('../../workflow/troubleshooter-v2.mmd', import.meta.url), 'utf8');
|
|
5
|
+
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
6
|
+
export const troubleshooterV2 = new GraphRuntime(source, {
|
|
7
|
+
states: new Set(['idle', 'check_ordinary', 'check_run', 'check_needs', 'check_id', 'check_barrier', 'skip', 'create']),
|
|
8
|
+
guards: {
|
|
9
|
+
failedOrBlocked: boolGuard('failedOrBlocked'),
|
|
10
|
+
ordinaryWork: boolGuard('ordinaryWork'),
|
|
11
|
+
currentRun: boolGuard('currentRun'),
|
|
12
|
+
needsRecovery: boolGuard('needsRecovery'),
|
|
13
|
+
idUnused: boolGuard('idUnused'),
|
|
14
|
+
noRecoveryBarrier: boolGuard('noRecoveryBarrier'),
|
|
15
|
+
},
|
|
16
|
+
actions: { 'troubleshooter.create': { id: 'troubleshooter.create' } },
|
|
17
|
+
events: { 'troubleshooter.reconcile': { id: 'troubleshooter.reconcile' } },
|
|
18
|
+
});
|
|
19
|
+
export function dispatchTroubleshooter(facts, onCreate) {
|
|
20
|
+
return dispatchWorkflowGraphSync(troubleshooterV2, {
|
|
21
|
+
initialState: 'idle', input: { transport: 'event', id: 'troubleshooter.reconcile', payload: {} }, durableFacts: facts,
|
|
22
|
+
isTransientState: state => state.startsWith('check_'), maxSteps: 6,
|
|
23
|
+
executeAction: actionId => { if (actionId === 'troubleshooter.create')
|
|
24
|
+
onCreate();
|
|
25
|
+
else
|
|
26
|
+
throw new Error('Unsupported troubleshooter graph action ' + actionId); return {}; },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function decideTroubleshooter(facts) { return dispatchTroubleshooter(facts, () => { }); }
|
package/dist/workspace.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
2
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
@@ -81,6 +81,11 @@ export function resolveRepositoryProjectPath(repositoryRoot, projectPath) {
|
|
|
81
81
|
return root;
|
|
82
82
|
if (isAbsolute(projectPath))
|
|
83
83
|
throw new Error('Job projectPath must be repository-relative');
|
|
84
|
+
// Legacy Jobs sometimes stored the repository folder name itself (for example
|
|
85
|
+
// projectPath='agents-relay' for the agents-relay repository). Interpret that
|
|
86
|
+
// historical form as the repository root instead of appending it twice.
|
|
87
|
+
if (projectPath === basename(root))
|
|
88
|
+
return root;
|
|
84
89
|
const target = resolve(root, projectPath);
|
|
85
90
|
const rel = relative(root, target);
|
|
86
91
|
if (rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel))
|
package/package.json
CHANGED
|
@@ -36,14 +36,15 @@ The graph is the explanation for the decomposition: every emitted Task should ma
|
|
|
36
36
|
|
|
37
37
|
### Semantic ownership vs execution flow
|
|
38
38
|
|
|
39
|
-
The planning graph and the durable Task tree are
|
|
39
|
+
The planning graph and the durable Task tree are different views:
|
|
40
40
|
|
|
41
41
|
- The graph and `dependencies` express execution flow and prerequisites.
|
|
42
42
|
- `parentTaskId` expresses semantic ownership of an artifact or milestone.
|
|
43
|
-
-
|
|
44
|
-
- `plan-N` records are planning provenance,
|
|
43
|
+
- Later planning rounds attach revision, re-review, specialist review, and recovery work to the existing artifact subtree instead of creating new roots merely because the work occurs later.
|
|
44
|
+
- `plan-N` records are planning provenance, never business milestones, and must never be used as `parentTaskId`.
|
|
45
|
+
- A terminal parent remains valid historical ownership. Dependencies may cross semantic subtrees.
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
Example: keep `architecture-v2` as the root milestone and place review, revision, and re-review beneath it; their dependencies express order without changing their semantic parent. An independent `graph-runtime-v2` milestone remains a root sibling.
|
|
47
48
|
|
|
48
49
|
### Examples
|
|
49
50
|
|
|
@@ -80,7 +81,6 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
|
|
|
80
81
|
- When a Task depends on mutable repository or product state, distinguish a frozen evidence target from the current execution head. A SHA may freeze that Task's own review/source evidence, but do not copy an upstream SHA into a dependent release, deploy, publication, or other follow-on Task unless the objective explicitly requires that same frozen target. After dependencies complete, downstream Tasks must resolve and verify the current durable Job/PR head before acting.
|
|
81
82
|
- When fresh UI, browser, store, runtime, or visual evidence is material to the Task outcome, say so explicitly in the Task input and require evidence from the real current state. Existing screenshots, prepared assets, or prior observations are not substitutes for fresh evidence unless the objective explicitly allows reuse.
|
|
82
83
|
- When a Job needs multi-perspective review, use the visible `review-chair` agent or an Agent Graph to select specialist roles dynamically. Keep specialist Tasks as independent siblings against the same immutable target/version, and leave worker/model/provider routing to Relay.
|
|
83
|
-
- Treat `parentTaskId` as semantic ownership, not execution order. Select an existing non-planner Task as the parent when a later-round revision, re-review, specialist review, or troubleshooting Task belongs to that artifact/milestone; a terminal parent remains valid historical ownership. Use `dependencies` independently for prerequisites, including cross-parent dependencies. Never use a `plan-N` Task as a parent, and never invent a synthetic parent merely to group a planning round.
|
|
84
84
|
- A Task describes what an agent must accomplish; never emit a shell command as a worker Task.
|
|
85
85
|
- Use dependencies only for Task IDs inside the current Job, and only when execution order actually matters. Never encode cross-Job or cross-repository dependencies in `dependencies`.
|
|
86
86
|
- When the current Job is a prerequisite for another durable Job/Task, emit that dependent in top-level `unblocks`. The current blocker Job owns this authoritative reverse relation. Dependent-side `blockedBy` is optional observability metadata and is not the source of truth. Use `resolutionMode: "merge"` when blocker PR merge is sufficient; use `"verified"` when publication, deployment, runtime health, human approval, or another post-merge fact must be verified.
|
|
@@ -24,31 +24,9 @@ Task producers choose semantic task properties such as objective, dependencies,
|
|
|
24
24
|
|
|
25
25
|
### Planning graph and semantic Task tree
|
|
26
26
|
|
|
27
|
-
The planning graph expresses execution flow. The Task tree expresses semantic
|
|
28
|
-
ownership. `dependencies` answer which durable outcomes must succeed before a
|
|
29
|
-
Task can run; `parentTaskId` answers which durable artifact, milestone, or
|
|
30
|
-
parent outcome the Task belongs to. These are independent relationships and
|
|
31
|
-
must not be inferred from one another.
|
|
32
|
-
|
|
33
|
-
For a multi-round artifact workflow, keep the business tree compact and
|
|
34
|
-
milestone-oriented:
|
|
35
|
-
|
|
36
|
-
```text
|
|
37
|
-
Job
|
|
38
|
-
├── Architecture v2
|
|
39
|
-
│ ├── Review
|
|
40
|
-
│ ├── Revision 1
|
|
41
|
-
│ └── Re-review 1
|
|
42
|
-
└── Graph Runtime v2
|
|
43
|
-
```
|
|
27
|
+
The planning graph expresses execution flow. The Task tree expresses semantic ownership. `dependencies` answer which durable outcomes must succeed before a Task can run; `parentTaskId` answers which durable artifact or milestone owns the Task. These are independent relationships.
|
|
44
28
|
|
|
45
|
-
|
|
46
|
-
both remain semantically owned by Architecture v2. A later Planner round must
|
|
47
|
-
inspect existing Tasks and attach review, change, validation-rerun, release
|
|
48
|
-
correction, or recovery work to the existing artifact subtree. An independent
|
|
49
|
-
milestone is a root sibling. `plan-N` records are planning provenance/history,
|
|
50
|
-
never semantic parents. Existing terminal Tasks are historical evidence and
|
|
51
|
-
must not be rewritten merely to improve the tree.
|
|
29
|
+
A later Planner round must inspect the existing semantic tree and attach review, revision, re-review, validation rerun, release correction, or recovery work to the existing artifact subtree. Independent milestones remain roots. `plan-N` records are provenance/history and must never be semantic parents. Existing terminal Tasks remain immutable historical evidence and may continue to own later work.
|
|
52
30
|
|
|
53
31
|
### Task input vs result
|
|
54
32
|
|
|
@@ -126,33 +104,6 @@ For the ChatGPT Browser Worker guardian, for example, the Job defines `pass ->
|
|
|
126
104
|
|
|
127
105
|
### Optional Review Chair Tasks
|
|
128
106
|
|
|
129
|
-
### Semantic Task tree and planning provenance
|
|
130
|
-
|
|
131
|
-
The planning graph, semantic Task tree, and dependency graph are separate:
|
|
132
|
-
|
|
133
|
-
```text
|
|
134
|
-
plan-2 (planning provenance only)
|
|
135
|
-
├─ revision (parentTaskId: milestone, dependencies: [review])
|
|
136
|
-
└─ re-review (parentTaskId: milestone, dependencies: [revision])
|
|
137
|
-
independent-milestone (parentTaskId: null, dependencies: [revision])
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
`parentTaskId` answers which durable artifact or milestone owns this work. It
|
|
141
|
-
may point to an existing terminal work Task when a later planning round
|
|
142
|
-
continues that artifact, or to another work Task in the same batch. It must
|
|
143
|
-
never point to a `plan-N`/historical `planner-N` record and must not be used
|
|
144
|
-
just to group a round. `dependencies` answer what must finish first and may
|
|
145
|
-
cross semantic subtrees. Reviews, changes-required revisions, re-reviews,
|
|
146
|
-
specialist reviews, and troubleshooter recovery belong under the relevant
|
|
147
|
-
artifact/milestone; independent milestones remain roots. Planning provenance
|
|
148
|
-
is retained separately in `planningDecisionId` and `planningRound`, and
|
|
149
|
-
historical terminal Tasks are immutable.
|
|
150
|
-
|
|
151
|
-
Incorrect: make every generated Task a child of the current `plan-N`, or use
|
|
152
|
-
`parentTaskId` as a substitute for a dependency. Correct: attach a re-review
|
|
153
|
-
to the original milestone, set its dependency to the revision Task, and leave
|
|
154
|
-
the original milestone's terminal record unchanged.
|
|
155
|
-
|
|
156
107
|
Projects may use the shared `review-chair` agent as a normal Task or Agent
|
|
157
108
|
Graph node. The chair discovers specialist agent roles from the resolved
|
|
158
109
|
visible-agent catalog, records selection reasons, and creates sibling durable
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
flowchart LR
|
|
2
|
+
%% relay: initial blocked
|
|
3
|
+
blocked["state: blocked"]
|
|
4
|
+
autonomous["state: autonomous_replan"]
|
|
5
|
+
fixed["state: fixed_retry"]
|
|
6
|
+
|
|
7
|
+
autonomousJob["guard: autonomousJob"]
|
|
8
|
+
replan["action: unblock.replan"]
|
|
9
|
+
retry["action: unblock.retry"]
|
|
10
|
+
|
|
11
|
+
blocked -->|on: task.authoritative_unblock| autonomousJob
|
|
12
|
+
autonomousJob -->|true| replan
|
|
13
|
+
replan --> autonomous
|
|
14
|
+
autonomousJob -->|false| retry
|
|
15
|
+
retry --> fixed
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
flowchart LR
|
|
2
|
+
%% relay: initial waiting
|
|
3
|
+
waiting["state: waiting"]
|
|
4
|
+
checkTarget["state: check_target"]
|
|
5
|
+
checkAutonomous["state: check_autonomous"]
|
|
6
|
+
checkEffective["state: check_effective"]
|
|
7
|
+
checkAlreadyUnblocked["state: check_already_unblocked"]
|
|
8
|
+
checkDraft["state: check_draft"]
|
|
9
|
+
checkBlocked["state: check_blocked"]
|
|
10
|
+
checkFailed["state: check_failed"]
|
|
11
|
+
checkUnblocked["state: check_unblocked"]
|
|
12
|
+
wait["state: wait"]
|
|
13
|
+
obsolete["state: obsolete"]
|
|
14
|
+
resumed["state: resumed"]
|
|
15
|
+
|
|
16
|
+
resolutionAllowed["guard: resolutionAllowed"]
|
|
17
|
+
targetMatches["guard: targetMatches"]
|
|
18
|
+
autonomousJob["guard: autonomousJob"]
|
|
19
|
+
effectivelyBlocked["guard: effectivelyBlocked"]
|
|
20
|
+
alreadyUnblocked["guard: alreadyUnblocked"]
|
|
21
|
+
isDraft["guard: isDraft"]
|
|
22
|
+
isBlocked["guard: isBlocked"]
|
|
23
|
+
isFailed["guard: isFailed"]
|
|
24
|
+
isUnblocked["guard: isUnblocked"]
|
|
25
|
+
|
|
26
|
+
autonomousUnblock["action: blocker.autonomousUnblock"]
|
|
27
|
+
acceptExisting["action: blocker.acceptExistingUnblock"]
|
|
28
|
+
releaseDraft["action: blocker.releaseDraft"]
|
|
29
|
+
unblockTask["action: blocker.unblockTask"]
|
|
30
|
+
retryFailed["action: blocker.retryFailed"]
|
|
31
|
+
retryUnblocked["action: blocker.retryUnblocked"]
|
|
32
|
+
|
|
33
|
+
waiting -->|on: blocker.resolved| resolutionAllowed
|
|
34
|
+
resolutionAllowed -->|false| wait
|
|
35
|
+
resolutionAllowed -->|true| checkTarget
|
|
36
|
+
|
|
37
|
+
checkTarget -->|on: blocker.resolved| targetMatches
|
|
38
|
+
targetMatches -->|false| obsolete
|
|
39
|
+
targetMatches -->|true| checkAutonomous
|
|
40
|
+
|
|
41
|
+
checkAutonomous -->|on: blocker.resolved| autonomousJob
|
|
42
|
+
autonomousJob -->|true| checkEffective
|
|
43
|
+
autonomousJob -->|false| checkDraft
|
|
44
|
+
|
|
45
|
+
checkEffective -->|on: blocker.resolved| effectivelyBlocked
|
|
46
|
+
effectivelyBlocked -->|true| autonomousUnblock
|
|
47
|
+
autonomousUnblock --> resumed
|
|
48
|
+
effectivelyBlocked -->|false| checkAlreadyUnblocked
|
|
49
|
+
|
|
50
|
+
checkAlreadyUnblocked -->|on: blocker.resolved| alreadyUnblocked
|
|
51
|
+
alreadyUnblocked -->|true| acceptExisting
|
|
52
|
+
acceptExisting --> resumed
|
|
53
|
+
alreadyUnblocked -->|false| checkDraft
|
|
54
|
+
|
|
55
|
+
checkDraft -->|on: blocker.resolved| isDraft
|
|
56
|
+
isDraft -->|true| releaseDraft
|
|
57
|
+
releaseDraft --> resumed
|
|
58
|
+
isDraft -->|false| checkBlocked
|
|
59
|
+
|
|
60
|
+
checkBlocked -->|on: blocker.resolved| isBlocked
|
|
61
|
+
isBlocked -->|true| unblockTask
|
|
62
|
+
unblockTask --> resumed
|
|
63
|
+
isBlocked -->|false| checkFailed
|
|
64
|
+
|
|
65
|
+
checkFailed -->|on: blocker.resolved| isFailed
|
|
66
|
+
isFailed -->|true| retryFailed
|
|
67
|
+
retryFailed --> resumed
|
|
68
|
+
isFailed -->|false| checkUnblocked
|
|
69
|
+
|
|
70
|
+
checkUnblocked -->|on: blocker.resolved| isUnblocked
|
|
71
|
+
isUnblocked -->|true| retryUnblocked
|
|
72
|
+
retryUnblocked --> resumed
|
|
73
|
+
isUnblocked -->|false| obsolete
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
flowchart LR
|
|
2
|
+
%% relay: initial draft
|
|
3
|
+
draft["state: DRAFT"]
|
|
4
|
+
active["state: ACTIVE"]
|
|
5
|
+
completed["state: COMPLETED"]
|
|
6
|
+
cancelled["state: CANCELLED"]
|
|
7
|
+
notMergedDraft["guard: notMerged"]
|
|
8
|
+
notMergedActive["guard: notMerged"]
|
|
9
|
+
autoMergeReview["guard: autoMerge"]
|
|
10
|
+
autoMergeImplementation["guard: autoMerge"]
|
|
11
|
+
prOpenReconcile["guard: prOpen"]
|
|
12
|
+
reviewDisabledReconcile["guard: reviewDisabled"]
|
|
13
|
+
implementationCompleteReconcile["guard: implementationComplete"]
|
|
14
|
+
autoMergeReconcile["guard: autoMerge"]
|
|
15
|
+
headAvailableReconcile["guard: headAvailable"]
|
|
16
|
+
mergeCapableReconcile["guard: mergeCapable"]
|
|
17
|
+
mergeReconcile["action: effect.mergePullRequest"]
|
|
18
|
+
cancelDraftMerge["action: effect.cancelActiveTasks"]
|
|
19
|
+
cancelDraftClose["action: effect.cancelActiveTasks"]
|
|
20
|
+
mergeReview["action: effect.mergePullRequest"]
|
|
21
|
+
mergeImplementation["action: effect.mergePullRequest"]
|
|
22
|
+
cancelActiveMerge["action: effect.cancelActiveTasks"]
|
|
23
|
+
cancelActiveClose["action: effect.cancelActiveTasks"]
|
|
24
|
+
cancelCancelledMerge["action: effect.cancelActiveTasks"]
|
|
25
|
+
|
|
26
|
+
draft -->|on: activate| active
|
|
27
|
+
draft -->|on: pr.merged| cancelDraftMerge
|
|
28
|
+
cancelDraftMerge --> completed
|
|
29
|
+
draft -->|on: cancel| cancelDraft
|
|
30
|
+
cancelDraft["action: effect.cancelActiveTasks"]
|
|
31
|
+
cancelDraft --> cancelled
|
|
32
|
+
draft -->|on: pr.closed| notMergedDraft
|
|
33
|
+
notMergedDraft -->|true| cancelDraftClose
|
|
34
|
+
cancelDraftClose --> cancelled
|
|
35
|
+
|
|
36
|
+
active -->|on: reconcile| prOpenReconcile
|
|
37
|
+
prOpenReconcile -->|true| reviewDisabledReconcile
|
|
38
|
+
prOpenReconcile -->|false| active
|
|
39
|
+
reviewDisabledReconcile -->|true| implementationCompleteReconcile
|
|
40
|
+
reviewDisabledReconcile -->|false| active
|
|
41
|
+
implementationCompleteReconcile -->|true| autoMergeReconcile
|
|
42
|
+
implementationCompleteReconcile -->|false| active
|
|
43
|
+
autoMergeReconcile -->|true| headAvailableReconcile
|
|
44
|
+
autoMergeReconcile -->|false| active
|
|
45
|
+
headAvailableReconcile -->|true| mergeCapableReconcile
|
|
46
|
+
headAvailableReconcile -->|false| active
|
|
47
|
+
mergeCapableReconcile -->|true| mergeReconcile
|
|
48
|
+
mergeCapableReconcile -->|false| active
|
|
49
|
+
mergeReconcile --> active
|
|
50
|
+
|
|
51
|
+
active -->|on: review.approved| autoMergeReview
|
|
52
|
+
autoMergeReview -->|true| mergeReview
|
|
53
|
+
mergeReview --> active
|
|
54
|
+
active -->|on: implementation.completed| autoMergeImplementation
|
|
55
|
+
autoMergeImplementation -->|true| mergeImplementation
|
|
56
|
+
mergeImplementation --> active
|
|
57
|
+
active -->|on: effect.mergePullRequest.failed| active
|
|
58
|
+
active -->|on: pr.merged| cancelActiveMerge
|
|
59
|
+
cancelActiveMerge --> completed
|
|
60
|
+
active -->|on: cancel| cancelActive
|
|
61
|
+
cancelActive["action: effect.cancelActiveTasks"]
|
|
62
|
+
cancelActive --> cancelled
|
|
63
|
+
active -->|on: pr.closed| notMergedActive
|
|
64
|
+
notMergedActive -->|true| cancelActiveClose
|
|
65
|
+
cancelActiveClose --> cancelled
|
|
66
|
+
|
|
67
|
+
cancelled -->|on: pr.merged| cancelCancelledMerge
|
|
68
|
+
cancelCancelledMerge --> completed
|
|
69
|
+
cancelled -->|on: effect.cancelActiveTasks.failed| cancelled
|
|
70
|
+
completed -->|on: effect.cancelActiveTasks.failed| completed
|
|
@@ -3,14 +3,14 @@ stateDiagram-v2
|
|
|
3
3
|
|
|
4
4
|
DRAFT --> ACTIVE: activate
|
|
5
5
|
DRAFT --> COMPLETED: pr.merged / effect.cancelActiveTasks
|
|
6
|
-
DRAFT --> CANCELLED: cancel
|
|
6
|
+
DRAFT --> CANCELLED: cancel / effect.cancelActiveTasks
|
|
7
7
|
DRAFT --> CANCELLED: pr.closed [notMerged] / effect.cancelActiveTasks
|
|
8
8
|
|
|
9
9
|
ACTIVE --> ACTIVE: review.approved [autoMerge] / effect.mergePullRequest
|
|
10
10
|
ACTIVE --> ACTIVE: implementation.completed [autoMerge] / effect.mergePullRequest
|
|
11
11
|
ACTIVE --> ACTIVE: effect.mergePullRequest.failed
|
|
12
12
|
ACTIVE --> COMPLETED: pr.merged / effect.cancelActiveTasks
|
|
13
|
-
ACTIVE --> CANCELLED: cancel
|
|
13
|
+
ACTIVE --> CANCELLED: cancel / effect.cancelActiveTasks
|
|
14
14
|
ACTIVE --> CANCELLED: pr.closed [notMerged] / effect.cancelActiveTasks
|
|
15
15
|
|
|
16
16
|
CANCELLED --> COMPLETED: pr.merged / effect.cancelActiveTasks
|