agents-relay 1.0.177 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +4 -2
  2. package/dist/adapters.js +26 -2
  3. package/dist/blockers.js +60 -65
  4. package/dist/cli-support.js +33 -1
  5. package/dist/cli.js +72 -86
  6. package/dist/dashboard-actions.js +13 -35
  7. package/dist/dashboard-html.js +1 -1
  8. package/dist/dashboard.js +39 -0
  9. package/dist/execution-recovery.js +37 -0
  10. package/dist/graph-runtime.js +1 -0
  11. package/dist/job-actions.js +53 -0
  12. package/dist/manual-plan.js +15 -17
  13. package/dist/planner-runtime.js +135 -0
  14. package/dist/planner.js +76 -60
  15. package/dist/pool.js +54 -10
  16. package/dist/projected-store.js +314 -0
  17. package/dist/reconciler-review.js +185 -110
  18. package/dist/reconciler-runtime.js +67 -7
  19. package/dist/reconciler.js +134 -282
  20. package/dist/relay-store.js +380 -0
  21. package/dist/relayd-main.js +6 -0
  22. package/dist/relayd.js +122 -29
  23. package/dist/runtime-status.js +2 -1
  24. package/dist/scheduled-jobs.js +40 -31
  25. package/dist/scheduler.js +19 -18
  26. package/dist/store.js +1 -1
  27. package/dist/task-actions.js +139 -0
  28. package/dist/troubleshooter.js +14 -8
  29. package/dist/types.js +3 -3
  30. package/dist/unblock.js +31 -21
  31. package/dist/workflow/authoritative-unblock-v2.js +20 -0
  32. package/dist/workflow/blocker-resolution-v2.js +33 -0
  33. package/dist/workflow/contracts.js +21 -0
  34. package/dist/workflow/definitions-v2.js +75 -0
  35. package/dist/workflow/dispatcher.js +56 -0
  36. package/dist/workflow/entity-creation.js +32 -0
  37. package/dist/workflow/graph-v2.js +391 -0
  38. package/dist/workflow/job-reconcile-v2.js +19 -0
  39. package/dist/workflow/lifecycle.js +82 -4
  40. package/dist/workflow/planner-orchestration-v2.js +58 -0
  41. package/dist/workflow/planner-trigger-v2.js +24 -0
  42. package/dist/workflow/review-v2.js +42 -0
  43. package/dist/workflow/runtime.js +15 -1
  44. package/dist/workflow/schedule-cycle-v2.js +25 -0
  45. package/dist/workflow/scheduled-run-v2.js +30 -0
  46. package/dist/workflow/task-action-policy.js +19 -0
  47. package/dist/workflow/task-reconcile.js +6 -6
  48. package/dist/workflow/task-related-action-v2.js +42 -0
  49. package/dist/workflow/troubleshooter-v2.js +29 -0
  50. package/dist/workspace.js +6 -1
  51. package/package.json +3 -3
  52. package/skills/agents-relay/agents/planner.agent.md +5 -5
  53. package/skills/agents-relay/contracts/task-job-contract.md +2 -51
  54. package/workflow/authoritative-unblock-v2.mmd +15 -0
  55. package/workflow/blocker-resolution-v2.mmd +73 -0
  56. package/workflow/job-lifecycle-v2.mmd +70 -0
  57. package/workflow/job-lifecycle.mmd +2 -2
  58. package/workflow/planner-orchestration-v2.mmd +65 -0
  59. package/workflow/planner-trigger-v2.mmd +32 -0
  60. package/workflow/review-lifecycle-v2.mmd +124 -0
  61. package/workflow/schedule-cycle-v2.mmd +19 -0
  62. package/workflow/scheduled-run-v2.mmd +55 -0
  63. package/workflow/task-lifecycle-v2.mmd +201 -0
  64. package/workflow/task-lifecycle.mmd +3 -0
  65. package/workflow/task-related-action-v2.mmd +28 -0
  66. package/workflow/troubleshooter-v2.mmd +43 -0
@@ -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 { decideTaskLifecycle } from './lifecycle.js';
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 decision = decideTaskLifecycle(task, 'reconcile', facts);
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 ${task.state}`);
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
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.177",
3
+ "version": "2.0.1",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agents-relay": "dist/cli.js",
8
- "agents-relayd": "dist/relayd.js"
8
+ "agents-relayd": "dist/relayd-main.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsc -p tsconfig.json && chmod +x dist/cli.js dist/relayd.js",
11
+ "build": "tsc -p tsconfig.json && chmod +x dist/cli.js dist/relayd.js dist/relayd-main.js",
12
12
  "test": "npm run build && node --test test/*.test.js",
13
13
  "test:chatgpt-temp-quality": "AGENTS_RELAY_RUN_CHATGPT_TEMP_QUALITY=1 AGENTS_RELAY_CHATGPT_BROWSER_WORKER_AGENT=/Users/chengli/Workspace/neo/skills/chatgpt-browser-worker/agents/browser-worker.agent.md sh -c \"npm run build && node test/quality/run-temporary-chat.mjs\"",
14
14
  "dev": "tsx src/cli.ts",
@@ -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 two different views:
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
- - A later planning round must inspect the existing Task tree and attach work to the existing artifact it changes. Do not turn a later round into a new root merely because its work follows an earlier Task.
44
- - `plan-N` records are planning provenance, not business milestones, and must never be used as `parentTaskId`.
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
- For example, produce `architecture-v2` as a root milestone, then place `architecture-review`, `architecture-revision-1`, and `architecture-rereview-1` under it. The revision may depend on the review, and the re-review may depend on the revision; those dependencies do not determine their semantic parent. An independent `graph-runtime-v2` milestone remains a root sibling. A troubleshooter belongs under the failed Task (or its artifact subtree), and specialist review work stays under its review/artifact context.
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
- The revision may depend on Review and Re-review may depend on Revision 1, but
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
@@ -0,0 +1,65 @@
1
+ flowchart LR
2
+ %% relay: initial idle
3
+ idle["state: idle"]
4
+ checkLegacy["state: check_legacy_pending"]
5
+ checkRecover["state: check_recover"]
6
+ checkSeed["state: check_seed"]
7
+ checkPlanner["state: check_planner"]
8
+ checkBlocked["state: check_blocked"]
9
+ checkWork["state: check_work"]
10
+ checkMaterialized["state: check_materialized"]
11
+ checkObjective["state: check_objective"]
12
+ wait["state: wait"]
13
+ recovered["state: recovered"]
14
+ created["state: created"]
15
+
16
+ autonomous["guard: autonomousJob"]
17
+ legacyPending["guard: legacyPlanningPending"]
18
+ recoverablePending["guard: recoverablePendingPlan"]
19
+ hasSeed["guard: hasSeedWork"]
20
+ noActivePlanner["guard: noActivePlanner"]
21
+ noBlocked["guard: noBlockedWork"]
22
+ noActiveWork["guard: noActiveImplementation"]
23
+ materialized["guard: continuationMaterialized"]
24
+ unsatisfied["guard: objectiveUnsatisfied"]
25
+
26
+ recoverAction["action: planner.recoverPending"]
27
+ createAction["action: planner.createRound"]
28
+
29
+ idle -->|on: planner.reconcile| autonomous
30
+ autonomous -->|false| wait
31
+ autonomous -->|true| checkLegacy
32
+
33
+ checkLegacy -->|on: planner.reconcile| legacyPending
34
+ legacyPending -->|true| wait
35
+ legacyPending -->|false| checkRecover
36
+
37
+ checkRecover -->|on: planner.reconcile| recoverablePending
38
+ recoverablePending -->|true| recoverAction
39
+ recoverAction --> recovered
40
+ recoverablePending -->|false| checkSeed
41
+
42
+ checkSeed -->|on: planner.reconcile| hasSeed
43
+ hasSeed -->|false| wait
44
+ hasSeed -->|true| checkPlanner
45
+
46
+ checkPlanner -->|on: planner.reconcile| noActivePlanner
47
+ noActivePlanner -->|false| wait
48
+ noActivePlanner -->|true| checkBlocked
49
+
50
+ checkBlocked -->|on: planner.reconcile| noBlocked
51
+ noBlocked -->|false| wait
52
+ noBlocked -->|true| checkWork
53
+
54
+ checkWork -->|on: planner.reconcile| noActiveWork
55
+ noActiveWork -->|false| wait
56
+ noActiveWork -->|true| checkMaterialized
57
+
58
+ checkMaterialized -->|on: planner.reconcile| materialized
59
+ materialized -->|false| wait
60
+ materialized -->|true| checkObjective
61
+
62
+ checkObjective -->|on: planner.reconcile| unsatisfied
63
+ unsatisfied -->|false| wait
64
+ unsatisfied -->|true| createAction
65
+ createAction --> created
@@ -0,0 +1,32 @@
1
+ flowchart LR
2
+ %% relay: initial idle
3
+ idle["state: idle"]
4
+ checkUnblock["state: check_unblock"]
5
+ checkSchedule["state: check_schedule"]
6
+ checkInitial["state: check_initial"]
7
+ manual["state: manual"]
8
+ unblock["state: unblock"]
9
+ schedule["state: schedule"]
10
+ initial["state: initial"]
11
+ continuation["state: continuation"]
12
+
13
+ manualGuard["guard: manualPlan"]
14
+ unblockGuard["guard: hasUnblock"]
15
+ scheduleGuard["guard: scheduledRun"]
16
+ initialGuard["guard: initialPlan"]
17
+
18
+ idle -->|on: plan.requested| manualGuard
19
+ manualGuard -->|true| manual
20
+ manualGuard -->|false| checkUnblock
21
+
22
+ checkUnblock -->|on: plan.requested| unblockGuard
23
+ unblockGuard -->|true| unblock
24
+ unblockGuard -->|false| checkSchedule
25
+
26
+ checkSchedule -->|on: plan.requested| scheduleGuard
27
+ scheduleGuard -->|true| schedule
28
+ scheduleGuard -->|false| checkInitial
29
+
30
+ checkInitial -->|on: plan.requested| initialGuard
31
+ initialGuard -->|true| initial
32
+ initialGuard -->|false| continuation