@nanobpm/nano-workforce 0.79.0 → 0.81.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/contracts.ts +23 -0
- package/app/delivery.test.ts +2 -1
- package/app/delivery.ts +76 -0
- package/app/instance-tracking.test.ts +2 -1
- package/app/lineage.test.ts +300 -0
- package/app/lineage.ts +537 -0
- package/app/migration037.test.ts +62 -0
- package/app/readiness.test.ts +300 -0
- package/app/readiness.ts +493 -0
- package/app/retro.ts +1 -1
- package/app/reviewWait.test.ts +19 -0
- package/app/reviewWait.ts +20 -0
- package/app/service.test.ts +104 -1
- package/app/service.ts +33 -71
- package/db/migrations/037_lineage.sql +69 -0
- package/e2e/readiness-gate.e2e.ts +285 -0
- package/nano.app.json +4 -0
- package/openapi.yaml +120 -0
- package/operations/getLineage.test.ts +105 -0
- package/operations/getLineage.ts +32 -0
- package/package.json +1 -1
- package/pages/cockpit.page.json +1 -0
- package/pages/epic-detail.page.json +1 -0
- package/pages/epic.page.json +1 -0
- package/pages/feature.page.json +1 -0
- package/pages/home.page.json +4 -0
- package/pages/lineage.page.json +146 -0
- package/pages/overview.page.json +1 -0
- package/pages/tasks.page.json +4 -0
- package/resources/forms/readiness-escalation.form +29 -0
- package/resources/processes/readiness-gate.bpmn +343 -0
- package/workers/converge-feature/worker.ts +1 -1
- package/workers/readiness-probe/worker.test.ts +236 -0
- package/workers/readiness-probe/worker.ts +146 -0
- package/workers/record-wave/worker.ts +2 -2
package/openapi.yaml
CHANGED
|
@@ -99,6 +99,99 @@ components:
|
|
|
99
99
|
type: array
|
|
100
100
|
items:
|
|
101
101
|
$ref: "#/components/schemas/ActivePr"
|
|
102
|
+
LineagePrView:
|
|
103
|
+
type: object
|
|
104
|
+
description: A member PR of a lineage thread (issue #245).
|
|
105
|
+
additionalProperties: false
|
|
106
|
+
required:
|
|
107
|
+
- prKey
|
|
108
|
+
- title
|
|
109
|
+
- url
|
|
110
|
+
- status
|
|
111
|
+
- round
|
|
112
|
+
- processKey
|
|
113
|
+
- outcome
|
|
114
|
+
properties:
|
|
115
|
+
prKey:
|
|
116
|
+
type: string
|
|
117
|
+
title:
|
|
118
|
+
type: string
|
|
119
|
+
nullable: true
|
|
120
|
+
url:
|
|
121
|
+
type: string
|
|
122
|
+
status:
|
|
123
|
+
type: string
|
|
124
|
+
round:
|
|
125
|
+
type: integer
|
|
126
|
+
processKey:
|
|
127
|
+
type: string
|
|
128
|
+
nullable: true
|
|
129
|
+
outcome:
|
|
130
|
+
type: string
|
|
131
|
+
nullable: true
|
|
132
|
+
LineageThreadView:
|
|
133
|
+
type: object
|
|
134
|
+
description: One stitched intent → progress arc, keyed by its origin request (issue #245).
|
|
135
|
+
additionalProperties: false
|
|
136
|
+
required:
|
|
137
|
+
- rootRequestKey
|
|
138
|
+
- kind
|
|
139
|
+
- title
|
|
140
|
+
- issueUrl
|
|
141
|
+
- stage
|
|
142
|
+
- stageLabel
|
|
143
|
+
- processKey
|
|
144
|
+
- prKeys
|
|
145
|
+
- prCount
|
|
146
|
+
- active
|
|
147
|
+
- prs
|
|
148
|
+
properties:
|
|
149
|
+
rootRequestKey:
|
|
150
|
+
type: string
|
|
151
|
+
description: The origin issue key (feature_key/plan_key), or a self-rooted pr_key.
|
|
152
|
+
kind:
|
|
153
|
+
type: string
|
|
154
|
+
enum: [feature, epic, pr]
|
|
155
|
+
title:
|
|
156
|
+
type: string
|
|
157
|
+
nullable: true
|
|
158
|
+
issueUrl:
|
|
159
|
+
type: string
|
|
160
|
+
nullable: true
|
|
161
|
+
stage:
|
|
162
|
+
type: string
|
|
163
|
+
description: The active-frontier stage (implementing|converging|merged|…).
|
|
164
|
+
stageLabel:
|
|
165
|
+
type: string
|
|
166
|
+
processKey:
|
|
167
|
+
type: string
|
|
168
|
+
nullable: true
|
|
169
|
+
description: The active-frontier process instance key (for the processExplorer link).
|
|
170
|
+
prKeys:
|
|
171
|
+
type: array
|
|
172
|
+
items:
|
|
173
|
+
type: string
|
|
174
|
+
prCount:
|
|
175
|
+
type: integer
|
|
176
|
+
active:
|
|
177
|
+
type: boolean
|
|
178
|
+
description: True while the arc has an active frontier; false once every stage has settled.
|
|
179
|
+
prs:
|
|
180
|
+
type: array
|
|
181
|
+
items:
|
|
182
|
+
$ref: "#/components/schemas/LineagePrView"
|
|
183
|
+
LineageList:
|
|
184
|
+
type: object
|
|
185
|
+
required:
|
|
186
|
+
- count
|
|
187
|
+
- threads
|
|
188
|
+
properties:
|
|
189
|
+
count:
|
|
190
|
+
type: integer
|
|
191
|
+
threads:
|
|
192
|
+
type: array
|
|
193
|
+
items:
|
|
194
|
+
$ref: "#/components/schemas/LineageThreadView"
|
|
102
195
|
AgenticSupplyWorker:
|
|
103
196
|
type: object
|
|
104
197
|
description: One connected worker in the supply mirror (H5 cockpit; sourced from the H1 presence registry).
|
|
@@ -936,6 +1029,33 @@ paths:
|
|
|
936
1029
|
application/json:
|
|
937
1030
|
schema:
|
|
938
1031
|
$ref: "#/components/schemas/ErrorBody"
|
|
1032
|
+
/lineage:
|
|
1033
|
+
get:
|
|
1034
|
+
operationId: getLineage
|
|
1035
|
+
summary: "Intent to progress lineage projection (issue #245): every request stitched into one arc (request, implementation, PRs, convergence, merge, outcome), active frontier first. Pass root to fetch a single origin's thread."
|
|
1036
|
+
security:
|
|
1037
|
+
- hookSecret: []
|
|
1038
|
+
- {}
|
|
1039
|
+
parameters:
|
|
1040
|
+
- name: root
|
|
1041
|
+
in: query
|
|
1042
|
+
required: false
|
|
1043
|
+
schema:
|
|
1044
|
+
type: string
|
|
1045
|
+
description: "An origin request key (feature_key/plan_key) or a self-rooted pr_key. When set, the response contains just that thread (empty if unknown)."
|
|
1046
|
+
responses:
|
|
1047
|
+
"200":
|
|
1048
|
+
description: The stitched lineage threads.
|
|
1049
|
+
content:
|
|
1050
|
+
application/json:
|
|
1051
|
+
schema:
|
|
1052
|
+
$ref: "#/components/schemas/LineageList"
|
|
1053
|
+
"401":
|
|
1054
|
+
description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
|
|
1055
|
+
content:
|
|
1056
|
+
application/json:
|
|
1057
|
+
schema:
|
|
1058
|
+
$ref: "#/components/schemas/ErrorBody"
|
|
939
1059
|
/agentic/supply:
|
|
940
1060
|
get:
|
|
941
1061
|
operationId: getAgenticSupply
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Tests for GET /app/api/lineage → operation `getLineage` (issue #245). Covers the stitched list
|
|
2
|
+
// projection, the `root` narrowing, an unknown root (empty), and the optional shared-secret guard.
|
|
3
|
+
// A minimal in-memory DataLayer backs the derivation (it reads the feature_runs / plans / plan_tasks
|
|
4
|
+
// / pull_requests tables via `.all()` / `.find()`).
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { assert, assertEquals } from "#test-assert";
|
|
7
|
+
import type { AppApi } from "@nanobpm/urban";
|
|
8
|
+
import { noopLog } from "../test/log.ts";
|
|
9
|
+
import handler from "./getLineage.ts";
|
|
10
|
+
|
|
11
|
+
function memApp(stores: Record<string, any[]>): AppApi {
|
|
12
|
+
const table = (name: string) => {
|
|
13
|
+
const rows = stores[name] ?? [];
|
|
14
|
+
return {
|
|
15
|
+
async all() {
|
|
16
|
+
return rows.slice();
|
|
17
|
+
},
|
|
18
|
+
async find(where: Record<string, unknown> = {}) {
|
|
19
|
+
return rows.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v));
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
return { data: { table }, log: noopLog() } as any as AppApi;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function input(query: Record<string, string> = {}, headers: Record<string, string> = {}) {
|
|
27
|
+
return {
|
|
28
|
+
req: {
|
|
29
|
+
method: "GET",
|
|
30
|
+
path: "/app/api/lineage",
|
|
31
|
+
query: new URLSearchParams(query),
|
|
32
|
+
headers: new Headers(headers),
|
|
33
|
+
text: async () => "",
|
|
34
|
+
} as any,
|
|
35
|
+
params: {},
|
|
36
|
+
query,
|
|
37
|
+
body: undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fixture(): Record<string, any[]> {
|
|
42
|
+
return {
|
|
43
|
+
feature_runs: [
|
|
44
|
+
{ feature_key: "o/r#1", title: "Feature", issue_url: "u1", status: "converging", process_key: "f1", pr_key: "o/r#100" },
|
|
45
|
+
],
|
|
46
|
+
plans: [
|
|
47
|
+
{ plan_key: "o/r#2", title: "Epic", issue_url: "u2", status: "done", process_key: "e1" },
|
|
48
|
+
],
|
|
49
|
+
plan_tasks: [
|
|
50
|
+
{ id: 1, plan_key: "o/r#2", pr_key: "o/r#200" },
|
|
51
|
+
{ id: 2, plan_key: "o/r#2", pr_key: "o/r#201" },
|
|
52
|
+
],
|
|
53
|
+
pull_requests: [
|
|
54
|
+
{ pr_key: "o/r#100", title: "Feat PR", url: "x", status: "converging", current_round: 2, process_key: "c1", outcome: null, root_request_key: "o/r#1" },
|
|
55
|
+
{ pr_key: "o/r#200", title: "S1", url: "x", status: "merged", current_round: 1, process_key: "c2", outcome: null, root_request_key: "o/r#2" },
|
|
56
|
+
{ pr_key: "o/r#201", title: "S2", url: "x", status: "converging", current_round: 1, process_key: "c3", outcome: null, root_request_key: "o/r#2" },
|
|
57
|
+
{ pr_key: "o/r#300", title: "Human PR", url: "x", status: "merged", current_round: 1, process_key: "c4", outcome: null, root_request_key: null },
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test("lists every stitched thread, active frontier first", async () => {
|
|
63
|
+
const res = (await handler(input(), memApp(fixture()))) as any;
|
|
64
|
+
assertEquals(res.status, 200);
|
|
65
|
+
assertEquals(res.body.count, 3);
|
|
66
|
+
// Active threads (feature + epic) sort ahead of the settled human PR.
|
|
67
|
+
assert(res.body.threads[res.body.threads.length - 1].active === false);
|
|
68
|
+
const kinds = res.body.threads.map((t: any) => t.kind).sort();
|
|
69
|
+
assertEquals(kinds, ["epic", "feature", "pr"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("narrows to a single origin thread via ?root=", async () => {
|
|
73
|
+
const res = (await handler(input({ root: "o/r#2" }), memApp(fixture()))) as any;
|
|
74
|
+
assertEquals(res.status, 200);
|
|
75
|
+
assertEquals(res.body.count, 1);
|
|
76
|
+
const t = res.body.threads[0];
|
|
77
|
+
assertEquals(t.kind, "epic");
|
|
78
|
+
assertEquals(t.rootRequestKey, "o/r#2");
|
|
79
|
+
assertEquals(t.prCount, 2);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("unknown root returns an empty thread list", async () => {
|
|
83
|
+
const res = (await handler(input({ root: "o/r#999" }), memApp(fixture()))) as any;
|
|
84
|
+
assertEquals(res.status, 200);
|
|
85
|
+
assertEquals(res.body.count, 0);
|
|
86
|
+
assertEquals(res.body.threads, []);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("shared-secret guard rejects a missing secret when configured", async () => {
|
|
90
|
+
const prev = process.env["NANO_PR_WEBHOOK_SECRET"];
|
|
91
|
+
process.env["NANO_PR_WEBHOOK_SECRET"] = "s3cr3t";
|
|
92
|
+
try {
|
|
93
|
+
const mod = await import(`./getLineage.ts?guard=${Date.now()}`);
|
|
94
|
+
const guarded = mod.default as typeof handler;
|
|
95
|
+
const app = memApp(fixture());
|
|
96
|
+
const bad = (await guarded(input(), app)) as any;
|
|
97
|
+
assertEquals(bad.status, 401);
|
|
98
|
+
const ok = (await guarded(input({}, { "x-hook-secret": "s3cr3t" }), app)) as any;
|
|
99
|
+
assertEquals(ok.status, 200);
|
|
100
|
+
assert("count" in ok.body);
|
|
101
|
+
} finally {
|
|
102
|
+
if (prev === undefined) delete process.env["NANO_PR_WEBHOOK_SECRET"];
|
|
103
|
+
else process.env["NANO_PR_WEBHOOK_SECRET"] = prev;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// GET /app/api/lineage → operationId `getLineage` (issue #245). Project user intent → progress as
|
|
2
|
+
// one lineage arc per origin request, so an operator or an external harness can see, in one place,
|
|
3
|
+
// that "issue #123 → implementation → PR #45 → converging (round 2) → merged" is a single thread of
|
|
4
|
+
// their intent — without opening the DB or joining the tables by hand. Read-only projection over the
|
|
5
|
+
// existing record-gateway joins (the same derivation the `lineage_threads` read table is built from).
|
|
6
|
+
//
|
|
7
|
+
// Optional `root` query param narrows to a single origin's thread (feature_key/plan_key, or a
|
|
8
|
+
// self-rooted pr_key for a human/webhook PR). Absent → every thread, active frontier first.
|
|
9
|
+
//
|
|
10
|
+
// The optional shared-secret guard stays HERE (the runtime does not enforce OpenAPI `security`):
|
|
11
|
+
// when NANO_PR_WEBHOOK_SECRET is set, callers must present it via the x-hook-secret header.
|
|
12
|
+
import { getLineage, listLineage } from "../app/lineage.ts";
|
|
13
|
+
import { envVar } from "../app/version.ts";
|
|
14
|
+
import { defineOperation } from "../nano-generated/operations.ts";
|
|
15
|
+
|
|
16
|
+
const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
|
|
17
|
+
|
|
18
|
+
export default defineOperation("getLineage", async ({ query, req }, app) => {
|
|
19
|
+
if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
|
|
20
|
+
app.log.warn("getLineage rejected: missing/invalid shared secret");
|
|
21
|
+
return { status: 401, body: { error: "unauthorized" } };
|
|
22
|
+
}
|
|
23
|
+
const rawRoot = query.root;
|
|
24
|
+
const root = typeof rawRoot === "string" ? rawRoot.trim() : "";
|
|
25
|
+
if (root) {
|
|
26
|
+
const thread = await getLineage(app.data, root);
|
|
27
|
+
const threads = thread ? [thread] : [];
|
|
28
|
+
return { status: 200, body: { count: threads.length, threads } };
|
|
29
|
+
}
|
|
30
|
+
const threads = await listLineage(app.data);
|
|
31
|
+
return { status: 200, body: { count: threads.length, threads } };
|
|
32
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.81.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",
|
package/pages/cockpit.page.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"title": "Nano Workforce",
|
|
11
11
|
"items": [
|
|
12
12
|
{ "label": "Overview", "page": "overview" },
|
|
13
|
+
{ "label": "Lineage", "page": "lineage" },
|
|
13
14
|
{ "label": "Convergence", "page": "home" },
|
|
14
15
|
{ "label": "Epics", "page": "epic" },
|
|
15
16
|
{ "label": "Feature", "page": "feature" },
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"title": "Nano Workforce",
|
|
12
12
|
"items": [
|
|
13
13
|
{ "label": "Overview", "page": "overview" },
|
|
14
|
+
{ "label": "Lineage", "page": "lineage" },
|
|
14
15
|
{ "label": "Convergence", "page": "home" },
|
|
15
16
|
{ "label": "Epics", "page": "epic" },
|
|
16
17
|
{ "label": "Feature", "page": "feature" },
|
package/pages/epic.page.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"title": "Nano Workforce",
|
|
12
12
|
"items": [
|
|
13
13
|
{ "label": "Overview", "page": "overview" },
|
|
14
|
+
{ "label": "Lineage", "page": "lineage" },
|
|
14
15
|
{ "label": "Convergence", "page": "home" },
|
|
15
16
|
{ "label": "Epics", "page": "epic" },
|
|
16
17
|
{ "label": "Feature", "page": "feature" },
|
package/pages/feature.page.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"title": "Nano Workforce",
|
|
12
12
|
"items": [
|
|
13
13
|
{ "label": "Overview", "page": "overview" },
|
|
14
|
+
{ "label": "Lineage", "page": "lineage" },
|
|
14
15
|
{ "label": "Convergence", "page": "home" },
|
|
15
16
|
{ "label": "Epics", "page": "epic" },
|
|
16
17
|
{ "label": "Feature", "page": "feature" },
|
package/pages/home.page.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"title": "Lineage",
|
|
4
|
+
"nodes": [
|
|
5
|
+
{
|
|
6
|
+
"type": "nav",
|
|
7
|
+
"id": "nav",
|
|
8
|
+
"props": {
|
|
9
|
+
"variant": "bar",
|
|
10
|
+
"title": "Nano Workforce",
|
|
11
|
+
"items": [
|
|
12
|
+
{ "label": "Overview", "page": "overview" },
|
|
13
|
+
{ "label": "Lineage", "page": "lineage" },
|
|
14
|
+
{ "label": "Convergence", "page": "home" },
|
|
15
|
+
{ "label": "Epics", "page": "epic" },
|
|
16
|
+
{ "label": "Feature", "page": "feature" },
|
|
17
|
+
{ "label": "Tasks", "page": "tasks" },
|
|
18
|
+
{ "label": "Cockpit", "page": "cockpit" }
|
|
19
|
+
],
|
|
20
|
+
"sticky": true
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "text",
|
|
25
|
+
"id": "title",
|
|
26
|
+
"props": { "text": "Lineage \u2014 intent \u2192 progress", "variant": "heading" }
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"type": "text",
|
|
30
|
+
"id": "subtitle",
|
|
31
|
+
"props": {
|
|
32
|
+
"text": "One narrative per request, not a card-swap. Each row is a single arc of your intent \u2014 request \u2192 implementing \u2192 PR opened \u2192 converging (round n) \u2192 merged/converged/abandoned \u2014 stitched from the feature/epic that started it, the PR(s) it produced, and their convergence + merge outcome. The Stage column is the active frontier; drill into a row for its member PR(s) and rounds. Epics fan out to N PR sub-threads. A human/webhook PR with no originating request is shown as its own root.",
|
|
33
|
+
"variant": "sub"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "dataGrid",
|
|
38
|
+
"id": "threads",
|
|
39
|
+
"props": {
|
|
40
|
+
"title": "Lineage threads",
|
|
41
|
+
"collapsible": true,
|
|
42
|
+
"defaultCollapsed": false,
|
|
43
|
+
"showCount": true,
|
|
44
|
+
"rowKey": "root_request_key",
|
|
45
|
+
"refreshMs": 5000,
|
|
46
|
+
"data": {
|
|
47
|
+
"kind": "datasource",
|
|
48
|
+
"source": "app",
|
|
49
|
+
"table": "lineage_threads",
|
|
50
|
+
"orderBy": { "field": "updated_at", "dir": "desc" },
|
|
51
|
+
"filter": [{ "field": "active", "in": [1] }]
|
|
52
|
+
},
|
|
53
|
+
"tabs": [
|
|
54
|
+
{
|
|
55
|
+
"label": "Active",
|
|
56
|
+
"filter": [{ "field": "active", "in": [1] }]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"label": "History",
|
|
60
|
+
"filter": [{ "field": "active", "in": [0] }]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"columns": [
|
|
64
|
+
{
|
|
65
|
+
"field": "title",
|
|
66
|
+
"template": "{{title}}",
|
|
67
|
+
"header": "Intent",
|
|
68
|
+
"subtitleField": "root_request_key",
|
|
69
|
+
"truncate": true,
|
|
70
|
+
"width": "34%",
|
|
71
|
+
"linkField": "issue_url"
|
|
72
|
+
},
|
|
73
|
+
{ "field": "kind", "header": "Kind", "width": "6rem" },
|
|
74
|
+
{
|
|
75
|
+
"field": "stage_label",
|
|
76
|
+
"header": "Stage",
|
|
77
|
+
"truncate": true,
|
|
78
|
+
"link": { "kind": "processExplorer", "keyField": "process_key" }
|
|
79
|
+
},
|
|
80
|
+
{ "field": "pr_count", "header": "PRs", "width": "5rem" },
|
|
81
|
+
{ "field": "updated_at", "header": "Updated", "width": "9rem" }
|
|
82
|
+
],
|
|
83
|
+
"detail": {
|
|
84
|
+
"linkField": "issue_url",
|
|
85
|
+
"fields": [
|
|
86
|
+
{ "field": "kind", "label": "Origin kind" },
|
|
87
|
+
{ "field": "stage", "label": "Frontier stage" },
|
|
88
|
+
{ "field": "stage_label", "label": "Frontier" },
|
|
89
|
+
{ "field": "issue_url", "label": "Origin issue" },
|
|
90
|
+
{ "field": "pr_count", "label": "PR count" }
|
|
91
|
+
],
|
|
92
|
+
"children": [
|
|
93
|
+
{
|
|
94
|
+
"title": "Pull requests in this thread",
|
|
95
|
+
"source": "app",
|
|
96
|
+
"table": "pull_requests",
|
|
97
|
+
"parentField": "root_request_key",
|
|
98
|
+
"childField": "root_request_key",
|
|
99
|
+
"orderBy": { "field": "updated_at", "dir": "asc" },
|
|
100
|
+
"columns": [
|
|
101
|
+
{ "field": "title", "template": "{{title}}", "header": "PR", "subtitleField": "pr_key", "truncate": true, "linkField": "url" },
|
|
102
|
+
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
103
|
+
{ "field": "current_round", "header": "Round" },
|
|
104
|
+
{ "field": "outcome", "header": "Outcome", "truncate": true },
|
|
105
|
+
{ "field": "updated_at", "header": "Updated", "width": "9rem" }
|
|
106
|
+
],
|
|
107
|
+
"detail": {
|
|
108
|
+
"linkField": "url",
|
|
109
|
+
"children": [
|
|
110
|
+
{
|
|
111
|
+
"title": "Rounds",
|
|
112
|
+
"source": "app",
|
|
113
|
+
"table": "rounds",
|
|
114
|
+
"parentField": "pr_key",
|
|
115
|
+
"childField": "pr_key",
|
|
116
|
+
"orderBy": { "field": "round_no", "dir": "asc" },
|
|
117
|
+
"columns": [
|
|
118
|
+
{ "field": "round_no", "header": "#" },
|
|
119
|
+
{ "field": "status", "header": "Result" },
|
|
120
|
+
{ "field": "summary", "header": "Summary" }
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"title": "Escalations",
|
|
125
|
+
"source": "app",
|
|
126
|
+
"table": "escalations",
|
|
127
|
+
"parentField": "pr_key",
|
|
128
|
+
"childField": "pr_key",
|
|
129
|
+
"orderBy": { "field": "id", "dir": "asc" },
|
|
130
|
+
"columns": [
|
|
131
|
+
{ "field": "round_no", "header": "#" },
|
|
132
|
+
{ "field": "kind", "header": "Kind" },
|
|
133
|
+
{ "field": "question", "header": "Question" },
|
|
134
|
+
{ "field": "status", "header": "Status" },
|
|
135
|
+
{ "field": "answer", "header": "Answer" }
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
package/pages/overview.page.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"title": "Nano Workforce",
|
|
12
12
|
"items": [
|
|
13
13
|
{ "label": "Overview", "page": "overview" },
|
|
14
|
+
{ "label": "Lineage", "page": "lineage" },
|
|
14
15
|
{ "label": "Convergence", "page": "home" },
|
|
15
16
|
{ "label": "Epics", "page": "epic" },
|
|
16
17
|
{ "label": "Feature", "page": "feature" },
|
package/pages/tasks.page.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "readiness-escalation",
|
|
3
|
+
"schemaVersion": 18,
|
|
4
|
+
"type": "default",
|
|
5
|
+
"components": [
|
|
6
|
+
{
|
|
7
|
+
"type": "text",
|
|
8
|
+
"text": "The artifact-readiness wait-gate timed out before its ReadinessProbe went green. A human decision is needed on how to proceed.",
|
|
9
|
+
"label": "Readiness timeout"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "select",
|
|
13
|
+
"key": "resolution",
|
|
14
|
+
"label": "Resolution",
|
|
15
|
+
"values": [
|
|
16
|
+
{ "label": "Acknowledge — the artifact is ready / proceed", "value": "acknowledge" },
|
|
17
|
+
{ "label": "Abandon — give up on this gate", "value": "abandon" }
|
|
18
|
+
],
|
|
19
|
+
"validate": {
|
|
20
|
+
"required": true
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "textarea",
|
|
25
|
+
"key": "answer",
|
|
26
|
+
"label": "Notes for the operator"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|