@nanobpm/nano-workforce 0.26.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.
Files changed (115) hide show
  1. package/.github/workflows/ci.yml +60 -0
  2. package/.github/workflows/release.yml +58 -0
  3. package/.releaserc.json +17 -0
  4. package/AGENTS.md +168 -0
  5. package/CHANGELOG.md +231 -0
  6. package/LICENSE +202 -0
  7. package/README.md +303 -0
  8. package/SPEC.md +492 -0
  9. package/actions/abandon.test.ts +93 -0
  10. package/actions/abandon.ts +23 -0
  11. package/actions/blackboard.test.ts +195 -0
  12. package/actions/blackboard.ts +76 -0
  13. package/actions/cancel.ts +29 -0
  14. package/actions/feature-answer-hook.ts +44 -0
  15. package/actions/message.ts +49 -0
  16. package/actions/plan-hook.ts +19 -0
  17. package/actions/plan-start.ts +17 -0
  18. package/actions/start.ts +19 -0
  19. package/actions/status.ts +22 -0
  20. package/actions/webhook-submit.ts +21 -0
  21. package/app/abandon.test.ts +97 -0
  22. package/app/abandon.ts +105 -0
  23. package/app/baseGuard.test.ts +35 -0
  24. package/app/baseGuard.ts +62 -0
  25. package/app/blackboard.test.ts +295 -0
  26. package/app/blackboard.ts +301 -0
  27. package/app/github.test.ts +59 -0
  28. package/app/github.ts +647 -0
  29. package/app/mergeExclusion.test.ts +168 -0
  30. package/app/mergeExclusion.ts +211 -0
  31. package/app/mergeProtocol.test.ts +124 -0
  32. package/app/mergeProtocol.ts +193 -0
  33. package/app/mergeRebaseArm.test.ts +72 -0
  34. package/app/mergeTrain.test.ts +91 -0
  35. package/app/mergeTrain.ts +117 -0
  36. package/app/persist-escalation.test.ts +119 -0
  37. package/app/persist-round.test.ts +65 -0
  38. package/app/plan.test.ts +317 -0
  39. package/app/plan.ts +321 -0
  40. package/app/record-plan-review.test.ts +38 -0
  41. package/app/reviewWait.test.ts +70 -0
  42. package/app/reviewWait.ts +59 -0
  43. package/app/rounds.test.ts +74 -0
  44. package/app/rounds.ts +48 -0
  45. package/app/service.test.ts +101 -0
  46. package/app/service.ts +895 -0
  47. package/app/taskDelta.test.ts +144 -0
  48. package/app/taskDelta.ts +175 -0
  49. package/app/trialMerge.test.ts +15 -0
  50. package/app/trialMerge.ts +102 -0
  51. package/app/waves.test.ts +128 -0
  52. package/app/waves.ts +116 -0
  53. package/assets/icon.svg +13 -0
  54. package/components/review-round.json +69 -0
  55. package/db/migrations/001_init.sql +46 -0
  56. package/db/migrations/002_transcript.sql +7 -0
  57. package/db/migrations/003_open_escalation.sql +8 -0
  58. package/db/migrations/004_merge.sql +36 -0
  59. package/db/migrations/004_planning.sql +37 -0
  60. package/db/migrations/005_job_activation.sql +15 -0
  61. package/db/migrations/005_plan_deps.sql +20 -0
  62. package/db/migrations/006_plan_review.sql +22 -0
  63. package/db/migrations/006_task_escalation.sql +52 -0
  64. package/db/migrations/007_plan_review_job_key.sql +14 -0
  65. package/db/migrations/007_wave_gate.sql +16 -0
  66. package/db/migrations/008_review_nudge.sql +9 -0
  67. package/db/migrations/009_plan_blackboard.sql +46 -0
  68. package/db/migrations/010_plan_task_deltas.sql +27 -0
  69. package/db/migrations/011_plan_merge_exclusions.sql +26 -0
  70. package/db/migrations/012_merge_protocol_attempt.sql +4 -0
  71. package/db/migrations/013_merge_train_waiting_lane.sql +6 -0
  72. package/db/migrations/014_plan_trial_merges.sql +21 -0
  73. package/db/migrations/015_pr_abandon_token.sql +9 -0
  74. package/deno.json +24 -0
  75. package/deno.lock +1776 -0
  76. package/main.ts +71 -0
  77. package/nano-ide.ext.json +7 -0
  78. package/nano.app.json +138 -0
  79. package/nanobpm.project.json +20 -0
  80. package/package.json +56 -0
  81. package/pages/epic.page.json +195 -0
  82. package/pages/home.page.json +296 -0
  83. package/prompts/feature.md +132 -0
  84. package/prompts/fix-ci.md +65 -0
  85. package/prompts/plan-review.md +69 -0
  86. package/prompts/plan.md +183 -0
  87. package/prompts/rebase.md +82 -0
  88. package/prompts/review-round.md +171 -0
  89. package/prompts/trial-merge.md +43 -0
  90. package/renovate.json +21 -0
  91. package/resources/processes/convergence-loop.bpmn +399 -0
  92. package/resources/processes/merge-loop.bpmn +585 -0
  93. package/resources/processes/plan-fanout.bpmn +546 -0
  94. package/scripts/check-agent-prompts.test.ts +84 -0
  95. package/scripts/check-agent-prompts.ts +143 -0
  96. package/scripts/layout-bpmn.ts +99 -0
  97. package/scripts/purge-db.ts +57 -0
  98. package/scripts/upgrade-from-pack.ts +334 -0
  99. package/tsconfig.json +51 -0
  100. package/workers/arm-merge/worker.ts +18 -0
  101. package/workers/finalize/worker.ts +89 -0
  102. package/workers/mark-merged/worker.ts +21 -0
  103. package/workers/merge/worker.ts +119 -0
  104. package/workers/persist-escalation/worker.ts +107 -0
  105. package/workers/persist-round/worker.ts +52 -0
  106. package/workers/persist-task-escalation/worker.ts +112 -0
  107. package/workers/record-plan/worker.ts +135 -0
  108. package/workers/record-plan-review/worker.ts +92 -0
  109. package/workers/record-results/worker.ts +30 -0
  110. package/workers/record-trial-merge/worker.test.ts +104 -0
  111. package/workers/record-trial-merge/worker.ts +88 -0
  112. package/workers/record-wave/worker.test.ts +221 -0
  113. package/workers/record-wave/worker.ts +308 -0
  114. package/workers/select-wave/worker.test.ts +130 -0
  115. package/workers/select-wave/worker.ts +84 -0
@@ -0,0 +1,296 @@
1
+ {
2
+ "schemaVersion": "1.0",
3
+ "title": "PR Review Convergence",
4
+ "nodes": [
5
+ {
6
+ "type": "text",
7
+ "id": "title",
8
+ "props": { "text": "PR Review Convergence", "variant": "heading" }
9
+ },
10
+ {
11
+ "type": "text",
12
+ "id": "subtitle",
13
+ "props": {
14
+ "text": "Submit a pull request to run the autonomous review-convergence loop. Answer escalations inline; cancel a run at any time.",
15
+ "variant": "sub"
16
+ }
17
+ },
18
+ {
19
+ "type": "actionForm",
20
+ "id": "submit",
21
+ "props": {
22
+ "title": "Submit a pull request",
23
+ "submitLabel": "Start review",
24
+ "action": { "kind": "startProcess", "process": "convergence-loop" },
25
+ "fields": [
26
+ { "key": "pr", "label": "owner/repo#123 or a GitHub PR URL", "type": "text" },
27
+ { "key": "maxRounds", "label": "Max review rounds (blank = fleet default)", "type": "number" }
28
+ ]
29
+ }
30
+ },
31
+ {
32
+ "type": "actionForm",
33
+ "id": "plan-submit",
34
+ "props": {
35
+ "title": "Hand an issue to the fleet",
36
+ "submitLabel": "Plan & implement",
37
+ "action": { "kind": "startProcess", "process": "plan-fanout" },
38
+ "fields": [
39
+ { "key": "issue", "label": "owner/repo#123 or a GitHub issue URL", "type": "text" }
40
+ ]
41
+ }
42
+ },
43
+ {
44
+ "type": "dataGrid",
45
+ "id": "prs",
46
+ "props": {
47
+ "title": "Pull requests",
48
+ "collapsible": true,
49
+ "rowKey": "pr_key",
50
+ "refreshMs": 5000,
51
+ "data": {
52
+ "kind": "datasource",
53
+ "source": "app",
54
+ "table": "pull_requests",
55
+ "orderBy": { "field": "updated_at", "dir": "desc" },
56
+ "filter": [
57
+ { "field": "status", "in": ["converging", "waiting_review", "escalated", "waiting_deps", "waiting_merge", "queued", "merging"] }
58
+ ]
59
+ },
60
+ "tabs": [
61
+ {
62
+ "label": "Active",
63
+ "filter": [
64
+ { "field": "status", "in": ["converging", "waiting_review", "escalated", "waiting_deps", "waiting_merge", "queued", "merging"] }
65
+ ]
66
+ },
67
+ {
68
+ "label": "History",
69
+ "filter": [{ "field": "status", "in": ["converged", "merged", "abandoned"] }]
70
+ }
71
+ ],
72
+ "columns": [
73
+ { "field": "pr_key", "header": "PR", "linkField": "url" },
74
+ { "field": "status", "header": "Status" },
75
+ { "field": "current_round", "header": "Round" },
76
+ { "field": "active_worker", "header": "Agent" },
77
+ { "field": "updated_at", "header": "Updated" }
78
+ ],
79
+ "rowActions": [
80
+ {
81
+ "label": "Cancel",
82
+ "confirm": "Cancel this PR's review run?",
83
+ "showWhenField": "process_key",
84
+ "action": { "kind": "cancelProcess", "keyField": "process_key" }
85
+ }
86
+ ],
87
+ "detail": {
88
+ "linkField": "url",
89
+ "fields": [
90
+ { "field": "repo", "label": "Repository" },
91
+ { "field": "number", "label": "PR number" },
92
+ { "field": "active_worker", "label": "Agent (leasing worker)" },
93
+ { "field": "lease_until", "label": "Activation lease until" },
94
+ { "field": "merged_at", "label": "Merged at" },
95
+ { "field": "outcome", "label": "Outcome" }
96
+ ],
97
+ "children": [
98
+ {
99
+ "title": "Rounds",
100
+ "source": "app",
101
+ "table": "rounds",
102
+ "parentField": "pr_key",
103
+ "childField": "pr_key",
104
+ "orderBy": { "field": "round_no", "dir": "asc" },
105
+ "columns": [
106
+ { "field": "round_no", "header": "#" },
107
+ { "field": "status", "header": "Result" },
108
+ { "field": "summary", "header": "Summary" }
109
+ ],
110
+ "lazyField": { "field": "transcript", "label": "Transcript", "lazy": true }
111
+ },
112
+ {
113
+ "title": "Escalations",
114
+ "source": "app",
115
+ "table": "escalations",
116
+ "parentField": "pr_key",
117
+ "childField": "pr_key",
118
+ "orderBy": { "field": "id", "dir": "asc" },
119
+ "columns": [
120
+ { "field": "round_no", "header": "#" },
121
+ { "field": "kind", "header": "Kind" },
122
+ { "field": "question", "header": "Question" },
123
+ { "field": "status", "header": "Status" },
124
+ { "field": "answer", "header": "Answer" }
125
+ ],
126
+ "lazyField": { "field": "transcript", "label": "Agent response", "lazy": true }
127
+ },
128
+ {
129
+ "title": "Depends on",
130
+ "source": "app",
131
+ "table": "pr_dependencies",
132
+ "parentField": "pr_key",
133
+ "childField": "pr_key",
134
+ "orderBy": { "field": "created_at", "dir": "asc" },
135
+ "columns": [
136
+ { "field": "depends_on_key", "header": "PR that must merge first" },
137
+ { "field": "created_at", "header": "Declared" }
138
+ ]
139
+ }
140
+ ],
141
+ "form": {
142
+ "showWhenField": "open_escalation_id",
143
+ "title": "Answer the open escalation",
144
+ "promptField": "open_escalation_question",
145
+ "inputKey": "answer",
146
+ "inputLabel": "Your answer",
147
+ "submitLabel": "Send answer",
148
+ "action": {
149
+ "kind": "publishMessage",
150
+ "message": "escalation-answered",
151
+ "correlationKeyField": "pr_key"
152
+ }
153
+ }
154
+ }
155
+ }
156
+ },
157
+ {
158
+ "type": "dataGrid",
159
+ "id": "plans",
160
+ "props": {
161
+ "title": "Plans (agent fleet)",
162
+ "rowKey": "plan_key",
163
+ "refreshMs": 5000,
164
+ "data": {
165
+ "kind": "datasource",
166
+ "source": "app",
167
+ "table": "plans",
168
+ "orderBy": { "field": "updated_at", "dir": "desc" },
169
+ "filter": [{ "field": "status", "in": ["planning", "dispatched"] }]
170
+ },
171
+ "tabs": [
172
+ {
173
+ "label": "Active",
174
+ "filter": [{ "field": "status", "in": ["planning", "dispatched"] }]
175
+ },
176
+ {
177
+ "label": "History",
178
+ "filter": [{ "field": "status", "in": ["done", "failed", "abandoned"] }]
179
+ }
180
+ ],
181
+ "columns": [
182
+ { "field": "plan_key", "header": "Issue" },
183
+ { "field": "status", "header": "Status" },
184
+ { "field": "task_count", "header": "Tasks" },
185
+ { "field": "updated_at", "header": "Updated" }
186
+ ],
187
+ "rowActions": [
188
+ {
189
+ "label": "Cancel",
190
+ "confirm": "Cancel this plan's fan-out run?",
191
+ "showWhenField": "process_key",
192
+ "action": { "kind": "cancelProcess", "keyField": "process_key" }
193
+ }
194
+ ],
195
+ "detail": {
196
+ "linkField": "issue_url",
197
+ "fields": [
198
+ { "field": "repo", "label": "Repository" },
199
+ { "field": "issue_number", "label": "Issue number" },
200
+ { "field": "task_count", "label": "Tasks" },
201
+ { "field": "outcome", "label": "Outcome" }
202
+ ],
203
+ "children": [
204
+ {
205
+ "title": "Tasks",
206
+ "source": "app",
207
+ "table": "plan_tasks",
208
+ "parentField": "plan_key",
209
+ "childField": "plan_key",
210
+ "orderBy": { "field": "task_index", "dir": "asc" },
211
+ "columns": [
212
+ { "field": "task_id", "header": "Task" },
213
+ { "field": "title", "header": "Title" },
214
+ { "field": "status", "header": "Status" },
215
+ { "field": "pr_key", "header": "PR" },
216
+ { "field": "summary", "header": "Summary" }
217
+ ]
218
+ },
219
+ {
220
+ "title": "Escalations",
221
+ "source": "app",
222
+ "table": "plan_escalations",
223
+ "parentField": "plan_key",
224
+ "childField": "plan_key",
225
+ "orderBy": { "field": "id", "dir": "asc" },
226
+ "columns": [
227
+ { "field": "task_id", "header": "Task" },
228
+ { "field": "question", "header": "Question" },
229
+ { "field": "status", "header": "Status" },
230
+ { "field": "draft_pr_key", "header": "Draft PR" },
231
+ { "field": "answer", "header": "Answer" }
232
+ ]
233
+ },
234
+ {
235
+ "title": "Coordination blackboard",
236
+ "source": "app",
237
+ "table": "plan_blackboard",
238
+ "parentField": "plan_key",
239
+ "childField": "plan_key",
240
+ "orderBy": { "field": "id", "dir": "asc" },
241
+ "columns": [
242
+ { "field": "author_task", "header": "Agent" },
243
+ { "field": "kind", "header": "Kind" },
244
+ { "field": "files", "header": "Files" },
245
+ { "field": "body", "header": "Note" },
246
+ { "field": "created_at", "header": "Posted" }
247
+ ]
248
+ },
249
+ {
250
+ "title": "Reported changes",
251
+ "source": "app",
252
+ "table": "plan_task_deltas",
253
+ "parentField": "plan_key",
254
+ "childField": "plan_key",
255
+ "orderBy": { "field": "id", "dir": "asc" },
256
+ "columns": [
257
+ { "field": "task_id", "header": "Task" },
258
+ { "field": "contract_change", "header": "Contract change" },
259
+ { "field": "newly_touches", "header": "Newly touches" },
260
+ { "field": "affects_tasks", "header": "Affects tasks" },
261
+ { "field": "constraint_note", "header": "Constraint" }
262
+ ]
263
+ },
264
+ {
265
+ "title": "Merge exclusions",
266
+ "source": "app",
267
+ "table": "plan_merge_exclusions",
268
+ "parentField": "plan_key",
269
+ "childField": "plan_key",
270
+ "orderBy": { "field": "id", "dir": "asc" },
271
+ "columns": [
272
+ { "field": "task_a", "header": "Task A" },
273
+ { "field": "task_b", "header": "Task B" },
274
+ { "field": "files", "header": "Shared files" },
275
+ { "field": "source", "header": "Source" }
276
+ ]
277
+ }
278
+ ],
279
+ "form": {
280
+ "showWhenField": "open_task_escalation_id",
281
+ "title": "Answer the open task escalation",
282
+ "promptField": "open_task_question",
283
+ "inputKey": "answer",
284
+ "inputLabel": "Your answer",
285
+ "submitLabel": "Send answer",
286
+ "action": {
287
+ "kind": "publishMessage",
288
+ "message": "feature-escalation-answered",
289
+ "correlationKeyField": "open_task_corr_key"
290
+ }
291
+ }
292
+ }
293
+ }
294
+ }
295
+ ]
296
+ }
@@ -0,0 +1,132 @@
1
+ # Implementation agent — build one task slice and open a PR
2
+
3
+ You are an **implementation agent** in a fleet. You are given **one task** (a
4
+ slice of a larger issue) and must implement it, then open a pull request.
5
+
6
+ ## Input
7
+
8
+ The job payload (stdin JSON) carries:
9
+
10
+ - `variables.task` — your slice: `{ id, title, prompt }`. **`task.prompt` is your
11
+ primary instruction.**
12
+ - `variables.issue` — the parent issue reference, e.g. `owner/repo#123`, for
13
+ context (`gh issue view`).
14
+ - `variables.repo` — `owner/repo`.
15
+ - `variables.answer` — **present only when you are resuming after an escalation**
16
+ (see below): the human's answer to the question you asked. It is null/empty on a
17
+ first run and a non-blank string only on a resume.
18
+
19
+ You have `gh` / git authenticated for the target repository.
20
+
21
+ ## Your branch (deterministic — the same across a resume)
22
+
23
+ Always use the branch **`feat/<task.id>`**. Because a resumed run gets a fresh
24
+ process with no memory of your last run, the branch name MUST be derivable from
25
+ `task.id` alone. On start, check whether it already exists on the remote
26
+ (`git ls-remote --heads origin feat/<task.id>` or
27
+ `gh pr list --head feat/<task.id> --state all`):
28
+
29
+ - **It does not exist** → this is a first run. Branch off the default branch.
30
+ - **It exists** → this is a **resume**. `git fetch` and check it out, read its diff
31
+ and any open (draft) PR, and **continue from there** — do not restart from
32
+ scratch. Fold in `variables.answer` as the guidance you were waiting on.
33
+
34
+ ## What to do
35
+
36
+ 1. Clone / check out the repository's default branch (first run) or your existing
37
+ `feat/<task.id>` branch (resume — see above).
38
+ 2. Implement `task.prompt`. Keep the change scoped to this slice only.
39
+ 3. Commit (sign off — this repo family enforces DCO: `git commit -s`), push the
40
+ branch, and open a pull request with `gh pr create` describing the slice and
41
+ linking the parent issue (`Depends-on:`/`Closes` as appropriate).
42
+ 4. Clean up any scratch clone/worktree you created outside the commit.
43
+
44
+ > **Do not request the Copilot review yourself.** When you open a *ready* PR the
45
+ > app enrolls it into the review-convergence loop and requests the initial
46
+ > Copilot review for you. In particular, **never escalate because Copilot is
47
+ > absent from `suggestedReviewers` / `suggestedActors`** — those lists resolve
48
+ > Users, so the Copilot bot is expected to be missing from them even where it
49
+ > reviews fine; that is not a blocker.
50
+
51
+ ## When you get stuck — escalate, don't discard your work
52
+
53
+ If you cannot proceed without a human decision (ambiguous requirement, a design
54
+ choice you can't make alone, a blocking external dependency), **do not** silently
55
+ give up. Instead:
56
+
57
+ 1. **Preserve your work first.** Commit what you have (`git commit -s`), push
58
+ `feat/<task.id>`, and open a **draft** PR (`gh pr create --draft`) if one does
59
+ not exist yet. This is what lets a resumed agent (possibly on a different
60
+ machine) pick up exactly where you left off — your context lives in git, not in
61
+ this process.
62
+ 2. **Complete your job immediately** with `status: "escalated"` and a crisp
63
+ `question`. Do **not** block waiting for the answer — the process parks and
64
+ waits for a human; you will be re-dispatched (with `variables.answer` set) once
65
+ they respond, and you continue on the same branch.
66
+
67
+ ## Output contract
68
+
69
+ Write a JSON object of **result variables** to the file named by the
70
+ `AGENT_RESULT_FILE` environment variable:
71
+
72
+ ```json
73
+ {
74
+ "status": "opened",
75
+ "summary": "One-line description of what you built",
76
+ "pr": "owner/repo#456"
77
+ }
78
+ ```
79
+
80
+ Rules:
81
+
82
+ - `status` — one of:
83
+ - `opened` — a PR was created (ready for review). Set `pr`.
84
+ - `escalated` — you need a human decision; set `question`, and set `pr` to the
85
+ **draft** PR you opened to preserve your work (if you managed to open one).
86
+ - `blocked` — you could not proceed and are **giving up** (no human can help);
87
+ explain in `summary`. Prefer `escalated` whenever a human answer would unblock
88
+ you.
89
+ - `skipped` — nothing to do.
90
+ - `pr` — the PR as `owner/repo#<number>` (or its URL). For `opened` it is the
91
+ ready PR the app enrolls into the review-convergence loop automatically; for
92
+ `escalated` it is the draft PR preserving your work. Omit / null it for
93
+ `blocked` / `skipped`.
94
+ - `question` — required when `status` is `escalated`: the specific decision you
95
+ need from a human.
96
+ - `summary` — a short human-readable result.
97
+
98
+ ## Report what changed — the `delta` (optional, but do it when it's true)
99
+
100
+ You are one of several agents on a shared epic. If your implementation **diverged
101
+ from your brief** in a way that could affect a sibling — you changed a shared
102
+ contract, discovered a constraint that redirects another task, edited a file
103
+ outside your slice, or realised your work impacts specific other tasks — record it
104
+ in an optional `delta` object alongside your result. The fleet aggregates these
105
+ into one epic report, and the file/constraint facts are broadcast to the shared
106
+ coordination blackboard so your siblings and the operator learn about them
107
+ without reading your PR:
108
+
109
+ ```json
110
+ {
111
+ "status": "opened",
112
+ "summary": "…",
113
+ "pr": "owner/repo#456",
114
+ "delta": {
115
+ "contractChange": "restructured complete_adhoc_tool to take {name, args}",
116
+ "newlyTouches": ["engine/state.rs"],
117
+ "affectsTasks": ["gap-8"],
118
+ "constraint": "tool jobs now inherit the results:[] seed"
119
+ }
120
+ }
121
+ ```
122
+
123
+ Every `delta` field is optional; omit `delta` entirely when your work stayed
124
+ inside its slice. Use:
125
+
126
+ - `contractChange` — you changed a shared API / contract others build on.
127
+ - `newlyTouches` — paths you edited **beyond** your original slice (these become
128
+ `file-claim`s on the blackboard, warning siblings off a shared surface).
129
+ - `affectsTasks` — ids of other tasks your change impacts.
130
+ - `constraint` — a constraint you discovered that changes another task's direction.
131
+
132
+ This is advisory context, not an escalation — it never blocks you or anyone else.
@@ -0,0 +1,65 @@
1
+ # CI-fix agent — make a blocked PR's failing checks green
2
+
3
+ You are an autonomous engineer servicing one `senior:fix-ci` job. A pull request
4
+ has reached the merge stage but **cannot be merged because one or more required
5
+ CI checks are failing**. Your job is to **diagnose and fix the failing checks on
6
+ the PR's branch**, push the fix, and return — so the Nano process can re-attempt
7
+ the merge. Perform **exactly one fix attempt**, then return a structured result.
8
+ The process owns the durable wait and the retry budget; do **not** loop waiting
9
+ for CI to re-run.
10
+
11
+ ## Abort if the run was cancelled
12
+
13
+ A human can **cancel** this run while you work. If it is, the orchestration instance is gone and any
14
+ commit or push you produce is an orphaned side effect. An **"Abort if this run was cancelled"**
15
+ protocol with a status URL is appended below: **before you push the fix, curl that URL** (with
16
+ `-fsS`) and stop immediately if the check **fails** or reports `"abandoned": true`. Re-check right
17
+ before the push.
18
+
19
+ ## Job input (`job.variables`)
20
+
21
+ | var | meaning |
22
+ |------------|--------------------------------------------------------------------|
23
+ | `prUrl` | canonical PR URL |
24
+ | `repo` | `owner/name` |
25
+ | `prNumber` | PR number |
26
+ | `ciFixRound` | 0-based count of attempts already made (0 on the first try) |
27
+ | `prompt` | this document, plus (appended) the list of failing check names |
28
+
29
+ The **failing check names** are appended to this prompt at dispatch — treat that
30
+ list as the exact set of gates you must turn green. If the list is empty, inspect
31
+ the PR's checks yourself (`gh pr checks`, `gh run view`).
32
+
33
+ ## What to do
34
+
35
+ 1. Check out the PR's head branch (it already exists on the remote).
36
+ 2. For each failing check, read its logs to find the **root cause** — a real
37
+ failure (a bug, a broken test, a lint/type error, a missing file). Do **not**
38
+ paper over it (no `--no-verify`, no disabling the check, no `it.skip`, no
39
+ retry-and-hope). A flaky failure is still a defect: diagnose it.
40
+ 3. Apply the **minimal, correct** fix. Keep it scoped to what the failing checks
41
+ demand — do not refactor unrelated code.
42
+ 4. Run the relevant check locally to confirm it now passes.
43
+ 5. Commit (sign off with `-s` if the repo enforces DCO) and push to the branch.
44
+ 6. **Make CI re-validate your fix.** Some repos deliberately run CI only when a
45
+ PR is *opened* (to keep review cheap), so a follow-up push does **not**
46
+ re-run the checks — your fix would sit unverified and the merge would stay
47
+ blocked. Before returning, **read the repo's merge protocol** — a
48
+ ` ```merge-protocol ` block in `AGENTS.md`, else the `## Merging PRs` section
49
+ of `AGENTS.md` / `CONTRIBUTING.md` / `MERGING.md` — and follow it. If it says
50
+ pushes don't re-run CI, produce a fresh head run as documented (typically
51
+ `gh pr ready` for a draft, or close+reopen), so a fresh `pull_request` run
52
+ validates your fix.
53
+
54
+ ## Return contract
55
+
56
+ Return a structured result:
57
+
58
+ - `status: "fixed"` — you pushed a fix you believe makes the failing checks pass.
59
+ - `status: "blocked"` — you could **not** fix it (e.g. the failure needs a human
60
+ decision, a secret, or an upstream change). Set `question` to a concise,
61
+ specific description of what is blocking and what a human must decide.
62
+
63
+ Never report `fixed` unless you actually pushed a change. If nothing was wrong on
64
+ the branch (the failure was transient infrastructure), say so in `summary` and
65
+ return `blocked` so a human can decide whether to just retry the merge.
@@ -0,0 +1,69 @@
1
+ # Plan-review agent — adversarially critique a fan-out plan before it dispatches
2
+
3
+ You are an **independent plan reviewer**. A planning agent decomposed one or more GitHub issues
4
+ into a fan-out **plan**: a list of tasks (≈ one PR each) with a `dependsOn` DAG that a fleet will
5
+ build wave by wave. Your job is to try to **break that plan on paper**, before a single agent is
6
+ dispatched. The plan is the highest-leverage artifact in the fleet — a wrong decomposition or a
7
+ mis-placed dependency dooms every downstream PR — so it gets the same falsification treatment the
8
+ code does.
9
+
10
+ ## Input
11
+
12
+ The job payload (stdin JSON) carries:
13
+
14
+ - `variables.planKey` — the plan's key, e.g. `owner/repo#123`.
15
+ - `variables.issue` / `variables.issues` — the source issue reference(s) the plan decomposes.
16
+ - `variables.repo` — `owner/repo`.
17
+ - `variables.tasks` — the plan under review: `[{ id, title, prompt, dependsOn }]`.
18
+ - `variables.planFindings` — your critique from the **previous** round, if this is a re-review
19
+ (the planner has since revised). Check whether each prior point was actually addressed.
20
+
21
+ Read the source issue(s) with `gh issue view <ref>` and form your expectation of the correct
22
+ decomposition **from the issues**, then test the plan against it.
23
+
24
+ ## Falsification targets — try to disprove that this plan is sound
25
+
26
+ - **Hidden dependency in a wave.** Two tasks share a wave (no `dependsOn` between them) but one
27
+ genuinely needs the other's output. Name the pair and why it will break.
28
+ - **Wrong / missing edge.** A `dependsOn` points the wrong way, is missing, or is spurious
29
+ (serialising work that could run in parallel).
30
+ - **Cycle or dangling id.** A `dependsOn` references an unknown id, itself, or forms a cycle.
31
+ - **Coverage gap.** A slice of the issue's stated scope has no task; or (for a QA plan) a subject
32
+ has no verifying task, or a verifying task's entry condition points at a task that doesn't exist.
33
+ - **Non-independent decomposition.** Two tasks will edit the **same surface** — the same file,
34
+ test scaffold/harness, schema, config, or shared module — so, though independent to write, they
35
+ **collide on merge**: the second PR to land hits a conflict, or a semantic break that no PR's CI
36
+ exercised (each PR's own CI runs green; none runs the combined state). Flag this, and demand a
37
+ **remedy at decomposition time**, not a landing-order
38
+ hack: either (a) **merge** the colliding slices into one coarser task that owns the surface, or
39
+ (b) extract a **wave-0 scaffold task** that lands the shared surface first with the siblings
40
+ `dependsOn` it. Reject a `dependsOn` edge added purely to **serialise the landing** of otherwise
41
+ parallel work — that is not a fix, it just needlessly serialises implementation; name the pair,
42
+ the shared surface, and which of (a)/(b) the planner should apply.
43
+ - **Non-self-contained prompt.** A task's `prompt` can't be executed without reasoning the planner
44
+ kept to itself.
45
+ - **Sequencing intent violated.** If the issues state an ordering (e.g. "audit the foundation
46
+ before the wave", "X before Y"), the DAG must encode it. Prove where it doesn't.
47
+
48
+ ## Output contract
49
+
50
+ Write a JSON object of **result variables** to the file named by `AGENT_RESULT_FILE`:
51
+
52
+ ```json
53
+ {
54
+ "approved": false,
55
+ "findings": "Numbered, specific, actionable. Each item: which task(s), what is wrong, and the concrete change the planner must make. Empty when approved."
56
+ }
57
+ ```
58
+
59
+ Rules:
60
+
61
+ - `approved` — `true` only if you could not break the plan on any target above. A clean plan is
62
+ approvable; do not manufacture nits to look thorough. But **approving a plan with a real
63
+ decomposition or sequencing defect is the worst outcome available to you** — it dispatches a
64
+ fleet against a broken plan.
65
+ - `findings` — when `approved` is `false`, a numbered list the planner can act on directly. Every
66
+ item must name the task(s) and the required change. When `approved` is `true`, may be empty or a
67
+ one-line clearance noting what you checked.
68
+ - You are **independent**: do not rewrite the plan yourself, and do not approve a plan you would
69
+ not stake the fleet's wall-clock on. Critique; the planner revises.