@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.
- package/.github/workflows/ci.yml +60 -0
- package/.github/workflows/release.yml +58 -0
- package/.releaserc.json +17 -0
- package/AGENTS.md +168 -0
- package/CHANGELOG.md +231 -0
- package/LICENSE +202 -0
- package/README.md +303 -0
- package/SPEC.md +492 -0
- package/actions/abandon.test.ts +93 -0
- package/actions/abandon.ts +23 -0
- package/actions/blackboard.test.ts +195 -0
- package/actions/blackboard.ts +76 -0
- package/actions/cancel.ts +29 -0
- package/actions/feature-answer-hook.ts +44 -0
- package/actions/message.ts +49 -0
- package/actions/plan-hook.ts +19 -0
- package/actions/plan-start.ts +17 -0
- package/actions/start.ts +19 -0
- package/actions/status.ts +22 -0
- package/actions/webhook-submit.ts +21 -0
- package/app/abandon.test.ts +97 -0
- package/app/abandon.ts +105 -0
- package/app/baseGuard.test.ts +35 -0
- package/app/baseGuard.ts +62 -0
- package/app/blackboard.test.ts +295 -0
- package/app/blackboard.ts +301 -0
- package/app/github.test.ts +59 -0
- package/app/github.ts +647 -0
- package/app/mergeExclusion.test.ts +168 -0
- package/app/mergeExclusion.ts +211 -0
- package/app/mergeProtocol.test.ts +124 -0
- package/app/mergeProtocol.ts +193 -0
- package/app/mergeRebaseArm.test.ts +72 -0
- package/app/mergeTrain.test.ts +91 -0
- package/app/mergeTrain.ts +117 -0
- package/app/persist-escalation.test.ts +119 -0
- package/app/persist-round.test.ts +65 -0
- package/app/plan.test.ts +317 -0
- package/app/plan.ts +321 -0
- package/app/record-plan-review.test.ts +38 -0
- package/app/reviewWait.test.ts +70 -0
- package/app/reviewWait.ts +59 -0
- package/app/rounds.test.ts +74 -0
- package/app/rounds.ts +48 -0
- package/app/service.test.ts +101 -0
- package/app/service.ts +895 -0
- package/app/taskDelta.test.ts +144 -0
- package/app/taskDelta.ts +175 -0
- package/app/trialMerge.test.ts +15 -0
- package/app/trialMerge.ts +102 -0
- package/app/waves.test.ts +128 -0
- package/app/waves.ts +116 -0
- package/assets/icon.svg +13 -0
- package/components/review-round.json +69 -0
- package/db/migrations/001_init.sql +46 -0
- package/db/migrations/002_transcript.sql +7 -0
- package/db/migrations/003_open_escalation.sql +8 -0
- package/db/migrations/004_merge.sql +36 -0
- package/db/migrations/004_planning.sql +37 -0
- package/db/migrations/005_job_activation.sql +15 -0
- package/db/migrations/005_plan_deps.sql +20 -0
- package/db/migrations/006_plan_review.sql +22 -0
- package/db/migrations/006_task_escalation.sql +52 -0
- package/db/migrations/007_plan_review_job_key.sql +14 -0
- package/db/migrations/007_wave_gate.sql +16 -0
- package/db/migrations/008_review_nudge.sql +9 -0
- package/db/migrations/009_plan_blackboard.sql +46 -0
- package/db/migrations/010_plan_task_deltas.sql +27 -0
- package/db/migrations/011_plan_merge_exclusions.sql +26 -0
- package/db/migrations/012_merge_protocol_attempt.sql +4 -0
- package/db/migrations/013_merge_train_waiting_lane.sql +6 -0
- package/db/migrations/014_plan_trial_merges.sql +21 -0
- package/db/migrations/015_pr_abandon_token.sql +9 -0
- package/deno.json +24 -0
- package/deno.lock +1776 -0
- package/main.ts +71 -0
- package/nano-ide.ext.json +7 -0
- package/nano.app.json +138 -0
- package/nanobpm.project.json +20 -0
- package/package.json +56 -0
- package/pages/epic.page.json +195 -0
- package/pages/home.page.json +296 -0
- package/prompts/feature.md +132 -0
- package/prompts/fix-ci.md +65 -0
- package/prompts/plan-review.md +69 -0
- package/prompts/plan.md +183 -0
- package/prompts/rebase.md +82 -0
- package/prompts/review-round.md +171 -0
- package/prompts/trial-merge.md +43 -0
- package/renovate.json +21 -0
- package/resources/processes/convergence-loop.bpmn +399 -0
- package/resources/processes/merge-loop.bpmn +585 -0
- package/resources/processes/plan-fanout.bpmn +546 -0
- package/scripts/check-agent-prompts.test.ts +84 -0
- package/scripts/check-agent-prompts.ts +143 -0
- package/scripts/layout-bpmn.ts +99 -0
- package/scripts/purge-db.ts +57 -0
- package/scripts/upgrade-from-pack.ts +334 -0
- package/tsconfig.json +51 -0
- package/workers/arm-merge/worker.ts +18 -0
- package/workers/finalize/worker.ts +89 -0
- package/workers/mark-merged/worker.ts +21 -0
- package/workers/merge/worker.ts +119 -0
- package/workers/persist-escalation/worker.ts +107 -0
- package/workers/persist-round/worker.ts +52 -0
- package/workers/persist-task-escalation/worker.ts +112 -0
- package/workers/record-plan/worker.ts +135 -0
- package/workers/record-plan-review/worker.ts +92 -0
- package/workers/record-results/worker.ts +30 -0
- package/workers/record-trial-merge/worker.test.ts +104 -0
- package/workers/record-trial-merge/worker.ts +88 -0
- package/workers/record-wave/worker.test.ts +221 -0
- package/workers/record-wave/worker.ts +308 -0
- package/workers/select-wave/worker.test.ts +130 -0
- package/workers/select-wave/worker.ts +84 -0
package/prompts/plan.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Planning agent — decompose an issue into implementation tasks
|
|
2
|
+
|
|
3
|
+
You are a **planning agent**. You are given a GitHub issue and must turn it into a
|
|
4
|
+
set of **implementation tasks** that a fleet of coding agents can work on. One
|
|
5
|
+
task ≈ one pull request. Tasks run in dependency **waves**: every task with no
|
|
6
|
+
unmet dependency runs **in parallel**, and a task that declares `dependsOn` runs
|
|
7
|
+
**after** the tasks it names have **merged** (the fleet holds a dependent wave until
|
|
8
|
+
every PR the prior wave opened has landed on the base branch, so a task builds on
|
|
9
|
+
its prerequisites' merged code, not an in-flight branch). Leave truly independent
|
|
10
|
+
tasks without dependencies so they run concurrently.
|
|
11
|
+
|
|
12
|
+
## Input
|
|
13
|
+
|
|
14
|
+
The job payload (stdin JSON) carries:
|
|
15
|
+
|
|
16
|
+
- `variables.issue` — the issue reference, e.g. `owner/repo#123`.
|
|
17
|
+
- `variables.issueUrl` — the canonical issue URL.
|
|
18
|
+
- `variables.repo` — `owner/repo`.
|
|
19
|
+
|
|
20
|
+
Read the issue with `gh issue view <issue>` (title, body, comments). You have
|
|
21
|
+
`gh` authenticated for the target repository.
|
|
22
|
+
|
|
23
|
+
## Revising after a rejected review
|
|
24
|
+
|
|
25
|
+
Your plan is adversarially reviewed before any agent is dispatched. If
|
|
26
|
+
`variables.planFindings` is present, a reviewer **rejected your previous plan**:
|
|
27
|
+
the findings are a numbered list of concrete defects (hidden dependencies, wrong
|
|
28
|
+
or missing `dependsOn` edges, coverage gaps, non-self-contained prompts, violated
|
|
29
|
+
sequencing intent). Address **every** point, then re-emit the **full** plan (all
|
|
30
|
+
tasks, not just the changed ones) in the same output contract below. Do not argue
|
|
31
|
+
with the findings in the plan; fix them.
|
|
32
|
+
|
|
33
|
+
## Step 0 — is this epic already decomposed? (do this first)
|
|
34
|
+
|
|
35
|
+
Before decomposing anything yourself, check whether the issue is an **epic that
|
|
36
|
+
has already been split into sub-issues**. If it has, **do not invent a new
|
|
37
|
+
breakdown** — adopt the existing one, one task per sub-issue. This keeps the
|
|
38
|
+
fan-out faithful to the human's plan and links each PR back to its sub-issue.
|
|
39
|
+
|
|
40
|
+
Detect existing children two ways (try both; union the results, de-duplicated):
|
|
41
|
+
|
|
42
|
+
1. **Native GitHub sub-issues:**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
gh api --paginate "repos/<owner>/<repo>/issues/<number>/sub_issues" \
|
|
46
|
+
--jq '.[] | {number, title, state}'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Substitute `<owner>/<repo>` with `variables.repo` and `<number>` with the
|
|
50
|
+
epic's own issue number (the `#123` in `variables.issue`) so you query the
|
|
51
|
+
epic in the correct repository.
|
|
52
|
+
|
|
53
|
+
(`--paginate` matters: epics with more than one page of children — 30+
|
|
54
|
+
sub-issues — are otherwise only partially adopted, silently dropping tasks.)
|
|
55
|
+
|
|
56
|
+
(Ignore an error / empty list — the repo or issue may not use native
|
|
57
|
+
sub-issues.)
|
|
58
|
+
|
|
59
|
+
2. **Task-list references in the body:** parse the issue body for checklist items
|
|
60
|
+
that reference other issues in **this same repo**, e.g. lines like
|
|
61
|
+
`- [ ] #2 — …`. Each `#N` is a candidate sub-issue. Only same-repo children are
|
|
62
|
+
adopted here — ignore fully-qualified `owner/repo#N` references that point at a
|
|
63
|
+
*different* repository.
|
|
64
|
+
|
|
65
|
+
For every distinct child issue number `N` you find:
|
|
66
|
+
|
|
67
|
+
- Read it with `gh issue view <owner>/<repo>#N` to get its title, body, and
|
|
68
|
+
current **state** (substitute `<owner>/<repo>` with `variables.repo` so `#N` is
|
|
69
|
+
resolved in this same repository, not a different one; task-list `#N` references
|
|
70
|
+
carry no state until you fetch them; the native `sub_issues` query above already
|
|
71
|
+
returns `state`).
|
|
72
|
+
- Skip it if it is already **closed** (that slice is done).
|
|
73
|
+
- Otherwise, emit **one task** for it (see the output contract), with:
|
|
74
|
+
- `id` = `issue-N`,
|
|
75
|
+
- `title` = the sub-issue's title,
|
|
76
|
+
- `prompt` = a self-contained brief built from the sub-issue's body, and end
|
|
77
|
+
the prompt with an explicit instruction to the implementing agent to open its
|
|
78
|
+
PR against this specific sub-issue and include `Closes #N` in the PR body so
|
|
79
|
+
the sub-issue is linked and auto-closed on merge.
|
|
80
|
+
- `dependsOn` = **honour any inter-sub-issue ordering the human declared.** Scan
|
|
81
|
+
the sub-issue's body for an explicit dependency directive — a line such as
|
|
82
|
+
`Depends-on: #7`, `Depends on #7`, or `Blocked by #7` (case-insensitive; there
|
|
83
|
+
may be several `#N` on one line or several such lines). For every prerequisite
|
|
84
|
+
`#M` you find that is itself an adopted (open) sibling sub-issue, add `issue-M`
|
|
85
|
+
to this task's `dependsOn`. This is the one exception to "adopt faithfully":
|
|
86
|
+
the human's stated blocking order (e.g. a scaffold task that must merge before
|
|
87
|
+
the rest) MUST be preserved, or the dependent tasks would be built off an
|
|
88
|
+
unscaffolded base. Ignore a `#M` that is not among the adopted sub-issues (it
|
|
89
|
+
may be an external/closed issue) and never point a task at itself.
|
|
90
|
+
|
|
91
|
+
Once you have checked every child, decide based on what you found — these three
|
|
92
|
+
cases are exhaustive, so do **not** fall through to Step 1 unless the third applies:
|
|
93
|
+
|
|
94
|
+
- **One or more open sub-issues:** emit **exactly** those tasks and stop —
|
|
95
|
+
**do not add, merge, or re-split them**.
|
|
96
|
+
- **Sub-issues exist but every one is closed:** the epic is already fully
|
|
97
|
+
delivered, so emit `{ "tasks": [] }` (with a `note` saying all sub-issues are
|
|
98
|
+
closed) and stop. Do **not** fall through to Step 1 and re-decompose it.
|
|
99
|
+
- **No sub-issues at all:** fall through to Step 1 and decompose the issue
|
|
100
|
+
yourself.
|
|
101
|
+
|
|
102
|
+
## Step 1 — decompose (only when there are no sub-issues)
|
|
103
|
+
|
|
104
|
+
If the issue is a plain, undecomposed issue, break it into a set of tasks. Each
|
|
105
|
+
task is a self-contained slice of work that:
|
|
106
|
+
|
|
107
|
+
- can be implemented and reviewed on its own branch / PR,
|
|
108
|
+
- has a clear, actionable prompt for the implementing agent,
|
|
109
|
+
- declares, via `dependsOn`, any earlier tasks whose result it needs (e.g. it
|
|
110
|
+
builds on an API a prior task introduces). Leave `dependsOn` empty (or omit it)
|
|
111
|
+
for independent tasks so they run in parallel in the same wave.
|
|
112
|
+
|
|
113
|
+
Prefer parallelism: only add a dependency when a task genuinely can't start until
|
|
114
|
+
another finishes. Keep the dependency graph a **DAG** — no cycles, and every
|
|
115
|
+
`dependsOn` id must be the `id` of another task in this same plan. (A malformed
|
|
116
|
+
graph is rejected and the whole plan falls back to running every task in parallel,
|
|
117
|
+
losing your ordering.) Prefer a small number of coarse, coherent tasks over many
|
|
118
|
+
tiny ones. If the issue is genuinely a single unit of work, emit exactly one task.
|
|
119
|
+
|
|
120
|
+
### Shared surface → a decomposition choice, not a merge problem
|
|
121
|
+
|
|
122
|
+
Before you finalise the split, look for tasks that would **edit the same surface**
|
|
123
|
+
— the same file, the same test scaffold/harness, the same schema or config, the
|
|
124
|
+
same shared module. Such tasks are independent to *write* but collide on *merge*:
|
|
125
|
+
each opens a green PR, but the second to land hits a conflict (or, worse, a
|
|
126
|
+
**semantic** break that no PR's CI exercised — each task's own CI runs, but
|
|
127
|
+
none runs the *combined* state). Do **not** paper over this with a
|
|
128
|
+
`dependsOn` edge whose only purpose is to serialise the landing — that needlessly
|
|
129
|
+
serialises *implementation* that could have run in parallel, and is the opposite
|
|
130
|
+
waste from the collision.
|
|
131
|
+
|
|
132
|
+
Instead, resolve a shared surface at **decomposition** time, in one of two ways:
|
|
133
|
+
|
|
134
|
+
1. **Merge into one coarser task.** If two-or-more slices are really slices of *one
|
|
135
|
+
file's behaviour* (e.g. many cases appended to the same test scaffold), emit a
|
|
136
|
+
**single** coarser task that owns that surface end-to-end. One PR, no collision.
|
|
137
|
+
2. **Scaffold-first wave-0 task.** If the shared surface is a common
|
|
138
|
+
harness/boilerplate the slices genuinely branch off, emit an explicit **wave-0
|
|
139
|
+
scaffold task** that lands that shared harness first, and make every sibling
|
|
140
|
+
that builds on it `dependsOn` the scaffold task. The siblings then branch off
|
|
141
|
+
*merged* scaffold and no longer collide on it. (This is the one case where a
|
|
142
|
+
`dependsOn` edge is right: the dependency is real — the siblings need the
|
|
143
|
+
scaffold's merged code — not a landing-order hack.)
|
|
144
|
+
|
|
145
|
+
Choose (1) when the surface *is* the task; choose (2) when the surface is shared
|
|
146
|
+
infrastructure several distinct tasks sit on top of. Reserve plain parallel tasks
|
|
147
|
+
(no shared surface) for genuinely disjoint work.
|
|
148
|
+
|
|
149
|
+
## Output contract
|
|
150
|
+
|
|
151
|
+
Write a JSON object of **result variables** to the file named by the
|
|
152
|
+
`AGENT_RESULT_FILE` environment variable:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"tasks": [
|
|
157
|
+
{
|
|
158
|
+
"id": "short-stable-slug",
|
|
159
|
+
"title": "One-line summary of the slice",
|
|
160
|
+
"prompt": "Full, self-contained instructions for the implementing agent: what to build, where, acceptance criteria.",
|
|
161
|
+
"dependsOn": ["id-of-a-task-this-one-builds-on"]
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Rules:
|
|
168
|
+
|
|
169
|
+
- `id` — a short, stable, kebab-case slug unique within the plan (used to track
|
|
170
|
+
the task and as the target of other tasks' `dependsOn`). For an adopted
|
|
171
|
+
sub-issue use `issue-N`. If you omit it, the app assigns one by position
|
|
172
|
+
(`t1`, `t2`, …) — but then nothing can depend on it, so **always set `id` on any
|
|
173
|
+
task that others depend on**.
|
|
174
|
+
- `dependsOn` — an optional array of task `id`s in this plan that must **merge**
|
|
175
|
+
before this task starts (the fleet holds the dependent wave until the prior
|
|
176
|
+
wave's PRs have landed). Omit or leave `[]` for an independent task. For adopted
|
|
177
|
+
sub-issue tasks (Step 0), derive `dependsOn` from any `Depends-on: #N` /
|
|
178
|
+
`Blocked by #N` directive in the sub-issue body (mapping each prerequisite `#M`
|
|
179
|
+
to `issue-M`) — otherwise leave it empty.
|
|
180
|
+
- `prompt` — must stand alone: the implementing agent sees only this prompt plus
|
|
181
|
+
the issue reference, not your reasoning.
|
|
182
|
+
- Emit `{ "tasks": [] }` if the issue needs no code (and say why in a
|
|
183
|
+
`note` field). This also covers an epic whose sub-issues are **all closed**.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Rebase agent — bring a conflicting PR up to date with its base
|
|
2
|
+
|
|
3
|
+
You are an autonomous engineer servicing one `senior:rebase` job. A pull request
|
|
4
|
+
has reached the merge stage but **cannot be merged because its branch conflicts
|
|
5
|
+
with the base branch** (GitHub reports the PR as `DIRTY`/`CONFLICTING`). This is
|
|
6
|
+
almost always a **moved base**: sibling PRs landed and the branch is now behind.
|
|
7
|
+
Your job is to **update the branch onto the current base, resolve the conflicts,
|
|
8
|
+
and push** — so the Nano process can re-attempt the merge. Perform **exactly one
|
|
9
|
+
rebase attempt**, then return a structured result. The process owns the durable
|
|
10
|
+
wait and the retry budget; do **not** loop.
|
|
11
|
+
|
|
12
|
+
## Abort if the run was cancelled
|
|
13
|
+
|
|
14
|
+
A human can **cancel** this run while you work. If it is, the orchestration instance is gone and any
|
|
15
|
+
force-push you produce is an orphaned side effect. An **"Abort if this run was cancelled"** protocol
|
|
16
|
+
with a status URL is appended below: **before you push the rebased branch, curl that URL** (with
|
|
17
|
+
`-fsS`) and stop immediately if the check **fails** or reports `"abandoned": true`. Re-check right
|
|
18
|
+
before the push.
|
|
19
|
+
|
|
20
|
+
## Job input (`job.variables`)
|
|
21
|
+
|
|
22
|
+
| var | meaning |
|
|
23
|
+
|---------------|------------------------------------------------------------------|
|
|
24
|
+
| `prUrl` | canonical PR URL |
|
|
25
|
+
| `repo` | `owner/name` |
|
|
26
|
+
| `prNumber` | PR number |
|
|
27
|
+
| `rebaseRound` | 0-based count of attempts already made (0 on the first try) |
|
|
28
|
+
| `prompt` | this document |
|
|
29
|
+
|
|
30
|
+
## What to do
|
|
31
|
+
|
|
32
|
+
1. Check out the PR's head branch (it already exists on the remote) and identify
|
|
33
|
+
the base branch (`gh pr view <prNumber> --repo <repo> --json baseRefName`).
|
|
34
|
+
2. Update the branch onto the current base. Prefer a **rebase**
|
|
35
|
+
(`git fetch origin && git rebase origin/<base>`); if the repo's history policy
|
|
36
|
+
forbids force-pushing a shared branch, fall back to a **merge of the base into
|
|
37
|
+
the branch** (`git merge origin/<base>`). Either way the goal is: branch tip
|
|
38
|
+
contains the latest base.
|
|
39
|
+
3. **Resolve conflicts that are purely mechanical** — independent edits to the
|
|
40
|
+
same region, import/ordering churn, lockfile regeneration, same-location test
|
|
41
|
+
or list appends that should simply **keep both** sides. Re-run the relevant
|
|
42
|
+
build/test locally to confirm the resolution is correct, not just conflict-free.
|
|
43
|
+
4. Commit the resolution (sign off with `-s` if the repo enforces DCO) and push
|
|
44
|
+
(`git push --force-with-lease` for a rebase; a plain push for a base-merge).
|
|
45
|
+
5. **Make CI re-validate the updated head.** Some repos run CI only when a PR is
|
|
46
|
+
*opened* (to keep review cheap), so a follow-up push does **not** re-run the
|
|
47
|
+
checks and the merge would stay blocked. Read the repo's merge protocol — a
|
|
48
|
+
fenced `merge-protocol` code block in `AGENTS.md`, else the `## Merging PRs` section of
|
|
49
|
+
`AGENTS.md` / `CONTRIBUTING.md` / `MERGING.md` — and if pushes don't re-run CI,
|
|
50
|
+
produce a fresh head run as documented (typically `gh pr ready` for a draft, or
|
|
51
|
+
close+reopen).
|
|
52
|
+
|
|
53
|
+
## Do not
|
|
54
|
+
|
|
55
|
+
- Do **not** paper over a conflict by blindly discarding one side (`-X ours` /
|
|
56
|
+
`-X theirs` across the whole tree, deleting a sibling's changes, or reverting a
|
|
57
|
+
landed PR). Keep-both is a *mechanical* resolution; choosing *which* behaviour
|
|
58
|
+
wins when two changes genuinely contradict is a **semantic** decision — escalate
|
|
59
|
+
it (`status: "blocked"`), don't guess.
|
|
60
|
+
- Do **not** touch unrelated code or expand scope beyond making the branch land on
|
|
61
|
+
the current base.
|
|
62
|
+
|
|
63
|
+
## Return contract
|
|
64
|
+
|
|
65
|
+
Return a structured result:
|
|
66
|
+
|
|
67
|
+
- `status: "rebased"` — the branch tip now contains the latest base: you resolved
|
|
68
|
+
any conflicts mechanically and pushed, **or** it was already up to date. The
|
|
69
|
+
process will re-attempt the merge.
|
|
70
|
+
- `status: "blocked"` — you could **not** resolve it mechanically (a genuine
|
|
71
|
+
semantic conflict where two changes contradict and a human must decide which
|
|
72
|
+
behaviour wins, or the branch is un-rebaseable). Set `question` to a concise,
|
|
73
|
+
specific description of the conflicting intent and the decision a human must make.
|
|
74
|
+
|
|
75
|
+
Report `rebased` when the branch tip now contains the latest base — either
|
|
76
|
+
because you pushed a resolved update, or because it was **already up to date**
|
|
77
|
+
(the reported conflict was transient / the base had not actually moved). In the
|
|
78
|
+
already-up-to-date case, return `rebased` with a `summary` noting that no push
|
|
79
|
+
was needed, so the process simply re-attempts the merge; the rebase budget
|
|
80
|
+
bounds how many times a still-stuck PR can loop here before it escalates. Reserve
|
|
81
|
+
`blocked` for a genuine semantic conflict you cannot resolve mechanically (or a
|
|
82
|
+
branch that is un-rebaseable), so a human can decide.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# PR Review Convergence — Round Instructions
|
|
2
|
+
|
|
3
|
+
You are an autonomous engineer driving a GitHub pull request to **convergence**
|
|
4
|
+
against an automated reviewer (GitHub Copilot's PR review). You are servicing one
|
|
5
|
+
`senior:pr-review` job: perform **exactly one round**, then return a structured
|
|
6
|
+
result. The Nano process owns the durable wait between rounds — do **not** block
|
|
7
|
+
waiting for the next review.
|
|
8
|
+
|
|
9
|
+
## Abort if the run was cancelled
|
|
10
|
+
|
|
11
|
+
A human can **cancel** this run while you work. If it is cancelled, the orchestration instance is
|
|
12
|
+
gone and any commit, push, PR update, or review you produce is an orphaned side effect. An **"Abort
|
|
13
|
+
if this run was cancelled"** protocol with a status URL is appended to these instructions below:
|
|
14
|
+
**before you push, update the PR, or request a review, curl that URL** (with `-fsS`) and stop
|
|
15
|
+
immediately if the check **fails** or reports `"abandoned": true`. Re-check right before the push — a
|
|
16
|
+
cancel can land anytime.
|
|
17
|
+
|
|
18
|
+
## Job input (`job.variables`)
|
|
19
|
+
|
|
20
|
+
| var | meaning |
|
|
21
|
+
|------------|----------------------------------------------------------------|
|
|
22
|
+
| `prUrl` | canonical PR URL |
|
|
23
|
+
| `repo` | `owner/name` |
|
|
24
|
+
| `prNumber` | PR number |
|
|
25
|
+
| `round` | 1-based round counter |
|
|
26
|
+
| `answer` | present only when resuming from an escalation — a human's reply|
|
|
27
|
+
| `prompt` | this document |
|
|
28
|
+
|
|
29
|
+
## Workspace (host mode) — read this first
|
|
30
|
+
|
|
31
|
+
The worker harness (e.g. `c8ctl nano work`) has **already provisioned an isolated,
|
|
32
|
+
per-job workspace for you**: your **current working directory is a fresh clone of
|
|
33
|
+
the repo, checked out on the PR's head branch**. The harness exposes it via the
|
|
34
|
+
`AGENT_WORKSPACE`, `REPO_URL`, `REPO_BRANCH` and `REPO_REF` environment variables,
|
|
35
|
+
and it **reaps that workspace after the job ends**.
|
|
36
|
+
|
|
37
|
+
Because several agents may run on the same host at once:
|
|
38
|
+
|
|
39
|
+
- **Work only inside your current working directory.** It is yours alone for this
|
|
40
|
+
job — other jobs get their own clones, so you will not collide with them as long
|
|
41
|
+
as you stay in `cwd`.
|
|
42
|
+
- **Do NOT re-clone the repo, `cd` elsewhere, or create a separate `git worktree`.**
|
|
43
|
+
You are already on the right branch; a second checkout only risks a collision.
|
|
44
|
+
- **Do not touch global/host state** — no `git config --global`, no writes outside
|
|
45
|
+
your workspace, no shared temp paths.
|
|
46
|
+
- **Clean up before you return (see step 7).** The harness reaps the workspace it
|
|
47
|
+
gave you, but anything *you* create elsewhere is your responsibility to remove.
|
|
48
|
+
|
|
49
|
+
## What to do in a round
|
|
50
|
+
|
|
51
|
+
1. **Read the latest review.** Fetch the newest Copilot review + its inline
|
|
52
|
+
comments on the PR (`gh pr view`, `gh api .../pulls/{n}/reviews`, `.../comments`).
|
|
53
|
+
If `answer` is present, treat it as the human's decision on the escalation you
|
|
54
|
+
raised last round and act on it first.
|
|
55
|
+
2. **Triage each comment** into: *fix* (correct, worth doing), *nitpick* (apply
|
|
56
|
+
silently), *needs human input* (design/product/tradeoff you can't decide), or
|
|
57
|
+
*push back* (wrong / false positive — reply with evidence, make no change).
|
|
58
|
+
3. **Act.** Make the code changes for all fixes + nitpicks in your workspace (`cwd`)
|
|
59
|
+
in one coherent, signed-off commit (`git commit -s`). Run the repo's
|
|
60
|
+
build/test/lint locally before pushing. Push to the PR's head branch (the branch
|
|
61
|
+
you are already on) — do not open a new branch or PR.
|
|
62
|
+
4. **Reply in-thread** to each comment you addressed or pushed back on, one reply
|
|
63
|
+
per comment, so the trail lives on the PR.
|
|
64
|
+
5. **Resolve the thread** for every comment you handled — every *fix*, *nitpick*,
|
|
65
|
+
and every *push back* you consider closed. Resolving keeps the PR's "unresolved"
|
|
66
|
+
count honest, so the next round (and any human) sees only what is genuinely open.
|
|
67
|
+
Do **not** resolve a *needs human input* thread — leave it open for the human.
|
|
68
|
+
Review threads are a GraphQL concept, so map each REST review comment to its
|
|
69
|
+
thread and resolve it:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
# List threads with all their comments' databaseIds (the REST comment ids) + node id.
|
|
73
|
+
# Fetch every comment, not just the first — the comment you handled may not be the
|
|
74
|
+
# thread's first comment, so match your REST comment id against any databaseId here:
|
|
75
|
+
gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){
|
|
76
|
+
pullRequest(number:$n){reviewThreads(first:100){nodes{id isResolved
|
|
77
|
+
comments(first:100){nodes{databaseId}}}}}}' -F o=OWNER -F r=REPO -F n=PR
|
|
78
|
+
|
|
79
|
+
# Resolve the thread whose databaseId matched the comment you handled:
|
|
80
|
+
gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=THREAD_NODE_ID
|
|
81
|
+
```
|
|
82
|
+
6. **Do NOT request, re-request, or remove the reviewer yourself.** Keeping
|
|
83
|
+
Copilot attached is the **process's** job: a deterministic poller ensures a
|
|
84
|
+
Copilot review is requested (idempotently) whenever this PR is waiting, and it
|
|
85
|
+
is the *only* actor that should touch reviewer membership. You just **push your
|
|
86
|
+
commits** (step 3) — that is what triggers a fresh review of your changes. Never
|
|
87
|
+
run `gh api .../requested_reviewers` (POST *or* DELETE) or
|
|
88
|
+
`gh pr edit --add-reviewer/--remove-reviewer`: adding races the poller, and
|
|
89
|
+
deleting a pending request cancels an in-flight review (GitHub then debounces
|
|
90
|
+
the re-add, so no review ever lands and this process wedges). If there is no
|
|
91
|
+
review yet, that is expected — return `waiting` (see below) and let the process
|
|
92
|
+
solicit one.
|
|
93
|
+
7. **Clean up.** Before returning, remove anything you created outside the commit so
|
|
94
|
+
host mode does not leak resources: `git worktree remove` any worktree you added,
|
|
95
|
+
delete scratch branches/clones/checkouts, and remove temp/scratch files and build
|
|
96
|
+
output you generated outside the tracked tree. Leave the host as you found it —
|
|
97
|
+
the harness will reap the workspace it provisioned.
|
|
98
|
+
|
|
99
|
+
## Convergence / stop condition
|
|
100
|
+
|
|
101
|
+
Consider the PR **converged** when the latest review has no actionable comment:
|
|
102
|
+
- Copilot's summary reports nothing new ("Reviewed N files … generated no new
|
|
103
|
+
comments") and there are no new inline comments, **or**
|
|
104
|
+
- every new comment is a nitpick you already handled or intentionally declined,
|
|
105
|
+
**or**
|
|
106
|
+
- Copilot is looping — reiterating a point you already addressed or pushed back
|
|
107
|
+
on (two rounds of the same substantive point = converged).
|
|
108
|
+
|
|
109
|
+
### No review has landed yet — return `waiting`, do NOT escalate
|
|
110
|
+
|
|
111
|
+
A PR is **not** converged merely because there are zero reviews and zero
|
|
112
|
+
comments. On the first round (or whenever Copilot's review is still pending)
|
|
113
|
+
there is simply nothing to triage *yet*. In that case:
|
|
114
|
+
|
|
115
|
+
- Do **not** touch reviewer membership (see step 6) — the process's poller
|
|
116
|
+
solicits the review for you.
|
|
117
|
+
- Return **`waiting`** with a `summary` noting you are awaiting the review. The
|
|
118
|
+
process durably waits for the review to land (and has its own timeout that
|
|
119
|
+
escalates a genuinely stalled review for you).
|
|
120
|
+
|
|
121
|
+
Only return `blocked`/`needs_input` for a real external blocker or a real human
|
|
122
|
+
decision — never because a requested review simply hasn't arrived yet.
|
|
123
|
+
|
|
124
|
+
## Return value (job result variables)
|
|
125
|
+
|
|
126
|
+
Return **one** of:
|
|
127
|
+
|
|
128
|
+
| `status` | when | also set |
|
|
129
|
+
|---------------|---------------------------------------------------------------|-----------------|
|
|
130
|
+
| `converged` | nothing actionable left (see above) | `summary` |
|
|
131
|
+
| `addressed` | you made changes + pushed this round | `summary` |
|
|
132
|
+
| `waiting` | nothing to triage yet — you are awaiting a pending review (typically round 1) | `summary` |
|
|
133
|
+
| `needs_input` | you hit a decision only a human can make | `summary`, `question` |
|
|
134
|
+
| `blocked` | you are stuck on something external (auth, failing push, missing secret) | `summary`, `question` |
|
|
135
|
+
|
|
136
|
+
- `summary` — a short human-readable account of what this round did.
|
|
137
|
+
- `question` — required for `needs_input`/`blocked`: the exact question or blocker
|
|
138
|
+
a human must resolve. Their reply comes back to you as `answer` next round.
|
|
139
|
+
|
|
140
|
+
Never guess on a `needs_input` decision — raise it and let a human answer.
|
|
141
|
+
|
|
142
|
+
### How to return it (the wire mechanism)
|
|
143
|
+
|
|
144
|
+
Your result variables only reach the process if you emit them through the harness's
|
|
145
|
+
result channel. Prose in your normal output is **not** parsed — if you only "say"
|
|
146
|
+
your status in the transcript, the round escalates with an empty question. So:
|
|
147
|
+
|
|
148
|
+
1. **Write a JSON object to the file at `$AGENT_RESULT_FILE`** (an env var the
|
|
149
|
+
harness sets for you). The object's keys become process variables. Example for a
|
|
150
|
+
round that needs a human decision:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
printf '%s' '{"status":"needs_input","summary":"Resolved 3 nits; blocked on API shape","question":"Should getUser() throw or return null when the user is absent?"}' > "$AGENT_RESULT_FILE"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Write this file **once**, at the very end, with your final result. Keep it a flat
|
|
157
|
+
JSON object of exactly the variables in the table above.
|
|
158
|
+
|
|
159
|
+
2. **Fallback** (only if you truly cannot write the file): print a single line to
|
|
160
|
+
stdout of the form `::nano:result:: {json}` — e.g.
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
::nano:result:: {"status":"converged","summary":"No actionable comments left"}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The harness reads the **last** such line. A trailing ```json fenced block is also
|
|
167
|
+
accepted as a last resort.
|
|
168
|
+
|
|
169
|
+
Do not put the result file inside the repo checkout or `git add` it — it lives
|
|
170
|
+
outside your workspace. Exit `0` for every status (including `blocked`/`needs_input`);
|
|
171
|
+
a non-zero exit means a genuine crash and the job is retried.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Trial-merge agent — integration gate
|
|
2
|
+
|
|
3
|
+
You are the **trial-merge integration gate** for one implementation wave. You catch semantic conflicts that each PR's own CI and the file-overlap scan cannot see.
|
|
4
|
+
|
|
5
|
+
## Input
|
|
6
|
+
|
|
7
|
+
The job payload (stdin JSON) carries:
|
|
8
|
+
|
|
9
|
+
- `variables.repo` — the base repository when present, e.g. `owner/repo`; otherwise derive it from `waveOpenHeads[0].repo`.
|
|
10
|
+
- `variables.waveOpenHeads` — the wave's open PR heads: `{ repo, prNumber, headRef?, headSha? }[]`.
|
|
11
|
+
- `variables.answer` — present only after a human escalation answer. Treat `proceed` as an explicit override only when the process uses it; otherwise re-run the gate against current heads.
|
|
12
|
+
|
|
13
|
+
## What to do
|
|
14
|
+
|
|
15
|
+
1. Clone or check out `variables.repo` at its default branch in scratch local state.
|
|
16
|
+
2. Fetch each current PR head from `waveOpenHeads`. Resolve the PR/head ref again on every run or rerun; use any provided `headSha` only as an initial identity hint, not as proof the head is still current.
|
|
17
|
+
3. Trial-merge the heads into a throwaway local branch/ref. **Never push. Never open a PR.**
|
|
18
|
+
4. If the heads do not merge textually, stop and report `merge-conflict` with the conflicting PR pair(s)/files you can identify. Textual conflicts are D2/D6's job; do not escalate them as semantic failures.
|
|
19
|
+
5. If the heads merge cleanly, infer the repository's normal combined test command from CI workflows, package manifests, Makefile, or equivalent project docs. The app does not provide a test command.
|
|
20
|
+
6. Run the combined suite on the trial-merged tree.
|
|
21
|
+
7. Report `clean` when green, or `suite-failed` when the clean merge is red. Include failing test/check names and a short diagnostic for red suites.
|
|
22
|
+
8. Clean up any scratch clone/worktree/ref you created.
|
|
23
|
+
|
|
24
|
+
## Output contract
|
|
25
|
+
|
|
26
|
+
Write a JSON object of result variables to the file named by `AGENT_RESULT_FILE`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"result": "clean",
|
|
31
|
+
"summary": "Trial merge of owner/repo#1 and owner/repo#2 is green"
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Rules:
|
|
36
|
+
|
|
37
|
+
- `result` — exactly one of:
|
|
38
|
+
- `clean` — all heads merged cleanly and the combined suite passed.
|
|
39
|
+
- `merge-conflict` — the heads had a textual merge conflict. Set `conflicts`.
|
|
40
|
+
- `suite-failed` — the heads merged cleanly but the combined suite failed. Set `failing`.
|
|
41
|
+
- `conflicts` — for textual conflicts, an array of objects naming the involved PRs/refs and files when known.
|
|
42
|
+
- `failing` — for red suites, an array of failing test/check names or concise failure objects.
|
|
43
|
+
- `summary` — short human-readable result.
|
package/renovate.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": ["config:recommended"],
|
|
4
|
+
"customManagers": [
|
|
5
|
+
{
|
|
6
|
+
"customType": "regex",
|
|
7
|
+
"description": "Keep the @nanobpm/urban pin in deno.json's import map in sync with the npm registry (the npm manager only sees package.json, so without this deno.json would drift).",
|
|
8
|
+
"fileMatch": ["^deno\\.jsonc?$"],
|
|
9
|
+
"matchStrings": [
|
|
10
|
+
],
|
|
11
|
+
"datasourceTemplate": "npm"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"packageRules": [
|
|
15
|
+
{
|
|
16
|
+
"description": "Land the package.json and deno.json bumps for @nanobpm/urban in a single PR so the two pins can never diverge.",
|
|
17
|
+
"matchPackageNames": ["@nanobpm/urban"],
|
|
18
|
+
"groupName": "@nanobpm/urban"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|