@nanobpm/nano-workforce 0.56.0 → 0.58.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 +7 -0
- package/AGENTS.md +83 -1
- package/CHANGELOG.md +14 -0
- package/README.md +10 -2
- package/SPEC.md +27 -24
- package/app/agentCompletion.test.ts +337 -0
- package/app/agentCompletion.ts +219 -0
- package/app/agentGuide.ts +1 -1
- package/app/answer-escalation.test.ts +106 -0
- package/app/answerEscalation.test.ts +67 -0
- package/app/baseGuard.test.ts +9 -1
- package/app/baseGuard.ts +11 -0
- package/app/escalationSla.test.ts +39 -0
- package/app/escalationSla.ts +28 -0
- package/app/escalationTaxonomy.test.ts +115 -0
- package/app/escalationTaxonomy.ts +115 -0
- package/app/feature.test.ts +161 -0
- package/app/feature.ts +173 -0
- package/app/github.test.ts +179 -1
- package/app/github.ts +132 -0
- package/app/mergeProtocol.test.ts +15 -0
- package/app/mergeProtocol.ts +10 -0
- package/app/persist-escalation.test.ts +34 -36
- package/app/plan.test.ts +267 -313
- package/app/plan.ts +173 -231
- package/app/reviewWait.ts +12 -4
- package/app/roundResultDefault.test.ts +111 -2
- package/app/roundResultDefault.ts +35 -0
- package/app/service.test.ts +6 -7
- package/app/service.ts +52 -35
- package/db/migrations/026_agent_completion.sql +36 -0
- package/db/migrations/027_retire_escalation_subsystem.sql +43 -0
- package/db/migrations/028_feature_runs.sql +28 -0
- package/e2e/agent-answerable.e2e.ts +185 -0
- package/e2e/convergence-escalation.e2e.ts +180 -0
- package/e2e/convergence-loop.e2e.ts +1 -1
- package/e2e/feature-run.e2e.ts +231 -0
- package/e2e/plan-fanout-sla.e2e.ts +238 -0
- package/e2e/plan-fanout.e2e.ts +303 -0
- package/e2e/retire-escalation-subsystem.e2e.ts +223 -0
- package/e2e/support/github-admit.ts +99 -0
- package/e2e/user-task-spine.e2e.ts +155 -0
- package/nano.app.json +41 -11
- package/openapi.yaml +237 -84
- package/operations/agentCompleteEscalation.ts +53 -0
- package/operations/listActivePrs.test.ts +39 -6
- package/operations/postMessage.ts +10 -41
- package/operations/revertEscalationCompletion.ts +44 -0
- package/operations/startAndMessage.test.ts +62 -60
- package/operations/startFeature.ts +127 -0
- package/operations/startPlanFanout.admission.integration.test.ts +263 -0
- package/operations/startPlanFanout.ts +70 -11
- package/package.json +4 -1
- package/pages/cockpit.page.json +1 -0
- package/pages/epic-detail.page.json +11 -37
- package/pages/epic.page.json +5 -2
- package/pages/feature.page.json +82 -0
- package/pages/home.page.json +6 -18
- package/resources/agent-guide.md +90 -26
- package/resources/forms/feature-escalation.form +27 -0
- package/resources/forms/plan-review-decision.form +27 -0
- package/resources/forms/pr-escalation.form +23 -0
- package/resources/forms/spine-demo.form +15 -0
- package/resources/forms/trial-merge-decision.form +25 -0
- package/resources/processes/convergence-loop.bpmn +127 -75
- package/resources/processes/feature.bpmn +240 -0
- package/resources/processes/plan-fanout.bpmn +322 -222
- package/resources/processes/spine-demo.bpmn +72 -0
- package/scripts/check-migrations.ts +68 -0
- package/workers/answer-escalation/worker.ts +78 -0
- package/workers/converge-feature/worker.ts +51 -0
- package/workers/ensure-base-branch/head-task.integration.test.ts +126 -0
- package/workers/ensure-base-branch/worker.test.ts +104 -0
- package/workers/ensure-base-branch/worker.ts +31 -0
- package/workers/finalize/worker.ts +0 -2
- package/workers/mark-merged/worker.ts +0 -2
- package/workers/merge/worker.ts +6 -5
- package/workers/persist-escalation/worker.ts +28 -32
- package/workers/record-feature/worker.ts +61 -0
- package/workers/record-plan-review/worker.test.ts +9 -10
- package/workers/record-plan-review/worker.ts +15 -5
- package/workers/resolve-trial-attention/worker.test.ts +77 -0
- package/workers/resolve-trial-attention/worker.ts +43 -0
- package/operations/answerFeatureEscalation.test.ts +0 -112
- package/operations/answerFeatureEscalation.ts +0 -58
- package/operations/answerPlanEscalation.test.ts +0 -115
- package/operations/answerPlanEscalation.ts +0 -41
- package/workers/persist-plan-escalation/worker.test.ts +0 -80
- package/workers/persist-plan-escalation/worker.ts +0 -73
- package/workers/persist-task-escalation/worker.ts +0 -120
|
@@ -10,7 +10,16 @@
|
|
|
10
10
|
// ONE of `issue` or `url` — so an empty or ambiguous target is a 400 at the edge; this delegate just
|
|
11
11
|
// narrows the validated variant and keeps the issue-FORMAT parse guard (schema can't express it).
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { BaseBranchMustExistError } from "../app/github.ts";
|
|
14
|
+
import {
|
|
15
|
+
admitPlan,
|
|
16
|
+
DefaultBaseNotConfirmedError,
|
|
17
|
+
InvalidBaseBranchError,
|
|
18
|
+
MissingBaseBranchError,
|
|
19
|
+
parseIssue,
|
|
20
|
+
SharedBaseError,
|
|
21
|
+
startPlan,
|
|
22
|
+
} from "../app/plan.ts";
|
|
14
23
|
import { defineOperation } from "../nano-generated/operations.ts";
|
|
15
24
|
|
|
16
25
|
export default defineOperation("startPlanFanout", async ({ body }, app) => {
|
|
@@ -26,16 +35,29 @@ export default defineOperation("startPlanFanout", async ({ body }, app) => {
|
|
|
26
35
|
app.log.warn("start-plan rejected: unparseable issue reference", { raw });
|
|
27
36
|
return { status: 400, body: { error: "could not parse issue (use owner/repo#123 or an issue URL)" } };
|
|
28
37
|
}
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
38
|
+
// Epic base branch (ADR 0003): admit the launch through the fail-fast `admitPlan` gate BEFORE any
|
|
39
|
+
// fan-out. It composes the four ordered admission rules — required+explicit, create-if-missing
|
|
40
|
+
// (epic/* guard, run synchronously so a typo is a clean edge 400), confirm-default, and
|
|
41
|
+
// shared-base — and returns the normalized base. Errors map to specific HTTP statuses at the edge.
|
|
42
|
+
const rawBase = "baseBranch" in body && typeof body.baseBranch === "string" ? body.baseBranch : null;
|
|
43
|
+
const allowSharedBase = "allowSharedBase" in body && body.allowSharedBase === true;
|
|
44
|
+
const confirmDefaultBase = "confirmDefaultBase" in body && body.confirmDefaultBase === true;
|
|
45
|
+
const token = process.env.GITHUB_TOKEN ?? "";
|
|
46
|
+
let normalizedBase: string;
|
|
36
47
|
try {
|
|
37
|
-
normalizedBase =
|
|
48
|
+
normalizedBase = await admitPlan(app.data, parsed.repo, rawBase, token, {
|
|
49
|
+
allowSharedBase,
|
|
50
|
+
confirmDefaultBase,
|
|
51
|
+
selfPlanKey: parsed.planKey,
|
|
52
|
+
});
|
|
38
53
|
} catch (err) {
|
|
54
|
+
if (err instanceof MissingBaseBranchError) {
|
|
55
|
+
app.log.warn("start-plan rejected: missing base branch");
|
|
56
|
+
return {
|
|
57
|
+
status: 400,
|
|
58
|
+
body: { error: "baseBranch is required (name the integration branch, e.g. epic/agent-protocol)" },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
39
61
|
if (err instanceof InvalidBaseBranchError) {
|
|
40
62
|
app.log.warn("start-plan rejected: invalid base branch", { baseBranch: err.value });
|
|
41
63
|
return {
|
|
@@ -43,13 +65,50 @@ export default defineOperation("startPlanFanout", async ({ body }, app) => {
|
|
|
43
65
|
body: { error: "invalid baseBranch (must be a plausible git branch name, e.g. epic/agent-protocol)" },
|
|
44
66
|
};
|
|
45
67
|
}
|
|
68
|
+
if (err instanceof BaseBranchMustExistError) {
|
|
69
|
+
app.log.warn("start-plan rejected: base branch does not exist", { baseBranch: err.branch });
|
|
70
|
+
return {
|
|
71
|
+
status: 400,
|
|
72
|
+
body: {
|
|
73
|
+
error:
|
|
74
|
+
`baseBranch "${err.branch}" does not exist and is not an epic/* branch, so it is not ` +
|
|
75
|
+
`auto-created — create it first, or use the epic/* convention`,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (err instanceof DefaultBaseNotConfirmedError) {
|
|
80
|
+
app.log.warn("start-plan rejected: default base not confirmed", { baseBranch: err.branch });
|
|
81
|
+
return {
|
|
82
|
+
status: 400,
|
|
83
|
+
body: {
|
|
84
|
+
error:
|
|
85
|
+
`baseBranch "${err.branch}" is the repository default branch — every task would land ` +
|
|
86
|
+
`directly on it with no integration branch. Re-submit with confirmDefaultBase: true to proceed`,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (err instanceof SharedBaseError) {
|
|
91
|
+
app.log.warn("start-plan rejected: shared base branch", { baseBranch: err.branch });
|
|
92
|
+
return {
|
|
93
|
+
status: 409,
|
|
94
|
+
body: {
|
|
95
|
+
error:
|
|
96
|
+
`baseBranch "${err.branch}" is already in use by another active epic. Re-submit with ` +
|
|
97
|
+
`allowSharedBase: true to stack on it, or name a distinct epic/* branch`,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
46
101
|
throw err;
|
|
47
102
|
}
|
|
48
103
|
const result = await startPlan(app.data, app.engine, parsed, normalizedBase);
|
|
104
|
+
const alreadyRunning = "alreadyRunning" in result && result.alreadyRunning === true;
|
|
49
105
|
app.log.info("plan fan-out started", {
|
|
50
106
|
planKey: parsed.planKey,
|
|
51
|
-
|
|
52
|
-
|
|
107
|
+
// The base the caller requested. When `alreadyRunning`, `startPlan` short-circuits before this
|
|
108
|
+
// base takes effect (it may not match the in-flight plan's persisted base), so name it as the
|
|
109
|
+
// request — not the effective base — to keep the log honest.
|
|
110
|
+
requestedBaseBranch: normalizedBase,
|
|
111
|
+
alreadyRunning,
|
|
53
112
|
});
|
|
54
113
|
return { status: 202, body: result };
|
|
55
114
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
"typecheck": "tsc --noEmit",
|
|
36
36
|
"pretypecheck": "urban gen",
|
|
37
37
|
"check:prompts": "node --experimental-strip-types scripts/check-agent-prompts.ts",
|
|
38
|
+
"check:migrations": "node --experimental-strip-types scripts/check-migrations.ts",
|
|
38
39
|
"gen": "urban gen",
|
|
39
40
|
"gen:check": "urban gen --check",
|
|
40
41
|
"layout": "node --experimental-strip-types scripts/layout-bpmn.ts",
|
|
41
42
|
"layout:check": "node --experimental-strip-types scripts/layout-bpmn.ts --check",
|
|
42
43
|
"dev": "urban dev",
|
|
44
|
+
"pretest": "urban gen",
|
|
43
45
|
"test": "node --experimental-strip-types --test",
|
|
46
|
+
"pree2e": "urban gen",
|
|
44
47
|
"e2e": "node --experimental-strip-types --test \"e2e/**/*.e2e.ts\"",
|
|
45
48
|
"lint": "biome check app operations workers pages components scripts e2e main.ts",
|
|
46
49
|
"lint:fix": "biome check --write app operations workers pages components scripts e2e main.ts"
|
package/pages/cockpit.page.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"items": [
|
|
13
13
|
{ "label": "Convergence", "page": "home" },
|
|
14
14
|
{ "label": "Epics", "page": "epic" },
|
|
15
|
+
{ "label": "Feature", "page": "feature" },
|
|
15
16
|
{ "label": "Cockpit", "page": "cockpit" }
|
|
16
17
|
]
|
|
17
18
|
}
|
|
@@ -29,6 +30,14 @@
|
|
|
29
30
|
"variant": "sub"
|
|
30
31
|
}
|
|
31
32
|
},
|
|
33
|
+
{
|
|
34
|
+
"type": "text",
|
|
35
|
+
"id": "escalations-pointer",
|
|
36
|
+
"props": {
|
|
37
|
+
"text": "Escalations (task, plan-review, trial-merge) are native user tasks — answer them from the Task inbox at /tasks: list the open tasks, pick the one for this epic, and submit the typed decision. There is no separate answer form on this page.",
|
|
38
|
+
"variant": "sub"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
32
41
|
{
|
|
33
42
|
"type": "dataGrid",
|
|
34
43
|
"id": "epic-plan",
|
|
@@ -48,51 +57,16 @@
|
|
|
48
57
|
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
49
58
|
{ "field": "wave_label", "header": "Wave" },
|
|
50
59
|
{ "field": "task_count", "header": "Tasks" },
|
|
51
|
-
{ "field": "open_task_id", "header": "Open escalation" },
|
|
52
|
-
{ "field": "open_plan_round", "header": "Plan escalation round" },
|
|
53
60
|
{ "field": "updated_at", "header": "Updated" }
|
|
54
61
|
],
|
|
55
|
-
"rowActions": [
|
|
56
|
-
{
|
|
57
|
-
"label": "Proceed with plan",
|
|
58
|
-
"confirm": "Dispatch the current unapproved plan as a human override?",
|
|
59
|
-
"showWhenField": "open_plan_escalation_id",
|
|
60
|
-
"action": {
|
|
61
|
-
"path": "/app/api/actions/message",
|
|
62
|
-
"body": {
|
|
63
|
-
"name": "plan-escalation-answered",
|
|
64
|
-
"correlationKey": "{{row.plan_key}}",
|
|
65
|
-
"variables": { "directive": "proceed", "note": "Proceed override from the epic page." }
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
],
|
|
70
62
|
"detail": {
|
|
71
63
|
"linkField": "issue_url",
|
|
72
64
|
"fields": [
|
|
73
65
|
{ "field": "repo", "label": "Repository" },
|
|
74
66
|
{ "field": "issue_number", "label": "Issue number" },
|
|
75
67
|
{ "field": "base_branch", "label": "Base branch (blank = repo default)" },
|
|
76
|
-
{ "field": "outcome", "label": "Outcome" }
|
|
77
|
-
|
|
78
|
-
{ "field": "open_task_question", "label": "Open escalation question" }
|
|
79
|
-
],
|
|
80
|
-
"form": {
|
|
81
|
-
"showWhenField": "open_plan_escalation_id",
|
|
82
|
-
"title": "Answer the open plan-review escalation",
|
|
83
|
-
"promptField": "open_plan_findings",
|
|
84
|
-
"inputKey": "note",
|
|
85
|
-
"inputLabel": "Revision directive note for the planner",
|
|
86
|
-
"submitLabel": "Revise plan",
|
|
87
|
-
"action": {
|
|
88
|
-
"path": "/app/api/actions/message",
|
|
89
|
-
"body": {
|
|
90
|
-
"name": "plan-escalation-answered",
|
|
91
|
-
"correlationKey": "{{row.plan_key}}",
|
|
92
|
-
"variables": { "directive": "revise", "note": "{{form.note}}" }
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
68
|
+
{ "field": "outcome", "label": "Outcome" }
|
|
69
|
+
]
|
|
96
70
|
}
|
|
97
71
|
}
|
|
98
72
|
},
|
package/pages/epic.page.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"items": [
|
|
13
13
|
{ "label": "Convergence", "page": "home" },
|
|
14
14
|
{ "label": "Epics", "page": "epic" },
|
|
15
|
+
{ "label": "Feature", "page": "feature" },
|
|
15
16
|
{ "label": "Cockpit", "page": "cockpit" }
|
|
16
17
|
]
|
|
17
18
|
}
|
|
@@ -38,7 +39,9 @@
|
|
|
38
39
|
"action": { "path": "/app/api/actions/start/plan-fanout", "body": "{{form}}" },
|
|
39
40
|
"fields": [
|
|
40
41
|
{ "key": "issue", "label": "owner/repo#123 or a GitHub issue URL", "type": "text" },
|
|
41
|
-
{ "key": "baseBranch", "label": "Base branch (
|
|
42
|
+
{ "key": "baseBranch", "label": "Base branch (REQUIRED; e.g. epic/agent-protocol to land the whole epic on an integration branch). A missing epic/* branch is auto-created off default HEAD; a non-epic/* branch must already exist.", "type": "text" },
|
|
43
|
+
{ "key": "confirmDefaultBase", "label": "Confirm landing on the default branch \u2014 required only when the base above IS the repository default (every task lands directly on it, with any merge-to-default side effect firing per task)", "type": "checkbox" },
|
|
44
|
+
{ "key": "allowSharedBase", "label": "Allow sharing a custom integration branch with another active epic \u2014 required only when another in-flight epic already targets this same custom base", "type": "checkbox" }
|
|
42
45
|
]
|
|
43
46
|
}
|
|
44
47
|
},
|
|
@@ -70,9 +73,9 @@
|
|
|
70
73
|
"columns": [
|
|
71
74
|
{ "field": "plan_key", "header": "Epic", "link": { "kind": "page", "page": "epic-detail", "keyField": "plan_key" } },
|
|
72
75
|
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
76
|
+
{ "field": "base_branch", "header": "Base branch" },
|
|
73
77
|
{ "field": "wave_label", "header": "Wave" },
|
|
74
78
|
{ "field": "task_count", "header": "Tasks" },
|
|
75
|
-
{ "field": "open_plan_findings", "header": "Attention", "badge": { "tone": "danger", "label": "!" } },
|
|
76
79
|
{ "field": "issue_number", "header": "Issue", "linkField": "issue_url" },
|
|
77
80
|
{ "field": "updated_at", "header": "Updated" }
|
|
78
81
|
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"title": "Feature Run",
|
|
4
|
+
"nodes": [
|
|
5
|
+
{
|
|
6
|
+
"type": "nav",
|
|
7
|
+
"id": "nav",
|
|
8
|
+
"props": {
|
|
9
|
+
"variant": "bar",
|
|
10
|
+
"sticky": true,
|
|
11
|
+
"title": "Nano Workforce",
|
|
12
|
+
"items": [
|
|
13
|
+
{ "label": "Convergence", "page": "home" },
|
|
14
|
+
{ "label": "Epics", "page": "epic" },
|
|
15
|
+
{ "label": "Feature", "page": "feature" },
|
|
16
|
+
{ "label": "Cockpit", "page": "cockpit" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "text",
|
|
22
|
+
"id": "title",
|
|
23
|
+
"props": { "text": "Single-issue feature run", "variant": "heading" }
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"type": "text",
|
|
27
|
+
"id": "subtitle",
|
|
28
|
+
"props": {
|
|
29
|
+
"text": "Hand one issue to a single implementation agent — it raises exactly one PR. Optionally converge (review rounds) and auto-merge as follow-on steps. The missing middle between Epics (many PRs) and PR convergence (an already-open PR).",
|
|
30
|
+
"variant": "sub"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"type": "actionForm",
|
|
35
|
+
"id": "feature-submit",
|
|
36
|
+
"props": {
|
|
37
|
+
"title": "Implement one issue",
|
|
38
|
+
"submitLabel": "Implement & raise PR",
|
|
39
|
+
"action": { "path": "/app/api/actions/start/feature", "body": "{{form}}" },
|
|
40
|
+
"fields": [
|
|
41
|
+
{ "key": "issue", "label": "owner/repo#123 or a GitHub issue URL", "type": "text" },
|
|
42
|
+
{ "key": "baseBranch", "label": "Base branch (REQUIRED; the branch the PR targets, e.g. main). A missing epic/* branch is auto-created off default HEAD; a non-epic/* branch must already exist.", "type": "text" },
|
|
43
|
+
{ "key": "converge", "label": "Converge \u2014 hand the raised PR to the review-convergence loop", "type": "checkbox" },
|
|
44
|
+
{ "key": "autoMerge", "label": "Auto-merge \u2014 after convergence, drive the merge-loop (only applies when Converge is on)", "type": "checkbox" },
|
|
45
|
+
{ "key": "confirmDefaultBase", "label": "Confirm landing on the default branch \u2014 required only when the base above IS the repository default", "type": "checkbox" },
|
|
46
|
+
{ "key": "allowSharedBase", "label": "Allow sharing a custom integration branch with another active epic", "type": "checkbox" }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"type": "dataGrid",
|
|
52
|
+
"id": "feature-runs",
|
|
53
|
+
"props": {
|
|
54
|
+
"title": "Feature runs",
|
|
55
|
+
"rowKey": "feature_key",
|
|
56
|
+
"refreshMs": 5000,
|
|
57
|
+
"data": {
|
|
58
|
+
"kind": "datasource",
|
|
59
|
+
"source": "app",
|
|
60
|
+
"table": "feature_runs",
|
|
61
|
+
"orderBy": { "field": "updated_at", "dir": "desc" },
|
|
62
|
+
"filter": [{ "field": "status", "in": ["running"] }]
|
|
63
|
+
},
|
|
64
|
+
"tabs": [
|
|
65
|
+
{ "label": "Active", "filter": [{ "field": "status", "in": ["running"] }] },
|
|
66
|
+
{ "label": "History", "filter": [{ "field": "status", "in": ["opened", "converging", "blocked", "skipped", "failed", "abandoned"] }] },
|
|
67
|
+
{ "label": "All", "filter": [] }
|
|
68
|
+
],
|
|
69
|
+
"columns": [
|
|
70
|
+
{ "field": "feature_key", "header": "Feature", "linkField": "issue_url" },
|
|
71
|
+
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
72
|
+
{ "field": "base_branch", "header": "Base branch" },
|
|
73
|
+
{ "field": "pr_key", "header": "PR", "link": { "kind": "page", "page": "home", "keyField": "pr_key" } },
|
|
74
|
+
{ "field": "converge", "header": "Converge" },
|
|
75
|
+
{ "field": "auto_merge", "header": "Auto-merge" },
|
|
76
|
+
{ "field": "outcome", "header": "Outcome" },
|
|
77
|
+
{ "field": "updated_at", "header": "Updated" }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
package/pages/home.page.json
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"label": "Epics",
|
|
18
18
|
"page": "epic"
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
"label": "Feature",
|
|
22
|
+
"page": "feature"
|
|
23
|
+
},
|
|
20
24
|
{
|
|
21
25
|
"label": "Cockpit",
|
|
22
26
|
"page": "cockpit"
|
|
@@ -37,7 +41,7 @@
|
|
|
37
41
|
"type": "text",
|
|
38
42
|
"id": "subtitle",
|
|
39
43
|
"props": {
|
|
40
|
-
"text": "Submit a pull request to run the autonomous review-convergence loop. Answer escalations
|
|
44
|
+
"text": "Submit a pull request to run the autonomous review-convergence loop. Answer escalations from the Task inbox at /tasks; cancel a run at any time.",
|
|
41
45
|
"variant": "sub"
|
|
42
46
|
}
|
|
43
47
|
},
|
|
@@ -329,23 +333,7 @@
|
|
|
329
333
|
}
|
|
330
334
|
]
|
|
331
335
|
}
|
|
332
|
-
]
|
|
333
|
-
"form": {
|
|
334
|
-
"showWhenField": "open_escalation_id",
|
|
335
|
-
"title": "Answer the open escalation",
|
|
336
|
-
"promptField": "open_escalation_question",
|
|
337
|
-
"inputKey": "answer",
|
|
338
|
-
"inputLabel": "Your answer",
|
|
339
|
-
"submitLabel": "Send answer",
|
|
340
|
-
"action": {
|
|
341
|
-
"path": "/app/api/actions/message",
|
|
342
|
-
"body": {
|
|
343
|
-
"name": "escalation-answered",
|
|
344
|
-
"correlationKey": "{{row.pr_key}}",
|
|
345
|
-
"variables": "{{form}}"
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
336
|
+
]
|
|
349
337
|
}
|
|
350
338
|
}
|
|
351
339
|
}
|
package/resources/agent-guide.md
CHANGED
|
@@ -97,13 +97,49 @@ PR is then enrolled into its own convergence loop (§1).
|
|
|
97
97
|
```bash
|
|
98
98
|
curl -sS -X POST __BASE__/actions/start/plan-fanout \
|
|
99
99
|
-H 'content-type: application/json' \
|
|
100
|
-
-d '{ "issue": "owner/repo#123" }'
|
|
100
|
+
-d '{ "issue": "owner/repo#123", "baseBranch": "epic/agent-protocol" }'
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
The body is flat: `issue` (or `url`) — `owner/repo#123` or an issue URL
|
|
103
|
+
The body is flat: `issue` (or `url`) — `owner/repo#123` or an issue URL — plus a REQUIRED
|
|
104
|
+
`baseBranch` (ADR 0003), the branch the fleet branches off and opens every PR against; a
|
|
105
|
+
blank/absent base is rejected with a 400. Starting a
|
|
104
106
|
plan is idempotent on the plan key; an already-running plan short-circuits. The
|
|
105
107
|
response (202) echoes the `planKey` and engine `processKey`.
|
|
106
108
|
|
|
109
|
+
### Base-branch admission (ADR 0003)
|
|
110
|
+
|
|
111
|
+
`startPlanFanout` admits the base through one fail-fast gate before any task fans out.
|
|
112
|
+
Four ordered rules govern which base is accepted:
|
|
113
|
+
|
|
114
|
+
1. **Required + explicit.** `baseBranch` is mandatory — a blank/absent value is a `400`
|
|
115
|
+
(`MissingBaseBranchError`), and an implausible name is a `400` (`InvalidBaseBranchError`).
|
|
116
|
+
There is no silent "land on the default branch" fallback.
|
|
117
|
+
2. **Create-if-missing, `epic/*` only.** A missing `epic/*` base is **auto-created** off the
|
|
118
|
+
repository default branch's HEAD (idempotently — an existing branch is never reset). A
|
|
119
|
+
missing base that is **not** `epic/*` is a clean `400` (`BaseBranchMustExistError`): a typo
|
|
120
|
+
can't silently spawn a wrong-rooted branch, so any non-`epic/*` base must already exist.
|
|
121
|
+
3. **Confirm-default.** Naming the repository **default branch** as the base requires
|
|
122
|
+
`confirmDefaultBase: true`, else `400` (`DefaultBaseNotConfirmedError`). This is a
|
|
123
|
+
deliberate acknowledgement that every task lands directly on the default branch with no
|
|
124
|
+
integration buffer, and any merge-to-default side effect fires per task.
|
|
125
|
+
4. **Shared-base guard.** If another **active** epic (status not `done`/`failed`/`abandoned`)
|
|
126
|
+
already targets the **same repo + same custom base**, admission is a `409` (`SharedBaseError`)
|
|
127
|
+
unless you pass `allowSharedBase: true`. The default branch is exempt — many epics target it
|
|
128
|
+
concurrently without colliding.
|
|
129
|
+
|
|
130
|
+
So the body may also carry two optional booleans — `confirmDefaultBase` and `allowSharedBase` —
|
|
131
|
+
each a "warn you can't skip" for its rule:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
curl -sS -X POST __BASE__/actions/start/plan-fanout \
|
|
135
|
+
-H 'content-type: application/json' \
|
|
136
|
+
-d '{ "issue": "owner/repo#123", "baseBranch": "main", "confirmDefaultBase": true }'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Grandfathered: in-flight plans launched before this admission gate (with a `null` base branch)
|
|
140
|
+
keep running unchanged — the requirement is enforced at admission of **new** launches, not by a
|
|
141
|
+
database constraint.
|
|
142
|
+
|
|
107
143
|
Track a plan the same way you track PRs — its `process_key` is an engine instance you
|
|
108
144
|
can inspect in §5, and the PRs it opens show up in `/status` as ordinary convergence
|
|
109
145
|
loops.
|
|
@@ -124,43 +160,71 @@ curl -sS __BASE__/status | jq '.prs[] | select(.openEscalation != null)
|
|
|
124
160
|
| { prKey, status, round, openEscalation }'
|
|
125
161
|
```
|
|
126
162
|
|
|
127
|
-
|
|
128
|
-
|
|
163
|
+
The four decision-required escalation kinds — **PR review-loop**, **implementation
|
|
164
|
+
(feature) task**, **plan-review**, and **trial-merge** — are now native BPMN
|
|
165
|
+
`userTask`s bearing a linked `.form`, all answered the same way through the **task
|
|
166
|
+
inbox** surface. There is no bespoke per-kind webhook or answer page any more.
|
|
167
|
+
|
|
168
|
+
**List the open escalation tasks.** Each task carries its context (e.g. `prKey` /
|
|
169
|
+
`question` / `findings` / `task`) in its `variables`, and its kind in `elementId`:
|
|
129
170
|
|
|
130
171
|
```bash
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}'
|
|
172
|
+
# Every parked escalation, across all kinds:
|
|
173
|
+
curl -sS __BASE__/../../tasks/api/tasks | jq '.[] | { userTaskKey, elementId, variables }'
|
|
174
|
+
|
|
175
|
+
# Filter to one kind (e.g. plan-review decisions) by elementId:
|
|
176
|
+
curl -sS __BASE__/../../tasks/api/tasks \
|
|
177
|
+
| jq '[.[] | select(.elementId == "plan-review-decision")]'
|
|
138
178
|
```
|
|
139
179
|
|
|
140
|
-
The
|
|
141
|
-
|
|
180
|
+
The inbox UI is also served at `__BASE__/../../tasks` for a human to browse, filter, and
|
|
181
|
+
answer (assignee/candidate-group and age surface on each task once assignment lands).
|
|
142
182
|
|
|
143
|
-
**Answer
|
|
144
|
-
|
|
183
|
+
**Answer a task** by completing it with the typed variables its form expects — the
|
|
184
|
+
completion resumes the parked process:
|
|
145
185
|
|
|
146
186
|
```bash
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
-d '{ "
|
|
187
|
+
# PR review-loop (elementId `wait-answer`, pr-escalation form):
|
|
188
|
+
curl -sS -X POST __BASE__/../../tasks/api/complete -H 'content-type: application/json' \
|
|
189
|
+
-d '{ "userTaskKey": "<key>", "variables": { "answer": "Cap retries at 5 and proceed." } }'
|
|
190
|
+
|
|
191
|
+
# Implementation (feature) task (elementId `feature-escalation`):
|
|
192
|
+
# { "resolution": "answer", "answer": "…" } to resume, or { "resolution": "abandon" }
|
|
193
|
+
curl -sS -X POST __BASE__/../../tasks/api/complete -H 'content-type: application/json' \
|
|
194
|
+
-d '{ "userTaskKey": "<key>", "variables": { "resolution": "answer", "answer": "Use v2." } }'
|
|
195
|
+
|
|
196
|
+
# Plan-review (elementId `plan-review-decision`):
|
|
197
|
+
# { "directive": "revise", "notes": "…" } (fresh review budget) or { "directive": "proceed" }
|
|
198
|
+
curl -sS -X POST __BASE__/../../tasks/api/complete -H 'content-type: application/json' \
|
|
199
|
+
-d '{ "userTaskKey": "<key>", "variables": { "directive": "revise", "notes": "Make issue-7 the seam." } }'
|
|
200
|
+
|
|
201
|
+
# Trial-merge (elementId `trial-merge-decision`):
|
|
202
|
+
# { "action": "proceed" | "rebase" | "abandon", "notes"?: "…" }
|
|
203
|
+
curl -sS -X POST __BASE__/../../tasks/api/complete -H 'content-type: application/json' \
|
|
204
|
+
-d '{ "userTaskKey": "<key>", "variables": { "action": "rebase", "notes": "Re-run after the fix." } }'
|
|
150
205
|
```
|
|
151
206
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
converge within its round budget. Use `revise` to send guidance back to the planner
|
|
156
|
-
with a fresh review budget, or `proceed` to explicitly approve the current plan as-is:
|
|
207
|
+
**Answer a merge-loop escalation** (the one out-of-scope kind that still uses the
|
|
208
|
+
durable message catch, not a user task). Use the message name `escalation-answered`,
|
|
209
|
+
correlated by the PR key:
|
|
157
210
|
|
|
158
211
|
```bash
|
|
159
|
-
curl -sS -X POST __BASE__/
|
|
212
|
+
curl -sS -X POST __BASE__/actions/message \
|
|
160
213
|
-H 'content-type: application/json' \
|
|
161
|
-
-d '{
|
|
214
|
+
-d '{
|
|
215
|
+
"name": "escalation-answered",
|
|
216
|
+
"correlationKey": "owner/repo#123",
|
|
217
|
+
"variables": { "answer": "Yes — cap the retries at 5 and proceed." }
|
|
218
|
+
}'
|
|
162
219
|
```
|
|
163
220
|
|
|
221
|
+
If `NANO_PR_WEBHOOK_SECRET` is set on the deployment, add `-H "x-hook-secret: <secret>"`.
|
|
222
|
+
|
|
223
|
+
The answer is delivered to the agent as its next-round context (e.g. the `answer`,
|
|
224
|
+
`directive`, or `action` variable), and the loop resumes. **Audit** is durable: the
|
|
225
|
+
completed user tasks form the escalation history, and the review/merge loops' rows
|
|
226
|
+
live in the `escalations` table (surfaced read-only per PR on the Convergence page).
|
|
227
|
+
|
|
164
228
|
Guidance for the human you assist: read the escalation `question` or plan-review
|
|
165
229
|
`findings` first, decide the smallest unblocking answer, and answer it precisely —
|
|
166
230
|
the answer becomes the agent's next-round context.
|
|
@@ -173,7 +237,7 @@ the answer becomes the agent's next-round context.
|
|
|
173
237
|
submit ──► convergence-loop
|
|
174
238
|
round (senior:pr-review) ──► addressed ──► wait review-ready ─┐
|
|
175
239
|
├─ converged ──► finalize ──► merge-loop (unless convergeOnly)
|
|
176
|
-
└─ needs_input/blocked ──► escalate ──► wait
|
|
240
|
+
└─ needs_input/blocked ──► escalate ──► wait-answer userTask (task inbox)
|
|
177
241
|
merge-loop: wait deps ─► arm merge ─► (queue-aware) merge / land
|
|
178
242
|
blocked (CI red) ─► senior:fix-ci ─► retry conflict ─► senior:rebase ─► retry
|
|
179
243
|
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "feature-escalation",
|
|
3
|
+
"schemaVersion": 18,
|
|
4
|
+
"type": "default",
|
|
5
|
+
"components": [
|
|
6
|
+
{
|
|
7
|
+
"type": "select",
|
|
8
|
+
"key": "resolution",
|
|
9
|
+
"label": "Resolution",
|
|
10
|
+
"values": [
|
|
11
|
+
{ "label": "Answer — re-dispatch the task with this guidance", "value": "answer" },
|
|
12
|
+
{ "label": "Abandon — give up on this task", "value": "abandon" }
|
|
13
|
+
],
|
|
14
|
+
"validate": {
|
|
15
|
+
"required": true
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "textarea",
|
|
20
|
+
"key": "answer",
|
|
21
|
+
"label": "Answer / guidance for the implementation agent",
|
|
22
|
+
"conditional": {
|
|
23
|
+
"hide": "=resolution != \"answer\""
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "plan-review-decision",
|
|
3
|
+
"schemaVersion": 18,
|
|
4
|
+
"type": "default",
|
|
5
|
+
"components": [
|
|
6
|
+
{
|
|
7
|
+
"type": "select",
|
|
8
|
+
"key": "directive",
|
|
9
|
+
"label": "Directive",
|
|
10
|
+
"values": [
|
|
11
|
+
{ "label": "Proceed — dispatch the current plan as-is", "value": "proceed" },
|
|
12
|
+
{ "label": "Revise — send the plan back to the planner", "value": "revise" }
|
|
13
|
+
],
|
|
14
|
+
"validate": {
|
|
15
|
+
"required": true
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "textarea",
|
|
20
|
+
"key": "notes",
|
|
21
|
+
"label": "Revision guidance for the planner",
|
|
22
|
+
"conditional": {
|
|
23
|
+
"hide": "=directive != \"revise\""
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "pr-escalation",
|
|
3
|
+
"schemaVersion": 18,
|
|
4
|
+
"type": "default",
|
|
5
|
+
"components": [
|
|
6
|
+
{
|
|
7
|
+
"type": "textarea",
|
|
8
|
+
"key": "question",
|
|
9
|
+
"label": "Escalation question",
|
|
10
|
+
"description": "A human decision is needed to resume the PR review-convergence loop.",
|
|
11
|
+
"readonly": true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"type": "textarea",
|
|
15
|
+
"key": "answer",
|
|
16
|
+
"label": "Your answer",
|
|
17
|
+
"description": "Your reply resumes the review loop and is handed to the next review round.",
|
|
18
|
+
"validate": {
|
|
19
|
+
"required": true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|