agents-relay 2.0.169 → 2.0.171

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 (74) hide show
  1. package/README.md +13 -19
  2. package/dist/adapters.js +2 -8
  3. package/dist/blockers.js +3 -2
  4. package/dist/cli-help.js +3 -3
  5. package/dist/cli-job-create.js +2 -4
  6. package/dist/cli-support.js +5 -16
  7. package/dist/cli.js +9 -11
  8. package/dist/dashboard-html.js +5 -9
  9. package/dist/dashboard-style.js +0 -1
  10. package/dist/execution-recovery.js +1 -1
  11. package/dist/force-restate-validation.js +11 -1
  12. package/dist/interactive-resume.js +1 -1
  13. package/dist/job-actions.js +1 -1
  14. package/dist/job-pause.js +1 -1
  15. package/dist/legacy-blocked-task-migration.js +1 -1
  16. package/dist/local-authority-store.js +0 -57
  17. package/dist/local-git-merge.js +1 -2
  18. package/dist/local-merge-delivery.js +1 -1
  19. package/dist/markers.js +5 -6
  20. package/dist/planned-review-head.js +2 -2
  21. package/dist/planner-runtime.js +3 -2
  22. package/dist/planner.js +2 -7
  23. package/dist/pool.js +7 -36
  24. package/dist/reconciler-review.js +4 -5
  25. package/dist/reconciler.js +11 -36
  26. package/dist/relay-create.js +3 -3
  27. package/dist/relay-job-snapshot-reads.js +6 -2
  28. package/dist/relay-store.js +2 -2
  29. package/dist/relay-workflow-operations.js +2 -2
  30. package/dist/relayd-control.js +9 -7
  31. package/dist/relayd-utilities.js +0 -4
  32. package/dist/relayd.js +7 -24
  33. package/dist/runtime-status.js +0 -19
  34. package/dist/single-job-task-control.js +1 -1
  35. package/dist/store.js +1 -1
  36. package/dist/task-actions.js +3 -3
  37. package/dist/task-recovery.js +2 -2
  38. package/dist/task-transitions.js +61 -0
  39. package/dist/troubleshooter-fallback.js +1 -1
  40. package/dist/troubleshooter.js +1 -4
  41. package/dist/types.js +1 -2
  42. package/dist/unblock.js +5 -9
  43. package/dist/worker-rejection.js +1 -1
  44. package/dist/workflow/authoritative-conditions.js +1 -1
  45. package/dist/workflow/blocker-resolution-v2.js +2 -4
  46. package/dist/workflow/contracts.js +0 -1
  47. package/dist/workflow/definitions-v2.js +3 -6
  48. package/dist/workflow/durable-transition.js +1 -1
  49. package/dist/workflow/force-restate-validation-v2.js +2 -1
  50. package/dist/workflow/orchestrator-rescue.js +1 -1
  51. package/dist/workflow/planner-orchestration-v2.js +2 -5
  52. package/dist/workflow/planner-trigger-v2.js +1 -2
  53. package/dist/workflow/review-v2.js +0 -1
  54. package/dist/workflow/task-reconcile.js +1 -7
  55. package/dist/workflow-effect-failures.js +1 -1
  56. package/dist/workspace.js +1 -1
  57. package/package.json +3 -4
  58. package/skills/agents-relay/SKILL.md +6 -6
  59. package/skills/agents-relay/agents/planner.agent.md +3 -3
  60. package/skills/agents-relay/contracts/event-contract.md +10 -0
  61. package/skills/agents-relay/contracts/orchestrator-workflows.md +2 -2
  62. package/skills/agents-relay/contracts/task-job-contract.md +5 -5
  63. package/workflow/blocker-resolution.mmd +1 -7
  64. package/workflow/force-restate-validation.mmd +7 -2
  65. package/workflow/planner-orchestration.mmd +2 -5
  66. package/workflow/planner-trigger.mmd +2 -9
  67. package/workflow/review-lifecycle.mmd +2 -8
  68. package/dist/e2e-injection.js +0 -76
  69. package/dist/scheduled-jobs.js +0 -67
  70. package/dist/scheduler.js +0 -214
  71. package/dist/workflow/schedule-cycle-v2.js +0 -25
  72. package/dist/workflow/scheduled-run-v2.js +0 -30
  73. package/workflow/schedule-cycle.mmd +0 -23
  74. package/workflow/scheduled-run.mmd +0 -59
@@ -104,29 +104,29 @@ The ChatGPT adapter uses Relay's `chatgpt-worker` agent contract and Browser Wor
104
104
 
105
105
  ChatGPT thread identity is diagnostic only. It is not persisted as a resumable task handle, and Relay does not poll, resume, retrieve a result from, or delete a ChatGPT conversation as part of task lifecycle.
106
106
 
107
- ## Autonomous planner
107
+ ## Objective-driven Planner
108
108
 
109
109
  Direct orchestrator-created Tasks do not require a Plan. The orchestrator may create the initial batch with `task create`; those Tasks remain top-level unless the orchestrator explicitly gives them normal task parentage. When planner-driven decomposition is desired instead, use `agents-relay plan --repo OWNER/REPO --pr N --id JOB_ID [--prompt TEXT | --prompt-file PATH|-]`.
110
110
 
111
- For explicit `plan` and autonomous continuation, Relay invokes the same planner agent as a pure decision function. The planner receives durable Job/Task state, previous planner rounds, and resolved execution context, then returns structured `{ objective_status, assessment, next_tasks }` output. The planner does not call Relay CLI and does not persist Tasks itself.
111
+ For explicit `plan` and automatic continuation, Relay invokes the same planner agent as a pure decision function. The planner receives durable Job/Task state, previous planner rounds, and resolved execution context, then returns structured `{ objective_status, assessment, next_tasks }` output. The planner does not call Relay CLI and does not persist Tasks itself.
112
112
 
113
113
  Each successful planner invocation is one real `plan-N` planning round. Relay persists that planner result, then creates returned work Tasks with provenance back to that planning round. `parentTaskId` expresses semantic ownership among work Tasks; it is not inherited from the Plan and must never point to a `plan-N` record. `dependencies` independently express execution prerequisites. Historical `planner-N` records remain readable. Only planner execution creates planner rounds; Relay never manufactures synthetic `plan-*` records for direct Task creation. In the dashboard, planner rounds remain provenance/grouping metadata, not semantic parents.
114
114
 
115
- A zero-task autonomous Job remains idle. After a direct orchestrator-created initial batch completes, Relay may start the first real planner round. Fixed Jobs do not automatically create planner rounds.
115
+ A zero-task DRAFT Job remains idle until explicit first planning or direct Task creation. After initial work settles, every Job automatically replans until the latest successful Plan is `satisfied`; there is no fixed/autonomous mode or Job scheduler.
116
116
 
117
117
  Planner-created child Tasks remain descriptive agent work and must include the output contract and `storySize`; worker requirements are optional soft routing facts unless an explicit runtime constraint is known. Execution Router chooses the complete runtime route afterward.
118
118
 
119
119
  ## Durable truth
120
120
 
121
- GitHub PR/job/task markers remain durable workflow state. Events are the authoritative execution-state transition signal for model-backed tasks. Runtime events, process exits, browser submission receipts, and conversation text are evidence only.
121
+ SQLite is the sole local durable lifecycle authority. GitHub PR/Job/Task markers are collaboration projections. Correlated model-worker events are inputs to Graph-selected, SQLite-committed transitions; runtime events, process exits, browser receipts, and conversation text are not themselves completion proof.
122
122
 
123
123
  Keep secrets, credentials, private prompts, and large private payloads out of durable markers and events.
124
124
 
125
125
  ### Automatic task troubleshooting
126
126
 
127
- When an ACTIVE Job contains a current-run `FAILED` task, reconciliation may create one P0-priority `troubleshooter` recovery task for that exact failed attempt. `BLOCKED` is resolved only through authoritative unblock, which records evidence and transitions the Task to `UNBLOCKED`. The recovery task is routed to a local Codex-capable worker with repository/tool access and runs under the bundled Troubleshooter agent contract. Autonomous `UNBLOCKED` Tasks are historical and feed the next Planner round instead of being retried or sent through Troubleshooter. Each Planner round is durably represented by an `agents-relay:plan:v1` PR comment created before generated Task comments and updated in place with its assessment and generated Task IDs. Troubleshooter diagnosis is event-first: it must inspect the failed task's task-scoped lifecycle event history (including ordering, duplicate/missing/late events, source and phase data) before classifying root cause or retrying.
127
+ When an ACTIVE Job contains a current-run `FAILED` task, reconciliation may create one P0-priority `troubleshooter` recovery task for that exact failed attempt. `BLOCKED` is resolved only through authoritative unblock, which records evidence and transitions the Task to `UNBLOCKED`. The recovery task is routed to a local Codex-capable worker with repository/tool access and runs under the bundled Troubleshooter agent contract. Historical ordinary unblock evidence is Planner recovery context; only an effectively blocked Troubleshooter can be authoritatively resumed. Each Planner round is durably represented by an `agents-relay:plan:v1` PR comment created before generated Task comments and updated in place with its assessment and generated Task IDs. Troubleshooter diagnosis is event-first: it must inspect the failed task's task-scoped lifecycle event history (including ordering, duplicate/missing/late events, source and phase data) before classifying root cause or retrying.
128
128
 
129
- The Troubleshooter diagnoses before retrying. A task-local or transient blocker is repaired narrowly; a shared worker/runtime/tool defect is repaired through the owning repository's P0/self-job workflow. Only after the blocker is removed does it retry the original task and verify durable progress. Historical failures from older scheduled autonomous runs do not spawn recovery work, and deterministic reconciliation does not duplicate a Troubleshooter task for the same failed attempt.
129
+ The Troubleshooter diagnoses before retrying. A task-local or transient blocker is repaired narrowly; a shared worker/runtime/tool defect is repaired through the owning repository's P0/self-job workflow. Only after the blocker is removed does it retry the original task and verify durable progress. Historical resolved failures do not spawn duplicate recovery work, and deterministic reconciliation does not duplicate a Troubleshooter task for the same failed attempt.
130
130
 
131
131
  ## Persistent service bootstrap
132
132
 
@@ -1,6 +1,6 @@
1
1
  # Planner Agent
2
2
 
3
- You are the default objective planner for Agents Relay autonomous jobs.
3
+ You are the default objective planner for all Agents Relay Jobs.
4
4
 
5
5
  Before producing tasks, read and follow `../contracts/task-job-contract.md`.
6
6
 
@@ -25,10 +25,10 @@ For every non-satisfied objective:
25
25
 
26
26
  ### Objective decomposition and convergence
27
27
 
28
- - **Never bundle independently testable acceptance layers into one giant Task.** Break objectives into separately verifiable deliverables (for example, contract tests, real-provider compliance, scheduler-driven end-to-end scenarios, and integration evidence). Give each Task concrete acceptance checks and an observable output.
28
+ - **Never bundle independently testable acceptance layers into one giant Task.** Break objectives into separately verifiable deliverables (for example, contract tests, real-provider compliance, L1 Graph branches, L2 role evidence, and L3 production-ingress SQLite replay, and integration evidence). Give each Task concrete acceptance checks and an observable output.
29
29
  - Split a large Task when its remaining work spans distinct implementation areas, independently testable failure paths, or more than one meaningful review/recovery cycle. Put shared prerequisites in earlier Tasks and express true dependencies explicitly; independent work may run in parallel.
30
30
  - For a failed Task with repeated deterministic acceptance gaps, do not reissue the identical broad assignment. Identify the *missing acceptance cases*, create focused same-Job repair Tasks with valid `same_job_dependency` resolutions, and specify how their evidence unblocks the original Task. Preserve the original failed Task and failure history.
31
- - For event-driven workflows, separate basic ingress/persistence checks from real scheduler-driven multi-stage scenarios. Require negative-path, multi-failure recovery and no-false-completion evidence before claiming full objective coverage.
31
+ - For event-driven workflows, separate basic ingress/persistence checks from L3 production Reconciler/Graph/SQLite multi-stage replay scenarios. Require negative-path, multi-failure recovery and no-false-completion evidence before claiming full objective coverage.
32
32
  - In `assessment`, show the entire remaining objective as a dependency graph, clearly distinguishing already proven work, the executable batch, subsequent verification, and Relay-owned review/merge. Never report overall `satisfied` merely because one batch has passed.
33
33
 
34
34
 
@@ -2,6 +2,16 @@
2
2
 
3
3
  **Scope:** Relay-specific event meaning and validation. This does **not** define Events Bus transport. The bus only handles generic event identity, type, timestamp, routing as needed, and opaque `data`. No Events Bus change is required when Relay adds agent roles or payload fields.
4
4
 
5
+ ## Execution event source
6
+
7
+ Every Task lifecycle event must identify the exact current Relay execution with:
8
+
9
+ ```text
10
+ source.id = job/<job-id>/task/<task-id>/execution/<execution-id>
11
+ ```
12
+
13
+ Relay supplies this complete identity in the managed Task context. Agents must copy it unchanged; they must not infer a Job/Task ID, generate an execution ID, or reuse an identity from a previous attempt. The Neo `events.publish` tool accepts this value as its string `source` argument; the Events Bus event envelope represents it as `source.id`. Transport acceptance alone does not establish that Relay accepted the event for the current execution.
14
+
5
15
  ## Producer contract
6
16
 
7
17
  Every Implementation, Reviewer, and Troubleshooter execution publishes `task.started`, optional `task.progress`, then **exactly one** agent terminal event:
@@ -20,7 +20,7 @@ job create --initial-plan
20
20
 
21
21
  Without `--initial-plan`, `job create` remains create-only and establishes durable Job/PR state without execution. `agents-relay plan` remains available for an existing create-only/adopted Job. Unless the user explicitly requested **create-only / no execution**, the orchestrator should use `job create --initial-plan` rather than issuing two separate commands.
22
22
 
23
- The initial decomposition belongs to that durable Planner round. The orchestrator must not hand-author the initial child Task batch as a substitute. If initial planning fails, the create command fails visibly after preserving the durable Job/Plan evidence; the orchestrator must not silently invent Tasks. This applies regardless of whether the Job uses `fixed` or `autonomous` execution mode. Later replanning continues to follow Relay's normal Planner/reconciliation rules.
23
+ The initial decomposition belongs to that durable Planner round. The orchestrator must not hand-author the initial child Task batch as a substitute. If initial planning fails, the create command fails visibly after preserving the durable Job/Plan evidence; the orchestrator must not silently invent Tasks. Every Job follows this same objective-driven lifecycle. After a completed implementation batch, Relay automatically creates subsequent Plan rounds until the latest successful Plan reports `objective_status: satisfied`; there is no fixed/autonomous execution mode or Job scheduling.
24
24
 
25
25
  ## P0 / self job workflow
26
26
 
@@ -48,7 +48,7 @@ state and hand subsequent execution to Relay.
48
48
 
49
49
  ## Merge completion vs deployment evidence
50
50
 
51
- For a normal one-shot managed Job, the canonical Job lifecycle graph owns completion: a durable `pr.merged` fact transitions the Job to `COMPLETED`. Reconciler and deployment code must not invent a second completion gate after merge.
51
+ For a managed Job, the canonical Job lifecycle Graph owns completion. A satisfied Plan, successful implementation evidence, applicable SHA-bound review, and a verified local merge/delivery receipt are required. Remote GitHub `pr.merged` facts are audit-only and never independently complete a local Job. Deployment code must not invent another completion gate after durable Job completion.
52
52
 
53
53
  Post-merge publication and deployment activity is separate from Job lifecycle. Events or evidence such as `package.published`, published version metadata, service restart, and deployment verification may be attached after the Job is already `COMPLETED`; they must not hold the Job open, reopen it, or redefine completion. If post-merge deployment needs remediation, record that as separate deployment/recovery work rather than mutating the completed implementation Job lifecycle.
54
54
 
@@ -77,7 +77,7 @@ The Job owns the meaning of the tag. Its trusted Job marker declares a human-rea
77
77
  ```json
78
78
  {
79
79
  "tag": {
80
- "description": "Deterministic E2E outcome. Set pass when the fixed E2E exits 0; set fail when it still fails after the required retry.",
80
+ "description": "L3 verification outcome. Set pass only after authoritative SQLite replay checks pass; set fail when a required check fails.",
81
81
  "values": {
82
82
  "pass": "✅",
83
83
  "fail": "❌"
@@ -109,9 +109,9 @@ Relay validates `data.tag` against `Job.tag.values`. If the value is allowed, Re
109
109
 
110
110
  The dashboard always keeps the normal lifecycle badge. When a valid Task tag is present, it additionally renders the display string from `Job.tag.values` after the lifecycle status. For the example above this appears as `SUCCEEDED ✅`.
111
111
 
112
- A tag must describe something specific to the Job contract, not Relay lifecycle. Examples include an E2E result (`pass` / `fail`), publication result, validation classification, or another small Job-defined outcome. The Job description must tell the worker when to set the tag and what each allowed value means. Jobs without a `tag` declaration behave exactly as before.
112
+ A tag must describe something specific to the Job contract, not Relay lifecycle. Examples include a verified L3 result (`pass` / `fail`), publication result, validation classification, or another small Job-defined outcome. The Job description must tell the worker when to set the tag and what each allowed value means. Jobs without a `tag` declaration behave exactly as before.
113
113
 
114
- For the ChatGPT Browser Worker guardian, for example, the Job defines `pass -> ✅` and `fail -> ❌`; each hourly E2E child Task reports `data.tag` after running the fixed deterministic E2E, while the Task lifecycle independently records whether Relay execution itself completed, failed, or became blocked.
114
+ For a validation Job, for example, the Job defines `pass -> ✅` and `fail -> ❌`; each L3 verification Task reports `data.tag` after the isolated replay and evidence checks, while Task lifecycle independently records the worker outcome.
115
115
 
116
116
 
117
117
  ### Task resolution classifications
@@ -139,9 +139,9 @@ SHA-bound final reviewer. For example, `video-director + market + style ->
139
139
  review-chair -> unified revision plan` is a project pattern, not a hard-coded
140
140
  Relay reviewer list.
141
141
 
142
- A scheduled autonomous job adds cron/timezone/next-run and missed-run/concurrency policy to the trusted job marker. The always-on `agents-relayd` process is the sole heartbeat for repositories it manages: on startup and every ten minutes it evaluates cached managed Jobs and reconciles due work through the local worker pool. There is no GitHub Actions scheduler fallback. Unscheduled autonomous Jobs and fixed Jobs do not use the scheduler heartbeat.
142
+ Every Job is objective-driven. After a direct initial Task batch or a Planner-produced batch settles, reconciliation evaluates the canonical Planner Graph and creates the next durable `plan-N` round until the latest successful Plan returns `objective_status: satisfied`. There is no fixed/autonomous mode, no Job schedule, no cron slot, and no Scheduler entity. The daemon performs startup recovery and wake-driven reconciliation from SQLite. A satisfied Plan alone does not complete a Job: successful implementation evidence, SHA-bound review when enabled, and verified local merge/delivery remain mandatory.
143
143
 
144
- The scheduler heartbeat filters cached managed Jobs, reloads each due Job to validate the marker's exact `nextRunAt`, claims the slot idempotently, advances the durable next run, and reconciles planner/worker execution locally. While the schedule is active, the durable Job remains `ACTIVE`; between runs its derived runtime status is `WAITING`. Scheduled recurring Jobs do not enter one-shot final-review/auto-merge after each satisfied run; older-cycle failures are historical evidence and do not block a later slot.
144
+ A newly created zero-task DRAFT Job waits for explicit initial planning (`plan` or `--initial-plan`) or direct work creation; this is only a start gate, not a distinct execution mode. Failed or blocked Tasks require durable Troubleshooter/owner recovery and never allow a fabricated completion. Duplicate events and daemon restarts must not create duplicate Plan rounds.
145
145
 
146
146
 
147
147
  ## Task evidence and head identity
@@ -6,7 +6,6 @@ flowchart LR
6
6
  %% relay: initial waiting
7
7
  waiting["state: waiting"]
8
8
  checkTarget["state: check_target"]
9
- checkAutonomous["state: check_autonomous"]
10
9
  checkEffective["state: check_effective"]
11
10
  checkAlreadyUnblocked["state: check_already_unblocked"]
12
11
  checkDraft["state: check_draft"]
@@ -19,7 +18,6 @@ resumed["state: resumed"]
19
18
 
20
19
  resolutionAllowed["guard: resolutionAllowed"]
21
20
  targetMatches["guard: targetMatches"]
22
- autonomousJob["guard: autonomousJob"]
23
21
  effectivelyBlocked["guard: effectivelyBlocked"]
24
22
  alreadyUnblocked["guard: alreadyUnblocked"]
25
23
  isDraft["guard: isDraft"]
@@ -35,11 +33,7 @@ resolutionAllowed -->|true| checkTarget
35
33
 
36
34
  checkTarget -->|on: blocker.resolved| targetMatches
37
35
  targetMatches -->|false| obsolete
38
- targetMatches -->|true| checkAutonomous
39
-
40
- checkAutonomous -->|on: blocker.resolved| autonomousJob
41
- autonomousJob -->|true| checkEffective
42
- autonomousJob -->|false| checkDraft
36
+ targetMatches -->|true| checkEffective
43
37
 
44
38
  checkEffective -->|on: blocker.resolved| effectivelyBlocked
45
39
  effectivelyBlocked -->|true| wait
@@ -8,12 +8,14 @@ idle["state: idle"]
8
8
  checkHistoryMode["state: check_history_mode"]
9
9
  checkHistory["state: check_history"]
10
10
  checkNormalHistory["state: check_normal_history"]
11
+ checkSatisfied["state: check_satisfied"]
11
12
  checkForward["state: check_forward"]
12
13
  commit["action: plan.apply.commit"]
13
14
  rejected["state: rejected"]
14
15
  committed["state: committed"]
15
16
  forceRestate["guard: forceRestateRequested"]
16
17
  historyConsistent["guard: historyConsistencyValid"]
18
+ objectiveSatisfied["guard: objectiveSatisfied"]
17
19
  forwardConverged["guard: forwardConvergenceValid"]
18
20
  normalHistoryClean["guard: normalHistoryMutationAbsent"]
19
21
  rejectHistory["action: plan.apply.rejectHistory"]
@@ -24,12 +26,15 @@ checkHistoryMode -->|on: plan.apply| forceRestate
24
26
  forceRestate -->|true| checkHistory
25
27
  forceRestate -->|false| checkNormalHistory
26
28
  checkNormalHistory -->|on: plan.apply| normalHistoryClean
27
- normalHistoryClean -->|true| checkForward
29
+ normalHistoryClean -->|true| checkSatisfied
28
30
  normalHistoryClean -->|false| rejectHistory
29
31
  checkHistory -->|on: plan.apply| historyConsistent
30
- historyConsistent -->|true| checkForward
32
+ historyConsistent -->|true| checkSatisfied
31
33
  historyConsistent -->|false| rejectHistory
32
34
  rejectHistory --> rejected
35
+ checkSatisfied -->|on: plan.apply| objectiveSatisfied
36
+ objectiveSatisfied -->|true| commit
37
+ objectiveSatisfied -->|false| checkForward
33
38
  checkForward -->|on: plan.apply| forwardConverged
34
39
  forwardConverged -->|true| commit
35
40
  forwardConverged -->|false| rejectForward
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Planner Orchestration
3
- description: Guards autonomous planning rounds, interrupted plans, and planner recovery.
3
+ description: Guards objective-driven planning rounds, interrupted plans, and planner recovery.
4
4
  ---
5
5
  flowchart LR
6
6
  %% relay: initial idle
@@ -21,7 +21,6 @@ wait["state: wait"]
21
21
  recovered["state: recovered"]
22
22
  created["state: created"]
23
23
 
24
- autonomous["guard: autonomousJob"]
25
24
  legacyPending["guard: legacyPlanningPending"]
26
25
  recoverablePending["guard: recoverablePendingPlan"]
27
26
  terminalFailure["guard: terminalPlanFailure"]
@@ -39,9 +38,7 @@ recoverAction["action: planner.recoverPending"]
39
38
  createRecovery["action: planner.createRecovery"]
40
39
  createAction["action: planner.createRound"]
41
40
 
42
- idle -->|on: planner.reconcile| autonomous
43
- autonomous -->|false| wait
44
- autonomous -->|true| checkLegacy
41
+ idle -->|on: planner.reconcile| checkLegacy
45
42
 
46
43
  checkLegacy -->|on: planner.reconcile| legacyPending
47
44
  legacyPending -->|true| wait
@@ -1,22 +1,19 @@
1
1
  ---
2
2
  title: Planner Trigger
3
- description: Classifies planning requests as manual, unblock, scheduled, initial, or continuation.
3
+ description: Classifies planning requests as manual, unblock, initial, or continuation.
4
4
  ---
5
5
  flowchart LR
6
6
  %% relay: initial idle
7
7
  idle["state: idle"]
8
8
  checkUnblock["state: check_unblock"]
9
- checkSchedule["state: check_schedule"]
10
9
  checkInitial["state: check_initial"]
11
10
  manual["state: manual"]
12
11
  unblock["state: unblock"]
13
- schedule["state: schedule"]
14
12
  initial["state: initial"]
15
13
  continuation["state: continuation"]
16
14
 
17
15
  manualGuard["guard: manualPlan"]
18
16
  unblockGuard["guard: hasUnblock"]
19
- scheduleGuard["guard: scheduledRun"]
20
17
  initialGuard["guard: initialPlan"]
21
18
 
22
19
  idle -->|on: plan.requested| manualGuard
@@ -25,11 +22,7 @@ manualGuard -->|false| checkUnblock
25
22
 
26
23
  checkUnblock -->|on: plan.requested| unblockGuard
27
24
  unblockGuard -->|true| unblock
28
- unblockGuard -->|false| checkSchedule
29
-
30
- checkSchedule -->|on: plan.requested| scheduleGuard
31
- scheduleGuard -->|true| schedule
32
- scheduleGuard -->|false| checkInitial
25
+ unblockGuard -->|false| checkInitial
33
26
 
34
27
  checkInitial -->|on: plan.requested| initialGuard
35
28
  initialGuard -->|true| initial
@@ -13,7 +13,6 @@ changes["state: CHANGES_REQUIRED"]
13
13
  blocked["state: BLOCKED"]
14
14
  stale["state: STALE"]
15
15
 
16
- unscheduledNone["guard: unscheduled"]
17
16
  reviewEnabledNone["guard: reviewEnabled"]
18
17
  jobActiveNone["guard: jobActive"]
19
18
  implementationCompleteNone["guard: implementationComplete"]
@@ -21,7 +20,6 @@ headAvailableNone["guard: headAvailable"]
21
20
  completionGateNone["guard: completionGateSatisfied"]
22
21
  noActiveReviewNone["guard: noActiveReviewTask"]
23
22
  noApprovedReviewNone["guard: noApprovedCurrentReview"]
24
- unscheduledStale["guard: unscheduled"]
25
23
  reviewEnabledStale["guard: reviewEnabled"]
26
24
  jobActiveStale["guard: jobActive"]
27
25
  implementationCompleteStale["guard: implementationComplete"]
@@ -49,9 +47,7 @@ fixChanges["action: review.createFix"]
49
47
  mergePending["action: review.requestMerge"]
50
48
  mergeStarted["action: review.requestMerge"]
51
49
 
52
- none -->|on: reconcile| unscheduledNone
53
- unscheduledNone -->|true| reviewEnabledNone
54
- unscheduledNone -->|false| none
50
+ none -->|on: reconcile| reviewEnabledNone
55
51
  reviewEnabledNone -->|true| jobActiveNone
56
52
  reviewEnabledNone -->|false| none
57
53
  jobActiveNone -->|true| implementationCompleteNone
@@ -68,9 +64,7 @@ noApprovedReviewNone -->|true| createNone
68
64
  noApprovedReviewNone -->|false| none
69
65
  createNone --> pending
70
66
 
71
- stale -->|on: reconcile| unscheduledStale
72
- unscheduledStale -->|true| reviewEnabledStale
73
- unscheduledStale -->|false| stale
67
+ stale -->|on: reconcile| reviewEnabledStale
74
68
  reviewEnabledStale -->|true| jobActiveStale
75
69
  reviewEnabledStale -->|false| stale
76
70
  jobActiveStale -->|true| implementationCompleteStale
@@ -1,76 +0,0 @@
1
- const legacy = {
2
- quota_exceeded: { method: 'worker.codex.launch', action: 'return', execution: 'skip', mock: 'worker_quota' },
3
- model_not_found: { method: 'worker.codex.launch', action: 'return', execution: 'skip', mock: 'worker_model_not_found' },
4
- provider_unavailable: { method: 'worker.codex.launch', action: 'return', execution: 'skip', mock: 'worker_provider_unavailable' },
5
- github_projection_failure: { method: 'github.projectLocalMerge', action: 'throw', execution: 'skip', error: { code: 'SERVICE_UNAVAILABLE', message: 'E2E transient GitHub projection transport failure', status: 503 } },
6
- };
7
- export function parseTestInjection(value) {
8
- if (typeof value === 'string')
9
- return legacy[value];
10
- if (!value || typeof value !== 'object' || Array.isArray(value))
11
- return undefined;
12
- const v = value;
13
- if (v.method !== 'worker.codex.launch' && v.method !== 'github.projectLocalMerge' && v.method !== 'browserWorkspace.command')
14
- return undefined;
15
- if (v.action !== 'throw' && v.action !== 'return' && v.action !== 'pass')
16
- return undefined;
17
- if (v.execution !== 'skip' && v.execution !== 'after' && v.execution !== 'run')
18
- return undefined;
19
- const error = v.error;
20
- if (v.action === 'throw' && (!error || typeof error !== 'object' || typeof error.message !== 'string' || typeof error.code !== 'string'))
21
- return undefined;
22
- const mock = v.mock;
23
- if (v.action === 'return' && (v.method !== 'worker.codex.launch' || !['worker_quota', 'worker_model_not_found', 'worker_provider_unavailable'].includes(String(mock))))
24
- return undefined;
25
- if (v.method === 'worker.codex.launch' && v.action !== 'pass' && (v.action !== 'return' || v.execution !== 'skip'))
26
- return undefined;
27
- if (v.action === 'pass' && v.execution !== 'run')
28
- return undefined;
29
- if (v.action !== 'pass' && v.execution === 'run')
30
- return undefined;
31
- return { method: v.method, action: v.action, execution: v.execution, error: error, mock: mock };
32
- }
33
- export function authorizedInjection(repository, raw, env = process.env) {
34
- return env.E2E_REPO && repository === env.E2E_REPO ? parseTestInjection(raw) : undefined;
35
- }
36
- function mockWorker(name) {
37
- const message = name === 'worker_quota' ? 'rate_limit_exceeded: usage limit reached' : name === 'worker_model_not_found' ? 'model_not_found: model is unavailable' : 'provider_unavailable: provider refused the request';
38
- const promise = Promise.reject(new Error(message));
39
- promise.catch(() => { });
40
- return { id: 'e2e-injected', launched: Promise.resolve(), promise, cancel: () => { }, processExit: { exitCode: 1, signal: null, stderr: message, terminationReason: null } };
41
- }
42
- /** Caller must supply a configuration already consumed durably before this invocation. */
43
- export function withTestInjection(method, config, execute) {
44
- if (!config || config.method !== method || config.action === 'pass')
45
- return execute();
46
- const apply = (original) => {
47
- if (config.action === 'throw') {
48
- const error = Object.assign(new Error(config.error.message), { code: config.error.code, status: config.error.status });
49
- throw error;
50
- }
51
- if (method === 'worker.codex.launch' && config.mock)
52
- return mockWorker(config.mock);
53
- return original;
54
- };
55
- return config.execution === 'after' ? Promise.resolve(execute()).then(apply) : apply();
56
- }
57
- /** Shared atomic consumer, scoped solely by E2E_REPO and exact method. */
58
- export async function consumeMethodInjection(relay, repository, jobId, method) {
59
- if (!process.env.E2E_REPO || process.env.E2E_REPO !== repository || !relay.mutateJob)
60
- return undefined;
61
- let config;
62
- await relay.mutateJob(jobId, job => {
63
- if (job.repository !== repository)
64
- return job;
65
- const task = job.tasks.find(t => authorizedInjection(job.repository, t.testInjection)?.method === method);
66
- if (!task)
67
- return job;
68
- config = authorizedInjection(job.repository, task.testInjection);
69
- delete task.testInjection;
70
- task.updatedAt = new Date(Math.max(Date.now(), Date.parse(task.updatedAt) + 1)).toISOString();
71
- return job;
72
- });
73
- if (config && (await relay.loadJob(jobId))?.tasks.some(t => authorizedInjection(repository, t.testInjection)?.method === method))
74
- throw Error(`E2E injection consumption not committed: ${jobId}`);
75
- return config;
76
- }
@@ -1,67 +0,0 @@
1
- import { nextScheduleRun, scheduleBucket } from './scheduler.js';
2
- import { dispatchScheduledRun } from './workflow/scheduled-run-v2.js';
3
- /** Local-first schedule entrypoint: immutable Job ID selects the SQLite store. */
4
- export async function runScheduledBucketFromStore(store, ref, engine, now = new Date()) {
5
- // SQLite authority provides an atomic claim. The fallback retains the
6
- // existing in-memory test adapter, but cannot be used for production claims.
7
- if (store.claimScheduledSlot) {
8
- if (!store.finishScheduledSlot)
9
- throw new Error('Atomic scheduled completion is required');
10
- const claimed = await store.claimScheduledSlot(ref.jobId, ref.bucket, now);
11
- if (claimed.status !== 'completed')
12
- return claimed;
13
- const slot = claimed.job.schedule?.currentRunSlot;
14
- if (!slot)
15
- throw new Error('Claimed schedule slot missing durable currentRunSlot');
16
- await engine.reconcile(ref.jobId);
17
- await engine.idle();
18
- const job = await store.finishScheduledSlot(ref.jobId, slot, now);
19
- return { job, status: 'completed' };
20
- }
21
- let job = await store.load(ref.jobId);
22
- if (!job.schedule)
23
- throw new Error('Scheduled job ' + job.id + ' has no schedule metadata');
24
- const schedule = job.schedule;
25
- const slot = new Date(schedule.nextRunAt);
26
- const active = job.tasks.some(task => ['QUEUED', 'RUNNING'].includes(task.state));
27
- let claimed = false;
28
- let status = 'skipped-idempotent';
29
- const decision = await dispatchScheduledRun({
30
- slotDue: scheduleBucket(slot) === ref.bucket && slot.getTime() <= now.getTime(),
31
- slotUnclaimed: !schedule.lastScheduledSlot || new Date(schedule.lastScheduledSlot).getTime() < slot.getTime(),
32
- jobActive: job.state === 'ACTIVE', hasActiveTasks: active,
33
- concurrencySkip: schedule.concurrencyPolicy === 'skip', concurrencyQueue: schedule.concurrencyPolicy === 'queue',
34
- runMissed: schedule.missedRunPolicy === 'run',
35
- }, async (actionId) => {
36
- if (actionId === 'schedule.skipActiveAdvance') {
37
- schedule.lastScheduledSlot = slot.toISOString();
38
- schedule.nextRunAt = nextScheduleRun(schedule, now).toISOString();
39
- status = 'skipped-active';
40
- }
41
- else if (actionId === 'schedule.claimFromSlot' || actionId === 'schedule.claimFromNow') {
42
- schedule.lastScheduledSlot = slot.toISOString();
43
- schedule.currentRunSlot = slot.toISOString();
44
- schedule.nextRunAt = nextScheduleRun(schedule, actionId === 'schedule.claimFromSlot' ? slot : now).toISOString();
45
- claimed = true;
46
- status = 'completed';
47
- }
48
- else
49
- throw new Error('Unknown local schedule action: ' + actionId);
50
- job.updatedAt = now.toISOString();
51
- await store.saveJob(job);
52
- return {};
53
- });
54
- if (!decision.accepted || !claimed)
55
- return { job, status };
56
- await engine.reconcile(job.id);
57
- await engine.idle();
58
- job = await store.load(job.id);
59
- if (!job.schedule)
60
- throw new Error('Scheduled job lost schedule metadata');
61
- job.schedule.lastRunAt = now.toISOString();
62
- // A claim and its completion can share the same wall-clock timestamp.
63
- // Advance the snapshot revision to satisfy SQLite's stale-write guard.
64
- job.updatedAt = new Date(Math.max(now.getTime(), Date.parse(job.updatedAt) + 1)).toISOString();
65
- await store.saveJob(job);
66
- return { job, status: 'completed' };
67
- }