@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/assets/icon.svg
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Nano Workforce">
|
|
2
|
+
<title>Nano Workforce</title>
|
|
3
|
+
<!-- Three coordinated worker nodes converging on a shared task -->
|
|
4
|
+
<circle cx="12" cy="4.25" r="2.25" />
|
|
5
|
+
<circle cx="4.75" cy="16.5" r="2.25" />
|
|
6
|
+
<circle cx="19.25" cy="16.5" r="2.25" />
|
|
7
|
+
<!-- Links forming the coordination triangle -->
|
|
8
|
+
<path d="M10.4 6L6.35 14.75" />
|
|
9
|
+
<path d="M13.6 6L17.65 14.75" />
|
|
10
|
+
<path d="M7 16.5H17" />
|
|
11
|
+
<!-- Central convergence mark -->
|
|
12
|
+
<circle cx="12" cy="16.5" r="0.9" fill="currentColor" stroke="none" />
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema@0.44.0/resources/schema.json",
|
|
3
|
+
"id": "io.nanobpm.urban.pr-review-round",
|
|
4
|
+
"name": "PR Review Round (agent)",
|
|
5
|
+
"description": "One decoupled review round serviced by an external agent (e.g. a Copilot instance via `c8ctl nano work`). The agent reads the latest review, addresses it, pushes, re-requests review, and returns a structured status.",
|
|
6
|
+
"appliesTo": ["bpmn:Task"],
|
|
7
|
+
"elementType": { "value": "bpmn:ServiceTask" },
|
|
8
|
+
"properties": [
|
|
9
|
+
{
|
|
10
|
+
"type": "Hidden",
|
|
11
|
+
"value": "senior:pr-review",
|
|
12
|
+
"binding": { "type": "zeebe:taskDefinition:type" }
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"label": "Input data-envelope type",
|
|
16
|
+
"type": "Hidden",
|
|
17
|
+
"value": "PrReviewRoundIn",
|
|
18
|
+
"binding": { "type": "zeebe:property", "name": "io.nanobpm.dataEnvelope.in" }
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"label": "Output data-envelope type",
|
|
22
|
+
"type": "Hidden",
|
|
23
|
+
"value": "PrReviewRoundOut",
|
|
24
|
+
"binding": { "type": "zeebe:property", "name": "io.nanobpm.dataEnvelope.out" }
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"label": "PR URL",
|
|
28
|
+
"type": "String",
|
|
29
|
+
"feel": "optional",
|
|
30
|
+
"value": "=prUrl",
|
|
31
|
+
"binding": { "type": "zeebe:input", "name": "prUrl" }
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"label": "Repo (owner/name)",
|
|
35
|
+
"type": "String",
|
|
36
|
+
"feel": "optional",
|
|
37
|
+
"value": "=repo",
|
|
38
|
+
"binding": { "type": "zeebe:input", "name": "repo" }
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"label": "PR number",
|
|
42
|
+
"type": "String",
|
|
43
|
+
"feel": "optional",
|
|
44
|
+
"value": "=prNumber",
|
|
45
|
+
"binding": { "type": "zeebe:input", "name": "prNumber" }
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"label": "Round",
|
|
49
|
+
"type": "String",
|
|
50
|
+
"feel": "optional",
|
|
51
|
+
"value": "=round",
|
|
52
|
+
"binding": { "type": "zeebe:input", "name": "round" }
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"label": "Prompt (instructions)",
|
|
56
|
+
"type": "String",
|
|
57
|
+
"feel": "optional",
|
|
58
|
+
"value": "=prompt",
|
|
59
|
+
"binding": { "type": "zeebe:input", "name": "prompt" }
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"label": "Answer (on escalation resume)",
|
|
63
|
+
"type": "String",
|
|
64
|
+
"feel": "optional",
|
|
65
|
+
"value": "=answer",
|
|
66
|
+
"binding": { "type": "zeebe:input", "name": "answer" }
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- nano-workforce schema (SPEC §7). One row per PR under convergence, its rounds,
|
|
2
|
+
-- and any escalations raised mid-convergence.
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS pull_requests (
|
|
5
|
+
pr_key TEXT PRIMARY KEY, -- "<owner>/<repo>#<number>"
|
|
6
|
+
repo TEXT NOT NULL, -- "<owner>/<repo>"
|
|
7
|
+
number INTEGER NOT NULL,
|
|
8
|
+
url TEXT NOT NULL,
|
|
9
|
+
title TEXT, -- fetched from GitHub (optional)
|
|
10
|
+
status TEXT NOT NULL, -- converging | waiting_review | escalated | converged | abandoned
|
|
11
|
+
current_round INTEGER NOT NULL DEFAULT 0,
|
|
12
|
+
process_key TEXT, -- engine process-instance key
|
|
13
|
+
waiting_since TEXT, -- ISO ts we began waiting for a review (poller cursor)
|
|
14
|
+
last_review_id INTEGER, -- last GitHub review id we reacted to
|
|
15
|
+
outcome TEXT, -- final summary
|
|
16
|
+
created_at TEXT NOT NULL,
|
|
17
|
+
updated_at TEXT NOT NULL,
|
|
18
|
+
converged_at TEXT
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE TABLE IF NOT EXISTS rounds (
|
|
22
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
+
pr_key TEXT NOT NULL REFERENCES pull_requests(pr_key),
|
|
24
|
+
round_no INTEGER NOT NULL,
|
|
25
|
+
status TEXT, -- converged | addressed | waiting | needs_input | blocked
|
|
26
|
+
summary TEXT,
|
|
27
|
+
started_at TEXT NOT NULL,
|
|
28
|
+
ended_at TEXT
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS escalations (
|
|
32
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
33
|
+
pr_key TEXT NOT NULL REFERENCES pull_requests(pr_key),
|
|
34
|
+
round_no INTEGER NOT NULL,
|
|
35
|
+
kind TEXT NOT NULL, -- question | blocker
|
|
36
|
+
question TEXT NOT NULL,
|
|
37
|
+
answer TEXT,
|
|
38
|
+
status TEXT NOT NULL, -- open | answered
|
|
39
|
+
asked_at TEXT NOT NULL,
|
|
40
|
+
answered_at TEXT
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_pr_status ON pull_requests(status);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_rounds_pr ON rounds(pr_key);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_esc_pr ON escalations(pr_key);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_esc_open ON escalations(pr_key, status);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- Auditability: keep the agent's full transcript (the harness's byte-capped
|
|
2
|
+
-- stdout capture, surfaced on the io.nanobpm.agentResult envelope) alongside the
|
|
3
|
+
-- round it produced and any escalation it raised, so a human can see exactly what
|
|
4
|
+
-- the agent did without re-running it.
|
|
5
|
+
|
|
6
|
+
ALTER TABLE rounds ADD COLUMN transcript TEXT;
|
|
7
|
+
ALTER TABLE escalations ADD COLUMN transcript TEXT;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Denormalise the currently-open escalation onto the PR row so the generic page
|
|
2
|
+
-- runtime (ADR 0042) can bind a conditional "answer" form to it without a join:
|
|
3
|
+
-- the form is shown when `open_escalation_id` is set and prints
|
|
4
|
+
-- `open_escalation_question`. The persist-escalation worker sets these when it
|
|
5
|
+
-- opens an escalation, and answering (or finalising) clears them.
|
|
6
|
+
|
|
7
|
+
ALTER TABLE pull_requests ADD COLUMN open_escalation_id INTEGER;
|
|
8
|
+
ALTER TABLE pull_requests ADD COLUMN open_escalation_question TEXT;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- Merge stage (SPEC §11). Once a PR converges, a separate `merge-loop` process drives it to
|
|
2
|
+
-- land: it waits for any declared cross-PR dependencies to merge first, then for the PR to
|
|
3
|
+
-- become mergeable (checks green, no conflicts), then merges it — directly or via the repo's
|
|
4
|
+
-- merge queue — escalating to a human when it is blocked. These columns/tables record that
|
|
5
|
+
-- stage alongside the convergence stage already captured by 001_init.sql.
|
|
6
|
+
|
|
7
|
+
-- Timestamp the app recorded the PR as landed (NULL until merged). Set by `pr.mark-merged` to the
|
|
8
|
+
-- time this app *observed/recorded* the merge, which can lag GitHub's actual `merged_at` (e.g. for
|
|
9
|
+
-- merge-queue or out-of-band merges). `outcome` (001) still holds the convergence summary; a
|
|
10
|
+
-- separate timestamp keeps "converged" and "merged" distinguishable on the row.
|
|
11
|
+
ALTER TABLE pull_requests ADD COLUMN merged_at TEXT;
|
|
12
|
+
|
|
13
|
+
-- Cross-PR merge dependencies: this PR must not merge until every `depends_on_key` PR has
|
|
14
|
+
-- merged. Declared at submit time (a `dependsOn` field on the submit endpoints and/or a
|
|
15
|
+
-- `Depends-on: owner/repo#N` line in the PR body). Re-submitting a PR replaces its dep set.
|
|
16
|
+
CREATE TABLE IF NOT EXISTS pr_dependencies (
|
|
17
|
+
pr_key TEXT NOT NULL, -- the dependent PR ("<owner>/<repo>#<number>")
|
|
18
|
+
depends_on_key TEXT NOT NULL, -- the PR it waits on ("<owner>/<repo>#<number>")
|
|
19
|
+
created_at TEXT NOT NULL,
|
|
20
|
+
PRIMARY KEY (pr_key, depends_on_key)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
-- Audit trail of merge attempts, so a human can see how a PR landed (or why it stalled)
|
|
24
|
+
-- without re-running anything — the merge-stage analogue of `rounds`.
|
|
25
|
+
CREATE TABLE IF NOT EXISTS merges (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
pr_key TEXT NOT NULL REFERENCES pull_requests(pr_key),
|
|
28
|
+
outcome TEXT NOT NULL, -- merged | queued | blocked
|
|
29
|
+
method TEXT, -- squash | merge | rebase | queue
|
|
30
|
+
detail TEXT, -- gh/API message (success line or error)
|
|
31
|
+
at TEXT NOT NULL
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_deps_pr ON pr_dependencies(pr_key);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_deps_on ON pr_dependencies(depends_on_key);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_merges_pr ON merges(pr_key);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- Planning fan-out (issue #14): a planning agent decomposes an issue into tasks,
|
|
2
|
+
-- a fleet of implementation agents build them in parallel, each opening a PR that
|
|
3
|
+
-- is then enrolled into the review-convergence loop.
|
|
4
|
+
--
|
|
5
|
+
-- `plans` is one row per issue put through the fleet; `plan_tasks` is one row per
|
|
6
|
+
-- slice the planner emitted, tracking the PR it produced and its dispatch state.
|
|
7
|
+
|
|
8
|
+
CREATE TABLE plans (
|
|
9
|
+
plan_key TEXT PRIMARY KEY, -- "<owner>/<repo>#<issue-number>"
|
|
10
|
+
repo TEXT NOT NULL, -- "<owner>/<repo>"
|
|
11
|
+
issue_number INTEGER NOT NULL,
|
|
12
|
+
issue_url TEXT NOT NULL,
|
|
13
|
+
title TEXT, -- issue title (best-effort)
|
|
14
|
+
status TEXT NOT NULL, -- planning | dispatched | done | failed | abandoned
|
|
15
|
+
task_count INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
process_key TEXT, -- engine process-instance key
|
|
17
|
+
outcome TEXT, -- final summary / note
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
updated_at TEXT NOT NULL
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE plan_tasks (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
plan_key TEXT NOT NULL REFERENCES plans(plan_key),
|
|
25
|
+
task_index INTEGER NOT NULL, -- position in the plan (aligns with the MI output collection)
|
|
26
|
+
task_id TEXT NOT NULL, -- planner-supplied slug (or "t<index>")
|
|
27
|
+
title TEXT,
|
|
28
|
+
prompt TEXT,
|
|
29
|
+
status TEXT NOT NULL, -- pending | opened | blocked | skipped
|
|
30
|
+
pr_key TEXT, -- the PR this slice produced ("<owner>/<repo>#<n>")
|
|
31
|
+
summary TEXT,
|
|
32
|
+
created_at TEXT NOT NULL,
|
|
33
|
+
updated_at TEXT NOT NULL
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX idx_plans_status ON plans(status);
|
|
37
|
+
CREATE INDEX idx_plan_tasks_plan ON plan_tasks(plan_key);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Job-activation visibility. The `converging` status conflates two distinct engine
|
|
2
|
+
-- states of the `senior:pr-review` job the process parks at (the `review-round`
|
|
3
|
+
-- service task): the job may be CREATED but not yet activated (no agent has picked
|
|
4
|
+
-- it up — "waiting for activation"), or activated/leased to a worker (an agent is
|
|
5
|
+
-- actively working the round). The engine tracks this (JobState::Created vs
|
|
6
|
+
-- ::Activated) but the Camunda-8 `/v2/jobs/search` wire API collapses Activated ->
|
|
7
|
+
-- CREATED (Camunda's JobStateEnum has no ACTIVATED value), so activation is read
|
|
8
|
+
-- off the standard `worker` + `deadline` fields: an activated job carries the
|
|
9
|
+
-- leasing worker's name and a lock deadline; a merely-created one carries neither.
|
|
10
|
+
--
|
|
11
|
+
-- The review-ready poller writes these from a `/v2/jobs/search` pass so the pages
|
|
12
|
+
-- surface can show "agent working" vs "queued (awaiting an agent)" — and, via the
|
|
13
|
+
-- lease deadline, a stalled/lost agent whose lock is lapsing.
|
|
14
|
+
ALTER TABLE pull_requests ADD COLUMN active_worker TEXT; -- leasing worker's name while an agent is working the round; NULL when queued/not at review-round
|
|
15
|
+
ALTER TABLE pull_requests ADD COLUMN lease_until TEXT; -- ISO ts the current activation lease expires (job deadline); NULL when not activated
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Mixed sequential + parallel plan fan-out (issue #20): planner-emitted tasks may
|
|
2
|
+
-- declare dependencies on earlier tasks. A pure levelizer (app/waves.ts) assigns
|
|
3
|
+
-- each task a 0-based `wave` (longest-path level); the plan-fanout process runs the
|
|
4
|
+
-- parallel multi-instance `implement` activity once per wave, in order. Tasks within
|
|
5
|
+
-- a wave still run in parallel; a later wave waits for the whole earlier wave.
|
|
6
|
+
--
|
|
7
|
+
-- `plan_task_deps` records the task DAG (one row per edge), mirroring
|
|
8
|
+
-- `pr_dependencies` for PRs — from which per-PR merge ordering already falls out.
|
|
9
|
+
-- `plan_tasks.wave` is the levelized wave index (NULL until levelized).
|
|
10
|
+
|
|
11
|
+
ALTER TABLE plan_tasks ADD COLUMN wave INTEGER;
|
|
12
|
+
|
|
13
|
+
CREATE TABLE plan_task_deps (
|
|
14
|
+
plan_key TEXT NOT NULL REFERENCES plans(plan_key),
|
|
15
|
+
task_id TEXT NOT NULL, -- the dependent task's slug
|
|
16
|
+
depends_on_task_id TEXT NOT NULL, -- the task it waits for
|
|
17
|
+
PRIMARY KEY (plan_key, task_id, depends_on_task_id)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE INDEX idx_plan_task_deps_plan ON plan_task_deps(plan_key);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Adversarial plan review (issue: gate the plan before fan-out). After the `senior:plan`
|
|
2
|
+
-- agent emits a plan and `record-plan` levelizes it, an INDEPENDENT `senior:plan-review`
|
|
3
|
+
-- agent adversarially critiques the decomposition and sequencing BEFORE any wave dispatches.
|
|
4
|
+
-- On a rejecting verdict the planner revises (bounded rounds); on approval (or once the round
|
|
5
|
+
-- cap is hit) the wave loop proceeds. The plan is the highest-leverage artifact in the fleet —
|
|
6
|
+
-- a wrong decomposition or a mis-placed dependency dooms every downstream PR — so it gets the
|
|
7
|
+
-- same falsification treatment the PRs themselves get.
|
|
8
|
+
--
|
|
9
|
+
-- `plan_reviews` is the append-only audit log: one row per review round, with the verdict and
|
|
10
|
+
-- the reviewer's findings. The current round is derived from the row count, so the loop needs
|
|
11
|
+
-- no counter variable.
|
|
12
|
+
|
|
13
|
+
CREATE TABLE plan_reviews (
|
|
14
|
+
plan_key TEXT NOT NULL REFERENCES plans(plan_key),
|
|
15
|
+
round INTEGER NOT NULL, -- 0-based review round
|
|
16
|
+
approved INTEGER NOT NULL, -- 1 = reviewer approved, 0 = rejected (revise)
|
|
17
|
+
findings TEXT, -- the reviewer's critique (drives the planner's revision)
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
PRIMARY KEY (plan_key, round)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE INDEX idx_plan_reviews_plan ON plan_reviews(plan_key);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
-- Implementation-phase escalation (issue #25): give a stuck implementation agent
|
|
2
|
+
-- a human-in-the-loop path DURING the fan-out, mirroring the review loop's per-PR
|
|
3
|
+
-- escalation but per-TASK.
|
|
4
|
+
--
|
|
5
|
+
-- The agent opens a DRAFT PR (to preserve its work in git), reports
|
|
6
|
+
-- `status = "escalated"` with a `question`, and completes its job promptly. The
|
|
7
|
+
-- `plan-fanout` process parks that child at a per-task message catch
|
|
8
|
+
-- (`feature-escalation-answered`, correlated on `<plan_key>:<task_id>`); a human
|
|
9
|
+
-- answers, the child re-dispatches the SAME task with the answer merged in, and
|
|
10
|
+
-- the agent continues on the same branch. Context is externalised to git (the
|
|
11
|
+
-- draft PR/branch) + the task row, so a fresh agent on ANY worker machine can
|
|
12
|
+
-- resume (see issue #25 "stateless cold re-dispatch").
|
|
13
|
+
|
|
14
|
+
-- Per-task escalation state. `corr_key` is "<plan_key>:<task_id>" — the message
|
|
15
|
+
-- correlation key the process opens its wait subscription on. `draft_pr_key` is
|
|
16
|
+
-- the work-preserving draft PR the agent opened before escalating.
|
|
17
|
+
ALTER TABLE plan_tasks ADD COLUMN open_question TEXT;
|
|
18
|
+
ALTER TABLE plan_tasks ADD COLUMN answer TEXT;
|
|
19
|
+
ALTER TABLE plan_tasks ADD COLUMN draft_pr_key TEXT;
|
|
20
|
+
ALTER TABLE plan_tasks ADD COLUMN corr_key TEXT;
|
|
21
|
+
|
|
22
|
+
-- Denormalise the plan's OLDEST still-open task escalation onto the plan row so
|
|
23
|
+
-- the generic page runtime (ADR 0042) can bind a single conditional "answer" form
|
|
24
|
+
-- to it without a per-child form (which the runtime does not support). The
|
|
25
|
+
-- persist worker sets these when it opens an escalation; answering re-points them
|
|
26
|
+
-- at the next-oldest open escalation (or clears them). `open_task_corr_key` is the
|
|
27
|
+
-- form's correlationKey; `open_task_id` is shown for context.
|
|
28
|
+
ALTER TABLE plans ADD COLUMN open_task_escalation_id INTEGER;
|
|
29
|
+
ALTER TABLE plans ADD COLUMN open_task_question TEXT;
|
|
30
|
+
ALTER TABLE plans ADD COLUMN open_task_corr_key TEXT;
|
|
31
|
+
ALTER TABLE plans ADD COLUMN open_task_id TEXT;
|
|
32
|
+
|
|
33
|
+
-- One row per implementation-phase escalation (the audit trail, mirroring
|
|
34
|
+
-- `escalations` for the review loop). `status` is open | answered.
|
|
35
|
+
CREATE TABLE plan_escalations (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
plan_key TEXT NOT NULL REFERENCES plans(plan_key),
|
|
38
|
+
task_id TEXT NOT NULL, -- the escalating task's slug
|
|
39
|
+
corr_key TEXT NOT NULL, -- "<plan_key>:<task_id>"
|
|
40
|
+
question TEXT NOT NULL,
|
|
41
|
+
answer TEXT,
|
|
42
|
+
draft_pr_key TEXT, -- draft PR the agent opened to preserve work
|
|
43
|
+
status TEXT NOT NULL, -- open | answered
|
|
44
|
+
asked_at TEXT NOT NULL,
|
|
45
|
+
answered_at TEXT
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX idx_plan_escalations_plan ON plan_escalations(plan_key);
|
|
49
|
+
CREATE INDEX idx_plan_escalations_open ON plan_escalations(plan_key, status);
|
|
50
|
+
-- The persist worker and `answerTaskEscalation` look up the open row by
|
|
51
|
+
-- (corr_key, status); index it to avoid a scan (mirrors escalations(pr_key, status)).
|
|
52
|
+
CREATE INDEX idx_plan_escalations_corr ON plan_escalations(corr_key, status);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Idempotency key for record-plan-review (issue: gate the plan before fan-out).
|
|
2
|
+
--
|
|
3
|
+
-- The review round is derived from `count(plan_reviews)` (no counter variable). That derivation
|
|
4
|
+
-- is NOT idempotent under Zeebe job retries: if the `pr.record-plan-review` job crashes or its
|
|
5
|
+
-- deadline lapses AFTER the row insert but BEFORE the job completes, the engine re-activates the
|
|
6
|
+
-- SAME job (stable `jobKey`). A naive retry would insert a SECOND row for a fresh round index,
|
|
7
|
+
-- inflating the count and tripping `reviewExhausted` early — proceeding to fan-out with fewer
|
|
8
|
+
-- review rounds than intended.
|
|
9
|
+
--
|
|
10
|
+
-- Recording the engine `jobKey` that wrote each row lets the worker detect a retry and reuse the
|
|
11
|
+
-- already-recorded verdict instead of appending a duplicate. Pre-existing rows keep `job_key`
|
|
12
|
+
-- NULL; SQLite treats NULLs as distinct in a UNIQUE index, so they never collide.
|
|
13
|
+
ALTER TABLE plan_reviews ADD COLUMN job_key TEXT;
|
|
14
|
+
CREATE UNIQUE INDEX idx_plan_reviews_job ON plan_reviews(plan_key, job_key);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Wave-merge barrier (release-notes-concierge epic): a later wave must not be
|
|
2
|
+
-- IMPLEMENTED until the prior wave's PRs have MERGED — not merely opened.
|
|
3
|
+
--
|
|
4
|
+
-- Before this, `record-wave` handed off a wave's PRs to convergence/merge and the
|
|
5
|
+
-- plan-fanout loop went straight back to `select-wave`, dispatching the next wave's
|
|
6
|
+
-- agents off an UNMERGED base. The `pr_dependencies` DAG only ordered the eventual
|
|
7
|
+
-- *merges*; it never held back a dependent wave's *implementation*. For a blocking
|
|
8
|
+
-- prerequisite (e.g. app scaffolding that every later task builds on) the next wave
|
|
9
|
+
-- must see the merged prerequisite on `main`.
|
|
10
|
+
--
|
|
11
|
+
-- `plans.gate_wave` is the durable marker for the barrier: when `record-wave` hands
|
|
12
|
+
-- off a wave that has a successor, it records that wave's index here and the process
|
|
13
|
+
-- parks at the `wait-wave-merged` catch event. The poller (`pollWaveGates`) clears it
|
|
14
|
+
-- and publishes `wave-merged` once every opened PR in that wave has merged. NULL means
|
|
15
|
+
-- the plan is not currently parked at the wave barrier.
|
|
16
|
+
ALTER TABLE plans ADD COLUMN gate_wave INTEGER;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Review-wait liveness. A PR parked in `waiting_review` blocks on a *fresh* Copilot review
|
|
2
|
+
-- arriving. Copilot does not spontaneously re-review a round with no new commit, and it
|
|
3
|
+
-- routinely dismisses a re-request (`review_request_removed`) — so without an active nudge the
|
|
4
|
+
-- loop can hang indefinitely at the `wait-review` catch (observed: three merlin convergence
|
|
5
|
+
-- processes stalled ~22h). The poller now re-requests Copilot on a waiting PR that has no
|
|
6
|
+
-- pending Copilot reviewer; `last_nudge_at` records when it last did so, so the nudge is
|
|
7
|
+
-- throttled to one attempt per cooldown window (NANO_PR_REVIEW_NUDGE_MINUTES) rather than
|
|
8
|
+
-- hammering the reviewers API on every poll tick. NULL = never nudged.
|
|
9
|
+
ALTER TABLE pull_requests ADD COLUMN last_nudge_at TEXT; -- ISO ts of the last Copilot re-request nudge; NULL when never nudged
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- Epic coordination blackboard — Tier 1 (issues #51 / #49 D4).
|
|
2
|
+
--
|
|
3
|
+
-- A per-plan advisory shared store that implementer agents READ on dispatch and WRITE to
|
|
4
|
+
-- (file-claims, constraint/scope changes), so parallel siblings in a wave can coordinate
|
|
5
|
+
-- without a human relay. Before this, the only structured channel was task->human escalation;
|
|
6
|
+
-- all sibling coordination ("task U now also touches state.rs") lived in PR/issue prose the
|
|
7
|
+
-- planner and other agents could not act on (retro: Magikcraft/nano-bpm#614).
|
|
8
|
+
--
|
|
9
|
+
-- Deliberately kept OUT of process/engine state: it is *advisory knowledge*, never gates a
|
|
10
|
+
-- sequence flow, and is read fresh (so it need not be replay-deterministic). Control-flow stays
|
|
11
|
+
-- in the BPMN; this is a shared scratchpad.
|
|
12
|
+
--
|
|
13
|
+
-- Delivery vs use: the plan process seeds a rendered coordination brief (including THIS plan's
|
|
14
|
+
-- capability URL) into `appendPrompt`; the c8ctl-nano harness forwards that prompt verbatim, so
|
|
15
|
+
-- it needs no change. The agent then talks to the endpoint DIRECTLY as a side-channel — a
|
|
16
|
+
-- different endpoint from the activation/completion channel.
|
|
17
|
+
|
|
18
|
+
-- Per-plan capability token: the unguessable credential baked into the blackboard URL handed to
|
|
19
|
+
-- agents (the URL *is* the credential). Minted at plan start; `/hooks/blackboard` maps it back to
|
|
20
|
+
-- this plan and scopes reads/writes to it. NULL for plans created before this migration.
|
|
21
|
+
ALTER TABLE plans ADD COLUMN blackboard_token TEXT;
|
|
22
|
+
|
|
23
|
+
CREATE TABLE plan_blackboard (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
plan_key TEXT NOT NULL REFERENCES plans(plan_key),
|
|
26
|
+
author_task TEXT NOT NULL DEFAULT 'system', -- the writing task's slug (or 'system' for host writes)
|
|
27
|
+
kind TEXT NOT NULL DEFAULT 'note', -- file-claim | constraint-change | scope-change | note
|
|
28
|
+
files TEXT, -- JSON array of repo-relative paths (nullable; feeds D2)
|
|
29
|
+
body TEXT NOT NULL, -- the human/agent-readable note
|
|
30
|
+
wave INTEGER, -- optional wave index the writer was dispatched in
|
|
31
|
+
dedupe_key TEXT, -- idempotency key (see the unique index below)
|
|
32
|
+
created_at TEXT NOT NULL
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- Idempotent write-back: the engine may re-activate a job on retry, so re-POSTing the same entry
|
|
36
|
+
-- (same author + stable dedupe_key) must collapse to one row. Partial index so the common
|
|
37
|
+
-- dedupe-less note (NULL dedupe_key) is unconstrained and always appends.
|
|
38
|
+
CREATE UNIQUE INDEX ux_plan_blackboard_dedupe
|
|
39
|
+
ON plan_blackboard (plan_key, dedupe_key) WHERE dedupe_key IS NOT NULL;
|
|
40
|
+
|
|
41
|
+
-- List a plan's entries in write order (id asc), and map a capability token back to its plan.
|
|
42
|
+
CREATE INDEX ix_plan_blackboard_plan ON plan_blackboard (plan_key, id);
|
|
43
|
+
-- The token is a capability credential, so it must map back to exactly ONE plan. A partial UNIQUE
|
|
44
|
+
-- index (NULLs excluded, so pre-migration plans without a token are unconstrained) enforces the
|
|
45
|
+
-- one-token→one-plan invariant at the DB boundary, and doubles as the token→plan lookup index.
|
|
46
|
+
CREATE UNIQUE INDEX ux_plans_blackboard_token ON plans (blackboard_token) WHERE blackboard_token IS NOT NULL;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Structured scope/impl-change report from implementer agents (D5, issue #55 / #49).
|
|
2
|
+
--
|
|
3
|
+
-- The implementer result contract (prompts/feature.md) gains an optional `delta`: the machine-
|
|
4
|
+
-- readable "I changed the contract / discovered a constraint / this now touches files X / affects
|
|
5
|
+
-- tasks A,B" that previously had nowhere to land but PR prose. One row per (plan, task) — upserted,
|
|
6
|
+
-- so a worker retry or a post-escalation resume overwrites rather than duplicates. `newly_touches`
|
|
7
|
+
-- and `affects_tasks` are JSON-encoded string arrays (feeds D2 conflict-scan + the D4 blackboard).
|
|
8
|
+
CREATE TABLE IF NOT EXISTS plan_task_deltas (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
plan_key TEXT NOT NULL,
|
|
11
|
+
task_id TEXT NOT NULL,
|
|
12
|
+
wave INTEGER,
|
|
13
|
+
contract_change TEXT,
|
|
14
|
+
newly_touches TEXT, -- JSON array of paths, or NULL
|
|
15
|
+
affects_tasks TEXT, -- JSON array of task ids, or NULL
|
|
16
|
+
constraint_note TEXT,
|
|
17
|
+
created_at TEXT NOT NULL,
|
|
18
|
+
updated_at TEXT NOT NULL
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- One delta per (plan, task): the upsert key. A resume/retry replaces the prior report in place.
|
|
22
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ux_plan_task_deltas_task
|
|
23
|
+
ON plan_task_deltas (plan_key, task_id);
|
|
24
|
+
|
|
25
|
+
-- Read a plan's whole report in write order.
|
|
26
|
+
CREATE INDEX IF NOT EXISTS ix_plan_task_deltas_plan
|
|
27
|
+
ON plan_task_deltas (plan_key, id);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- The merge-exclusion graph (D1, issue #57 / #49).
|
|
2
|
+
--
|
|
3
|
+
-- A SECOND relationship, distinct from the dispatch-DAG (`plan_task_deps` → waves). Those edges
|
|
4
|
+
-- gate *starting* a task; these gate *landing* it: two tasks can run in parallel but touch the same
|
|
5
|
+
-- surface, so they can't merge independently (nano-bpm#614: nine slices all appending to
|
|
6
|
+
-- engine/tests.rs). Undirected `(task_a, task_b)` pairs, `task_a < task_b` normalised so a pair has
|
|
7
|
+
-- exactly one row. `files` is the JSON-encoded overlapping path set; `source` records how the edge
|
|
8
|
+
-- was derived (e.g. `file-overlap`). These MUST NEVER enter computeWaves — they order landings only.
|
|
9
|
+
CREATE TABLE IF NOT EXISTS plan_merge_exclusions (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
plan_key TEXT NOT NULL,
|
|
12
|
+
task_a TEXT NOT NULL,
|
|
13
|
+
task_b TEXT NOT NULL,
|
|
14
|
+
files TEXT, -- JSON array of the overlapping paths
|
|
15
|
+
source TEXT NOT NULL,
|
|
16
|
+
created_at TEXT NOT NULL,
|
|
17
|
+
updated_at TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
-- One row per unordered pair on a plan: the upsert key (a refreshed scan updates files in place).
|
|
21
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ux_plan_merge_exclusions_pair
|
|
22
|
+
ON plan_merge_exclusions (plan_key, task_a, task_b);
|
|
23
|
+
|
|
24
|
+
-- Read a plan's whole exclusion graph.
|
|
25
|
+
CREATE INDEX IF NOT EXISTS ix_plan_merge_exclusions_plan
|
|
26
|
+
ON plan_merge_exclusions (plan_key, id);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- Merge-protocol liveness (D9). The frugal-CI fresh-head-run remedy is one-shot per landing
|
|
2
|
+
-- attempt, not per PR lifetime: a rebase creates a new head commit and may need a new synthetic
|
|
3
|
+
-- `pull_request` run even if an earlier head was already nudged.
|
|
4
|
+
ALTER TABLE pull_requests ADD COLUMN fresh_head_run_head TEXT;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- D6 merge-train lane serialization.
|
|
2
|
+
--
|
|
3
|
+
-- Adds the durable meaning of pull_requests.status = 'waiting_lane': a converged PR that is green
|
|
4
|
+
-- but parked behind the current head of its D2 merge-exclusion lane. SQLite has no status enum in
|
|
5
|
+
-- this schema, so no column shape changes are required; the existing idx_pr_status covers polling.
|
|
6
|
+
SELECT 1;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- D3 trial-merge integration gate audit (issue #69).
|
|
2
|
+
CREATE TABLE IF NOT EXISTS plan_trial_merges (
|
|
3
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4
|
+
plan_key TEXT NOT NULL,
|
|
5
|
+
wave INTEGER NOT NULL DEFAULT 0,
|
|
6
|
+
result TEXT NOT NULL CHECK (result IN ('clean', 'merge-conflict', 'suite-failed')),
|
|
7
|
+
heads TEXT,
|
|
8
|
+
conflicts TEXT,
|
|
9
|
+
failing TEXT,
|
|
10
|
+
summary TEXT,
|
|
11
|
+
job_key TEXT,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_plan_trial_merges_plan_wave
|
|
17
|
+
ON plan_trial_merges(plan_key, wave, id);
|
|
18
|
+
|
|
19
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_plan_trial_merges_plan_job
|
|
20
|
+
ON plan_trial_merges(plan_key, job_key)
|
|
21
|
+
WHERE job_key IS NOT NULL;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Cooperative abandon check (issue #76): a per-PR capability token the running agent curls to learn
|
|
2
|
+
-- whether its run was cancelled before it performs a side effect. The abandon state itself is
|
|
3
|
+
-- derived from pull_requests.status (set to 'abandoned' by cancelRun); this only adds the token the
|
|
4
|
+
-- /hooks/abandon endpoint resolves back to the PR.
|
|
5
|
+
ALTER TABLE pull_requests ADD COLUMN abandon_token TEXT;
|
|
6
|
+
|
|
7
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_pull_requests_abandon_token
|
|
8
|
+
ON pull_requests(abandon_token)
|
|
9
|
+
WHERE abandon_token IS NOT NULL;
|
package/deno.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": [
|
|
4
|
+
"deno.window",
|
|
5
|
+
"deno.ns"
|
|
6
|
+
]
|
|
7
|
+
},
|
|
8
|
+
"imports": {
|
|
9
|
+
"@nanobpm/urban": "npm:@nanobpm/urban@^0.24.0"
|
|
10
|
+
},
|
|
11
|
+
"tasks": {
|
|
12
|
+
"start": "deno run --allow-net --allow-read --allow-write --allow-run=gh --allow-env main.ts",
|
|
13
|
+
"purge": "deno run --allow-read --allow-write --allow-env scripts/purge-db.ts",
|
|
14
|
+
"check": "deno run -A @nanobpm/urban check",
|
|
15
|
+
"check:prompts": "deno run -A scripts/check-agent-prompts.ts",
|
|
16
|
+
"gen": "deno run -A @nanobpm/urban gen",
|
|
17
|
+
"gen:check": "deno run -A @nanobpm/urban gen --check",
|
|
18
|
+
"layout": "deno run -A scripts/layout-bpmn.ts",
|
|
19
|
+
"layout:check": "deno run -A scripts/layout-bpmn.ts --check",
|
|
20
|
+
"dev": "deno run -A @nanobpm/urban dev",
|
|
21
|
+
"compile": "deno compile --allow-net --allow-read --allow-write --allow-run=gh --allow-env -o dist/nano-workforce main.ts",
|
|
22
|
+
"test": "deno test -A"
|
|
23
|
+
}
|
|
24
|
+
}
|