@nanobpm/nano-workforce 0.48.1 → 0.49.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/CHANGELOG.md +14 -0
- package/app/mergeRebaseArm.test.ts +63 -0
- package/app/plan.ts +12 -0
- package/db/migrations/022_plan_wave_progress.sql +24 -0
- package/package.json +2 -2
- package/pages/epic-detail.page.json +314 -0
- package/pages/epic.page.json +8 -235
- package/pages/home.page.json +219 -47
- package/resources/processes/merge-loop.bpmn +76 -48
- package/scripts/pages-contract.test.ts +8 -5
- package/workers/record-plan/worker.test.ts +89 -0
- package/workers/record-plan/worker.ts +8 -0
- package/workers/record-wave/worker.test.ts +56 -0
- package/workers/record-wave/worker.ts +14 -0
- package/workers/select-wave/worker.test.ts +33 -2
- package/workers/select-wave/worker.ts +23 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [0.49.0](https://github.com/nanobpm/nano-workforce/compare/v0.48.2...v0.49.0) (2026-08-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **ui:** per-epic operator workspace with live wave progress ([#138](https://github.com/nanobpm/nano-workforce/issues/138)) ([127888d](https://github.com/nanobpm/nano-workforce/commit/127888d5d2b66e1e9d1c85d015cbd65f3696216f)), closes [#137](https://github.com/nanobpm/nano-workforce/issues/137) [nanobpm/nano-workforce#137](https://github.com/nanobpm/nano-workforce/issues/137) [nanobpm/nano-ide#213](https://github.com/nanobpm/nano-ide/issues/213) [nanobpm/nano-ide#214](https://github.com/nanobpm/nano-ide/issues/214)
|
|
7
|
+
|
|
8
|
+
## [0.48.2](https://github.com/nanobpm/nano-workforce/compare/v0.48.1...v0.48.2) (2026-08-12)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **merge-loop:** reconcile from ground truth instead of escalating on a missing agent status ([#135](https://github.com/nanobpm/nano-workforce/issues/135)) ([6861cbe](https://github.com/nanobpm/nano-workforce/commit/6861cbe160d0aebb903924f5aaf67f9c90adac6d)), closes [#134](https://github.com/nanobpm/nano-workforce/issues/134)
|
|
14
|
+
|
|
1
15
|
## [0.48.1](https://github.com/nanobpm/nano-workforce/compare/v0.48.0...v0.48.1) (2026-08-12)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -29,6 +29,26 @@ function hasFlow(source: string, target: string): boolean {
|
|
|
29
29
|
return re.test(flat);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Assert a specific `<sequenceFlow>` (matched by id) has the given source/target, regardless of
|
|
33
|
+
// attribute order. Unlike `hasFlow`, this pins the *named* flow so a test can't be satisfied by a
|
|
34
|
+
// sibling flow that happens to share the same source/target (e.g. a success arm masking a default).
|
|
35
|
+
function flowHasId(id: string, source: string, target: string): boolean {
|
|
36
|
+
const m = flat.match(new RegExp(`<bpmn:sequenceFlow\\b[^>]*\\bid="${id}"[^>]*/?>`));
|
|
37
|
+
if (!m) return false;
|
|
38
|
+
const tag = m[0];
|
|
39
|
+
return tag.includes(`sourceRef="${source}"`) && tag.includes(`targetRef="${target}"`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Assert an `<exclusiveGateway>` (matched by id) declares the given `default` flow, regardless of
|
|
43
|
+
// attribute order. Matching the start tag by id and reading `default` from it keeps the guard
|
|
44
|
+
// robust to harmless XML reformatting (attribute reordering / wrapping) that a fixed-order literal
|
|
45
|
+
// substring would spuriously trip on.
|
|
46
|
+
function gatewayDefault(id: string, def: string): boolean {
|
|
47
|
+
const m = flat.match(new RegExp(`<bpmn:exclusiveGateway\\b[^>]*\\bid="${id}"[^>]*>`));
|
|
48
|
+
if (!m) return false;
|
|
49
|
+
return m[0].includes(`default="${def}"`);
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
test("conflict routes to the rebase budget gate, not straight to a human", () => {
|
|
33
53
|
// The conflict verdict must reach the auto-rebase gate…
|
|
34
54
|
assert(hasFlow("gw-mergeable", "gw-rebase"), "gw-mergeable → gw-rebase (conflict) missing");
|
|
@@ -62,6 +82,49 @@ test("rebase result: success re-arms the poller; unresolved escalates to a human
|
|
|
62
82
|
);
|
|
63
83
|
});
|
|
64
84
|
|
|
85
|
+
test("rebase result: a missing/ambiguous status reconciles from ground truth, not escalation (#134)", () => {
|
|
86
|
+
// #134 / Magikcraft/nano-bpm#751: the rebase agent resolved everything and the PR became
|
|
87
|
+
// MERGEABLE, but it emitted no machine-readable `status`, so the old default arm escalated a
|
|
88
|
+
// PR that was already landable. Ground truth is authoritative: the default (no-verdict) arm
|
|
89
|
+
// now re-arms the merge poller, which re-checks `gw-mergeable` from GitHub state — bounded by
|
|
90
|
+
// the existing rebaseMax budget — instead of pulling in a human.
|
|
91
|
+
assert(
|
|
92
|
+
gatewayDefault("gw-rebase-result", "f_reb_reconcile"),
|
|
93
|
+
'gw-rebase-result must default to f_reb_reconcile (reconcile)',
|
|
94
|
+
);
|
|
95
|
+
// Pin the *default* reconcile flow itself, not just any gw-rebase-result → arm-merge edge (the
|
|
96
|
+
// success arm `f_reb_rebased` shares that target), so a mis-wired default can't pass silently.
|
|
97
|
+
assert(
|
|
98
|
+
flowHasId("f_reb_reconcile", "gw-rebase-result", "arm-merge"),
|
|
99
|
+
"f_reb_reconcile must default gw-rebase-result → arm-merge (reconcile)",
|
|
100
|
+
);
|
|
101
|
+
// Escalation is now reserved for the agent's explicit "I cannot proceed" verdict.
|
|
102
|
+
assertStringIncludes(flat, 'id="f_reb_blocked"');
|
|
103
|
+
const rebBlocked = flat.match(/<bpmn:sequenceFlow[^>]*id="f_reb_blocked"[\s\S]*?<\/bpmn:sequenceFlow>/);
|
|
104
|
+
assert(rebBlocked, "f_reb_blocked flow missing");
|
|
105
|
+
assertStringIncludes(rebBlocked![0], 'status = "blocked"');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("ci-fix result: a missing/ambiguous status reconciles from ground truth, not escalation (#134)", () => {
|
|
109
|
+
// Symmetric to the rebase arm: the CI-fix agent's no-verdict default re-arms the merge poller
|
|
110
|
+
// (ground-truth re-check, bounded by ciFixMax) rather than escalating a possibly-green PR.
|
|
111
|
+
assert(
|
|
112
|
+
gatewayDefault("gw-ci-result", "f_ci_reconcile"),
|
|
113
|
+
'gw-ci-result must default to f_ci_reconcile (reconcile)',
|
|
114
|
+
);
|
|
115
|
+
// Pin the *default* reconcile flow itself, not just any gw-ci-result → arm-merge edge (the
|
|
116
|
+
// success arm `f_ci_fixed` shares that target), so a mis-wired default can't pass silently.
|
|
117
|
+
assert(
|
|
118
|
+
flowHasId("f_ci_reconcile", "gw-ci-result", "arm-merge"),
|
|
119
|
+
"f_ci_reconcile must default gw-ci-result → arm-merge (reconcile)",
|
|
120
|
+
);
|
|
121
|
+
// Escalation reserved for the agent's explicit `blocked` verdict.
|
|
122
|
+
const ciBlocked = flat.match(/<bpmn:sequenceFlow[^>]*id="f_ci_blocked"[\s\S]*?<\/bpmn:sequenceFlow>/);
|
|
123
|
+
assert(ciBlocked, "f_ci_blocked flow missing");
|
|
124
|
+
assertStringIncludes(ciBlocked![0], 'status = "blocked"');
|
|
125
|
+
assert(hasFlow("gw-ci-result", "merge-esc-attempt"), "gw-ci-result → merge-esc-attempt (blocked) missing");
|
|
126
|
+
});
|
|
127
|
+
|
|
65
128
|
test("regression: the conflict verdict passes through the rebase actor, not straight to escalation", () => {
|
|
66
129
|
// The original #42 livelock had the conflict verdict escalate to a human with no remediation
|
|
67
130
|
// actor. The primary target of the conflict verdict must now be the rebase gate.
|
package/app/plan.ts
CHANGED
|
@@ -55,6 +55,18 @@ export interface Plan {
|
|
|
55
55
|
// Wave-merge barrier (007_wave_gate.sql): the wave index whose PRs the plan is currently
|
|
56
56
|
// waiting to see MERGED before dispatching the next wave, or null when not parked at the barrier.
|
|
57
57
|
gate_wave: number | null;
|
|
58
|
+
// Operator-visibility wave progress (022_plan_wave_progress.sql, #137): denormalised so the
|
|
59
|
+
// epics-index can show wave X/N at a glance. `wave_count` is the total waves (N); `current_wave`
|
|
60
|
+
// is the 0-based index of the wave the fleet is actively implementing (advanced by select-wave,
|
|
61
|
+
// pinned to wave_count-1 on completion). Display-only — never gates control flow. NULL until the
|
|
62
|
+
// plan is dispatched with tasks.
|
|
63
|
+
wave_count: number | null;
|
|
64
|
+
current_wave: number | null;
|
|
65
|
+
// Pre-formatted 1-based "X/N" progress string for the epics-index at-a-glance column
|
|
66
|
+
// (022_plan_wave_progress.sql, #137). The dataGrid has no per-cell templating (nano-ide#214),
|
|
67
|
+
// so this is projected alongside the numeric columns by the same worker writes. NULL until
|
|
68
|
+
// dispatched with tasks.
|
|
69
|
+
wave_label: string | null;
|
|
58
70
|
// Per-plan capability token for the coordination blackboard (009_plan_blackboard.sql, #51).
|
|
59
71
|
// Minted at plan start; baked into the blackboard URL handed to implementer agents. NULL for
|
|
60
72
|
// plans created before the blackboard shipped.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Operator visibility: at-a-glance wave X/N on the Epic Coordination view (issue #137).
|
|
2
|
+
--
|
|
3
|
+
-- `plan_tasks.wave` (005_plan_deps.sql) holds each task's 0-based wave, but the `plans`
|
|
4
|
+
-- row itself carries no progress summary — so the epics-index grid had no way to show how
|
|
5
|
+
-- far through its waves an epic is without joining/aggregating per row. Urban's datasource
|
|
6
|
+
-- cannot read a SQL VIEW (gateway.ts schema() whitelists only type='table'), so following
|
|
7
|
+
-- the codebase's existing convention (denormalised, worker-maintained pointers on `plans`
|
|
8
|
+
-- like open_task_id / open_plan_round / gate_wave) we project two flat columns:
|
|
9
|
+
--
|
|
10
|
+
-- • wave_count — total number of waves (N). Written by `record-plan` alongside
|
|
11
|
+
-- task_count, from the `waveCount` it already computes (app/waves.ts).
|
|
12
|
+
-- • current_wave — the 0-based index of the wave the fleet is ACTIVELY implementing
|
|
13
|
+
-- (live). Initialised to 0 by `record-plan` at dispatch, advanced by
|
|
14
|
+
-- `select-wave` at the start of each wave, and pinned to wave_count-1 on
|
|
15
|
+
-- completion so a finished epic reads N/N. Display-only: it must never
|
|
16
|
+
-- gate control flow (that stays driven by the currentWave/waveCount/
|
|
17
|
+
-- gate_wave process state). NULL until the plan is dispatched with tasks.
|
|
18
|
+
-- • wave_label — the human "X/N" progress string (1-based), e.g. "2/3". The dataGrid
|
|
19
|
+
-- renders raw field values with no per-cell templating, so this pre-formats
|
|
20
|
+
-- (current_wave+1)/wave_count for the epics-index at-a-glance column. Kept in
|
|
21
|
+
-- lockstep with the two numeric columns by the same worker writes.
|
|
22
|
+
ALTER TABLE plans ADD COLUMN wave_count INTEGER;
|
|
23
|
+
ALTER TABLE plans ADD COLUMN current_wave INTEGER;
|
|
24
|
+
ALTER TABLE plans ADD COLUMN wave_label TEXT;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"lint:fix": "biome check --write app operations workers pages components scripts e2e main.ts"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@nanobpm/urban": "^0.
|
|
49
|
+
"@nanobpm/urban": "^0.44.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@biomejs/biome": "^2.4.11",
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"title": "Epic",
|
|
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
|
+
]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "text",
|
|
20
|
+
"id": "title",
|
|
21
|
+
"props": { "text": "Epic {{param}}", "variant": "heading" }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "text",
|
|
25
|
+
"id": "subtitle",
|
|
26
|
+
"props": {
|
|
27
|
+
"text": "This epic's plan, review trace, wave state, conflict graph, coordination notes, and trial-merge results. Sections start collapsed and remember your choice.",
|
|
28
|
+
"variant": "sub"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "dataGrid",
|
|
33
|
+
"id": "epic-plan",
|
|
34
|
+
"props": {
|
|
35
|
+
"title": "Plan",
|
|
36
|
+
"rowKey": "plan_key",
|
|
37
|
+
"refreshMs": 5000,
|
|
38
|
+
"data": {
|
|
39
|
+
"kind": "datasource",
|
|
40
|
+
"source": "app",
|
|
41
|
+
"table": "plans",
|
|
42
|
+
"orderBy": { "field": "updated_at", "dir": "desc" },
|
|
43
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
44
|
+
},
|
|
45
|
+
"columns": [
|
|
46
|
+
{ "field": "plan_key", "header": "Issue", "linkField": "issue_url" },
|
|
47
|
+
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
48
|
+
{ "field": "wave_label", "header": "Wave" },
|
|
49
|
+
{ "field": "task_count", "header": "Tasks" },
|
|
50
|
+
{ "field": "open_task_id", "header": "Open escalation" },
|
|
51
|
+
{ "field": "open_plan_round", "header": "Plan escalation round" },
|
|
52
|
+
{ "field": "updated_at", "header": "Updated" }
|
|
53
|
+
],
|
|
54
|
+
"rowActions": [
|
|
55
|
+
{
|
|
56
|
+
"label": "Proceed with plan",
|
|
57
|
+
"confirm": "Dispatch the current unapproved plan as a human override?",
|
|
58
|
+
"showWhenField": "open_plan_escalation_id",
|
|
59
|
+
"action": {
|
|
60
|
+
"path": "/app/api/actions/message",
|
|
61
|
+
"body": {
|
|
62
|
+
"name": "plan-escalation-answered",
|
|
63
|
+
"correlationKey": "{{row.plan_key}}",
|
|
64
|
+
"variables": { "directive": "proceed", "note": "Proceed override from the epic page." }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"detail": {
|
|
70
|
+
"linkField": "issue_url",
|
|
71
|
+
"fields": [
|
|
72
|
+
{ "field": "repo", "label": "Repository" },
|
|
73
|
+
{ "field": "issue_number", "label": "Issue number" },
|
|
74
|
+
{ "field": "base_branch", "label": "Base branch (blank = repo default)" },
|
|
75
|
+
{ "field": "outcome", "label": "Outcome" },
|
|
76
|
+
{ "field": "open_plan_findings", "label": "Open plan-review findings" },
|
|
77
|
+
{ "field": "open_task_question", "label": "Open escalation question" }
|
|
78
|
+
],
|
|
79
|
+
"form": {
|
|
80
|
+
"showWhenField": "open_plan_escalation_id",
|
|
81
|
+
"title": "Answer the open plan-review escalation",
|
|
82
|
+
"promptField": "open_plan_findings",
|
|
83
|
+
"inputKey": "note",
|
|
84
|
+
"inputLabel": "Revision directive note for the planner",
|
|
85
|
+
"submitLabel": "Revise plan",
|
|
86
|
+
"action": {
|
|
87
|
+
"path": "/app/api/actions/message",
|
|
88
|
+
"body": {
|
|
89
|
+
"name": "plan-escalation-answered",
|
|
90
|
+
"correlationKey": "{{row.plan_key}}",
|
|
91
|
+
"variables": { "directive": "revise", "note": "{{form.note}}" }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "dataGrid",
|
|
100
|
+
"id": "wave-state",
|
|
101
|
+
"props": {
|
|
102
|
+
"title": "Wave state",
|
|
103
|
+
"rowKey": "id",
|
|
104
|
+
"refreshMs": 5000,
|
|
105
|
+
"collapsible": true,
|
|
106
|
+
"defaultCollapsed": true,
|
|
107
|
+
"groupBy": "wave",
|
|
108
|
+
"groupLabel": "Wave",
|
|
109
|
+
"data": {
|
|
110
|
+
"kind": "datasource",
|
|
111
|
+
"source": "app",
|
|
112
|
+
"table": "plan_tasks",
|
|
113
|
+
"orderBy": { "field": "wave", "dir": "asc" },
|
|
114
|
+
"filter": [
|
|
115
|
+
{ "field": "plan_key", "eqParam": true },
|
|
116
|
+
{ "field": "status", "in": ["pending", "opened", "escalated", "waiting-for-lane", "blocked"] }
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"tabs": [
|
|
120
|
+
{
|
|
121
|
+
"label": "Active",
|
|
122
|
+
"filter": [
|
|
123
|
+
{ "field": "plan_key", "eqParam": true },
|
|
124
|
+
{ "field": "status", "in": ["pending", "opened", "escalated", "waiting-for-lane", "blocked"] }
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"label": "Skipped",
|
|
129
|
+
"filter": [
|
|
130
|
+
{ "field": "plan_key", "eqParam": true },
|
|
131
|
+
{ "field": "status", "in": ["skipped"] }
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"label": "All",
|
|
136
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"columns": [
|
|
140
|
+
{ "field": "wave", "header": "Wave" },
|
|
141
|
+
{ "field": "task_id", "header": "Task" },
|
|
142
|
+
{ "field": "title", "header": "Title" },
|
|
143
|
+
{ "field": "status", "header": "Status" },
|
|
144
|
+
{ "field": "pr_key", "header": "PR" },
|
|
145
|
+
{ "field": "summary", "header": "Summary" },
|
|
146
|
+
{ "field": "updated_at", "header": "Updated" }
|
|
147
|
+
],
|
|
148
|
+
"detail": {
|
|
149
|
+
"fields": [
|
|
150
|
+
{ "field": "open_question", "label": "Open question" },
|
|
151
|
+
{ "field": "answer", "label": "Answer" },
|
|
152
|
+
{ "field": "draft_pr_key", "label": "Draft PR" },
|
|
153
|
+
{ "field": "prompt", "label": "Prompt" }
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"type": "dataGrid",
|
|
160
|
+
"id": "plan-reviews",
|
|
161
|
+
"props": {
|
|
162
|
+
"title": "Plan review trace",
|
|
163
|
+
"refreshMs": 5000,
|
|
164
|
+
"collapsible": true,
|
|
165
|
+
"defaultCollapsed": true,
|
|
166
|
+
"data": {
|
|
167
|
+
"kind": "datasource",
|
|
168
|
+
"source": "app",
|
|
169
|
+
"table": "plan_reviews",
|
|
170
|
+
"orderBy": { "field": "round", "dir": "asc" },
|
|
171
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
172
|
+
},
|
|
173
|
+
"columns": [
|
|
174
|
+
{ "field": "round", "header": "Round" },
|
|
175
|
+
{ "field": "epoch", "header": "Epoch" },
|
|
176
|
+
{ "field": "approved", "header": "Approved? (1/0)" },
|
|
177
|
+
{ "field": "findings", "header": "Reviewer findings" },
|
|
178
|
+
{ "field": "created_at", "header": "Recorded" }
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"type": "dataGrid",
|
|
184
|
+
"id": "plan-review-escalations",
|
|
185
|
+
"props": {
|
|
186
|
+
"title": "Plan-review escalations",
|
|
187
|
+
"rowKey": "id",
|
|
188
|
+
"refreshMs": 5000,
|
|
189
|
+
"collapsible": true,
|
|
190
|
+
"defaultCollapsed": true,
|
|
191
|
+
"data": {
|
|
192
|
+
"kind": "datasource",
|
|
193
|
+
"source": "app",
|
|
194
|
+
"table": "plan_review_escalations",
|
|
195
|
+
"orderBy": { "field": "id", "dir": "desc" },
|
|
196
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
197
|
+
},
|
|
198
|
+
"columns": [
|
|
199
|
+
{ "field": "epoch", "header": "Epoch" },
|
|
200
|
+
{ "field": "round", "header": "Round" },
|
|
201
|
+
{ "field": "findings", "header": "Findings" },
|
|
202
|
+
{ "field": "status", "header": "Status" },
|
|
203
|
+
{ "field": "directive", "header": "Directive" },
|
|
204
|
+
{ "field": "note", "header": "Human note" },
|
|
205
|
+
{ "field": "asked_at", "header": "Asked" },
|
|
206
|
+
{ "field": "answered_at", "header": "Answered" }
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"type": "dataGrid",
|
|
212
|
+
"id": "merge-exclusions",
|
|
213
|
+
"props": {
|
|
214
|
+
"title": "Conflict graph / who blocks whom",
|
|
215
|
+
"rowKey": "id",
|
|
216
|
+
"refreshMs": 5000,
|
|
217
|
+
"collapsible": true,
|
|
218
|
+
"defaultCollapsed": true,
|
|
219
|
+
"data": {
|
|
220
|
+
"kind": "datasource",
|
|
221
|
+
"source": "app",
|
|
222
|
+
"table": "plan_merge_exclusions",
|
|
223
|
+
"orderBy": { "field": "id", "dir": "asc" },
|
|
224
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
225
|
+
},
|
|
226
|
+
"columns": [
|
|
227
|
+
{ "field": "task_a", "header": "Task A" },
|
|
228
|
+
{ "field": "task_b", "header": "Task B" },
|
|
229
|
+
{ "field": "files", "header": "Shared files" },
|
|
230
|
+
{ "field": "source", "header": "Source" },
|
|
231
|
+
{ "field": "updated_at", "header": "Updated" }
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"type": "dataGrid",
|
|
237
|
+
"id": "coordination-notes",
|
|
238
|
+
"props": {
|
|
239
|
+
"title": "Coordination notes",
|
|
240
|
+
"rowKey": "id",
|
|
241
|
+
"refreshMs": 5000,
|
|
242
|
+
"collapsible": true,
|
|
243
|
+
"defaultCollapsed": true,
|
|
244
|
+
"data": {
|
|
245
|
+
"kind": "datasource",
|
|
246
|
+
"source": "app",
|
|
247
|
+
"table": "plan_blackboard",
|
|
248
|
+
"orderBy": { "field": "id", "dir": "asc" },
|
|
249
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
250
|
+
},
|
|
251
|
+
"columns": [
|
|
252
|
+
{ "field": "wave", "header": "Wave" },
|
|
253
|
+
{ "field": "author_task", "header": "Agent" },
|
|
254
|
+
{ "field": "kind", "header": "Kind" },
|
|
255
|
+
{ "field": "files", "header": "Files" },
|
|
256
|
+
{ "field": "body", "header": "Note" },
|
|
257
|
+
{ "field": "created_at", "header": "Posted" }
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"type": "dataGrid",
|
|
263
|
+
"id": "trial-merge-results",
|
|
264
|
+
"props": {
|
|
265
|
+
"title": "Trial-merge results",
|
|
266
|
+
"rowKey": "id",
|
|
267
|
+
"refreshMs": 5000,
|
|
268
|
+
"collapsible": true,
|
|
269
|
+
"defaultCollapsed": true,
|
|
270
|
+
"data": {
|
|
271
|
+
"kind": "datasource",
|
|
272
|
+
"source": "app",
|
|
273
|
+
"table": "plan_trial_merges",
|
|
274
|
+
"orderBy": { "field": "created_at", "dir": "desc" },
|
|
275
|
+
"filter": [
|
|
276
|
+
{ "field": "plan_key", "eqParam": true },
|
|
277
|
+
{ "field": "result", "in": ["merge-conflict", "suite-failed"] },
|
|
278
|
+
{ "field": "resolved", "in": [0] }
|
|
279
|
+
]
|
|
280
|
+
},
|
|
281
|
+
"tabs": [
|
|
282
|
+
{
|
|
283
|
+
"label": "Needs attention",
|
|
284
|
+
"filter": [
|
|
285
|
+
{ "field": "plan_key", "eqParam": true },
|
|
286
|
+
{ "field": "result", "in": ["merge-conflict", "suite-failed"] },
|
|
287
|
+
{ "field": "resolved", "in": [0] }
|
|
288
|
+
]
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"label": "Clean",
|
|
292
|
+
"filter": [
|
|
293
|
+
{ "field": "plan_key", "eqParam": true },
|
|
294
|
+
{ "field": "result", "in": ["clean"] }
|
|
295
|
+
]
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"label": "All",
|
|
299
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
300
|
+
}
|
|
301
|
+
],
|
|
302
|
+
"columns": [
|
|
303
|
+
{ "field": "wave", "header": "Wave" },
|
|
304
|
+
{ "field": "result", "header": "Result" },
|
|
305
|
+
{ "field": "heads", "header": "Heads" },
|
|
306
|
+
{ "field": "conflicts", "header": "Conflicts" },
|
|
307
|
+
{ "field": "failing", "header": "Failing" },
|
|
308
|
+
{ "field": "summary", "header": "Summary" },
|
|
309
|
+
{ "field": "created_at", "header": "Recorded" }
|
|
310
|
+
]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
}
|