@nanobpm/nano-workforce 0.32.2 → 0.33.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 +13 -0
- package/actions/version.test.ts +85 -0
- package/actions/version.ts +25 -0
- package/app/version.ts +205 -0
- package/nano.app.json +5 -0
- package/package.json +1 -1
- package/pages/epic.page.json +14 -2
- package/pages/home.page.json +2 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# [0.33.0](https://github.com/nanobpm/nano-workforce/compare/v0.32.2...v0.33.0) (2026-08-09)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **pages:** move the epic submission form to the Epic tab ([#97](https://github.com/nanobpm/nano-workforce/issues/97)) ([da6db32](https://github.com/nanobpm/nano-workforce/commit/da6db32438d8b9fd9c04fea08ffe1f981728f314))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **ops:** add GET /app/version endpoint for runtime identity ([#98](https://github.com/nanobpm/nano-workforce/issues/98)) ([538cf61](https://github.com/nanobpm/nano-workforce/commit/538cf615cf0f9b34cdb6417346e045fc6131423e))
|
|
12
|
+
* **pages:** link status column to the process explorer ([#99](https://github.com/nanobpm/nano-workforce/issues/99)) ([7f19a29](https://github.com/nanobpm/nano-workforce/commit/7f19a298b4c8c427c593a9d71181192bcac4a896))
|
|
13
|
+
|
|
1
14
|
## [0.32.2](https://github.com/nanobpm/nano-workforce/compare/v0.32.1...v0.32.2) (2026-08-09)
|
|
2
15
|
|
|
3
16
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Tests for GET /app/version (version/identity endpoint).
|
|
2
|
+
import { assert, assertEquals } from "jsr:@std/assert@1";
|
|
3
|
+
import type { AppApi } from "@nanobpm/urban";
|
|
4
|
+
import handler from "./version.ts";
|
|
5
|
+
import { buildVersionInfo } from "../app/version.ts";
|
|
6
|
+
|
|
7
|
+
// deno-lint-ignore no-explicit-any
|
|
8
|
+
const app = {} as any as AppApi;
|
|
9
|
+
|
|
10
|
+
function req(method: string, headers: Record<string, string> = {}) {
|
|
11
|
+
return {
|
|
12
|
+
method,
|
|
13
|
+
path: "/app/version",
|
|
14
|
+
query: new URLSearchParams(),
|
|
15
|
+
headers: new Headers(headers),
|
|
16
|
+
text: async () => "",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function call(method: string, headers: Record<string, string> = {}) {
|
|
21
|
+
// deno-lint-ignore no-explicit-any
|
|
22
|
+
const res = await handler({ req: req(method, headers) as any, body: undefined }, app);
|
|
23
|
+
// deno-lint-ignore no-explicit-any
|
|
24
|
+
return res as any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Deno.test("GET returns 200 with the app identity", async () => {
|
|
28
|
+
const res = await call("GET");
|
|
29
|
+
assertEquals(res.status, 200);
|
|
30
|
+
assertEquals(res.body.name, "nano-workforce");
|
|
31
|
+
// These are always present; their values are environment-dependent so we only assert shape.
|
|
32
|
+
assert("version" in res.body);
|
|
33
|
+
assert("urbanVersion" in res.body);
|
|
34
|
+
assert("gitSha" in res.body);
|
|
35
|
+
assert("gitBranch" in res.body);
|
|
36
|
+
assert(typeof res.body.runtime === "string" && res.body.runtime.length > 0);
|
|
37
|
+
assert(typeof res.body.startedAt === "string");
|
|
38
|
+
assert(typeof res.body.uptimeSeconds === "number");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
Deno.test("non-GET is rejected with 405", async () => {
|
|
42
|
+
const res = await call("POST");
|
|
43
|
+
assertEquals(res.status, 405);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
Deno.test("shared-secret guard rejects a missing/wrong secret when configured", async () => {
|
|
47
|
+
const prev = Deno.env.get("NANO_PR_WEBHOOK_SECRET");
|
|
48
|
+
Deno.env.set("NANO_PR_WEBHOOK_SECRET", "s3cr3t");
|
|
49
|
+
try {
|
|
50
|
+
// The handler binds SECRET at import time, so a freshly-imported module is needed to observe
|
|
51
|
+
// the guard. Import a cache-busted copy so this test is independent of import order.
|
|
52
|
+
const mod = await import(`./version.ts?guard=${Date.now()}`);
|
|
53
|
+
const guarded = mod.default as typeof handler;
|
|
54
|
+
// deno-lint-ignore no-explicit-any
|
|
55
|
+
const bad = (await guarded({ req: req("GET") as any, body: undefined }, app)) as any;
|
|
56
|
+
assertEquals(bad.status, 401);
|
|
57
|
+
// deno-lint-ignore no-explicit-any
|
|
58
|
+
const ok = (await guarded(
|
|
59
|
+
// deno-lint-ignore no-explicit-any
|
|
60
|
+
{ req: req("GET", { "x-hook-secret": "s3cr3t" }) as any, body: undefined },
|
|
61
|
+
app,
|
|
62
|
+
)) as any;
|
|
63
|
+
assertEquals(ok.status, 200);
|
|
64
|
+
} finally {
|
|
65
|
+
if (prev === undefined) Deno.env.delete("NANO_PR_WEBHOOK_SECRET");
|
|
66
|
+
else Deno.env.set("NANO_PR_WEBHOOK_SECRET", prev);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
Deno.test("buildVersionInfo is side-effect free and stable in shape", () => {
|
|
71
|
+
const a = buildVersionInfo();
|
|
72
|
+
const b = buildVersionInfo();
|
|
73
|
+
assertEquals(a.name, b.name);
|
|
74
|
+
assertEquals(Object.keys(a).sort(), [
|
|
75
|
+
"gitBranch",
|
|
76
|
+
"gitSha",
|
|
77
|
+
"name",
|
|
78
|
+
"pid",
|
|
79
|
+
"runtime",
|
|
80
|
+
"startedAt",
|
|
81
|
+
"uptimeSeconds",
|
|
82
|
+
"urbanVersion",
|
|
83
|
+
"version",
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// GET /app/version — the running app's identity (ADR: version endpoint for debugging).
|
|
2
|
+
//
|
|
3
|
+
// Answers "which code is this process actually running?" — the app version, the resolved
|
|
4
|
+
// `@nanobpm/urban` runtime version, the git commit/branch of the working tree, the JS runtime,
|
|
5
|
+
// pid, and how long it has been up. Because the app runs its `.ts` sources directly from a
|
|
6
|
+
// checkout with no build step, restarts alone don't tell you whether the fix you shipped is live;
|
|
7
|
+
// this endpoint does.
|
|
8
|
+
//
|
|
9
|
+
// Read-only and unauthenticated by design (no secrets in the payload); it mirrors the open
|
|
10
|
+
// posture of the pages surface. Optional shared-secret guard when NANO_PR_WEBHOOK_SECRET is set,
|
|
11
|
+
// mirroring /app/status.
|
|
12
|
+
import type { ActionHandler } from "@nanobpm/urban";
|
|
13
|
+
import { buildVersionInfo } from "../app/version.ts";
|
|
14
|
+
|
|
15
|
+
const SECRET = process.env.NANO_PR_WEBHOOK_SECRET ?? "";
|
|
16
|
+
|
|
17
|
+
const handler: ActionHandler = ({ req }) => {
|
|
18
|
+
if (req.method !== "GET") return { status: 405, body: { error: "method not allowed (use GET)" } };
|
|
19
|
+
if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
|
|
20
|
+
return { status: 401, body: { error: "unauthorized" } };
|
|
21
|
+
}
|
|
22
|
+
return { status: 200, body: buildVersionInfo() };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default handler;
|
package/app/version.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Runtime version/identity of the running nano-workforce app.
|
|
2
|
+
//
|
|
3
|
+
// The app runs its TypeScript sources DIRECTLY from a checkout (`node --experimental-strip-types
|
|
4
|
+
// main.ts`) with no build/bundle step, so "which code is running" can only be answered by
|
|
5
|
+
// inspecting the working tree at runtime. This module gathers that identity — the app's package
|
|
6
|
+
// version, the resolved `@nanobpm/urban` version, the git commit (read from `.git`, handling both
|
|
7
|
+
// an ordinary `.git` directory and the `gitdir:` file pointer used by worktrees/submodules, with
|
|
8
|
+
// an env override for detached deploys), plus the Node/Deno runtime, pid and start time — so an operator
|
|
9
|
+
// debugging a stuck instance can confirm the process is on the code they think it is.
|
|
10
|
+
//
|
|
11
|
+
// Every probe is best-effort: a missing file or unavailable `.git` yields `null` for that field
|
|
12
|
+
// rather than throwing, so `/app/version` never fails just because one source is absent.
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { dirname, join, resolve, isAbsolute } from "node:path";
|
|
16
|
+
|
|
17
|
+
// Captured once, at module load — i.e. when the running process booted this code.
|
|
18
|
+
const STARTED_AT = new Date();
|
|
19
|
+
|
|
20
|
+
const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
21
|
+
|
|
22
|
+
function readText(path: string): string | null {
|
|
23
|
+
try {
|
|
24
|
+
return readFileSync(path, "utf8");
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJson(path: string): Record<string, unknown> | null {
|
|
31
|
+
const text = readText(path);
|
|
32
|
+
if (text == null) return null;
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(text) as Record<string, unknown>;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read an env var across runtimes: Node exposes `process.env`; Deno may not populate it, so fall
|
|
42
|
+
* back to `Deno.env.get` (guarded — reading env can throw without `--allow-env`).
|
|
43
|
+
*/
|
|
44
|
+
function envVar(name: string): string | null {
|
|
45
|
+
const fromProcess = globalThis.process?.env?.[name];
|
|
46
|
+
if (typeof fromProcess === "string" && fromProcess.trim()) return fromProcess.trim();
|
|
47
|
+
const deno = (globalThis as { Deno?: { env?: { get?(k: string): string | undefined } } }).Deno;
|
|
48
|
+
try {
|
|
49
|
+
const fromDeno = deno?.env?.get?.(name);
|
|
50
|
+
if (typeof fromDeno === "string" && fromDeno.trim()) return fromDeno.trim();
|
|
51
|
+
} catch {
|
|
52
|
+
// Env access denied — treat as unset.
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Locate the repo's git directories. `.git` is usually a directory, but in a `git worktree` (this
|
|
59
|
+
* app is often run from one) or a submodule it is a FILE containing `gitdir: <path>`, so a naive
|
|
60
|
+
* `${REPO_ROOT}/.git/HEAD` read returns null even on a live checkout. Resolve both the (possibly
|
|
61
|
+
* per-worktree) git dir that holds `HEAD` and the COMMON dir that holds loose refs / `packed-refs`.
|
|
62
|
+
*/
|
|
63
|
+
function resolveGitDirs(): { gitDir: string; commonDir: string } | null {
|
|
64
|
+
const dotGit = join(REPO_ROOT, ".git");
|
|
65
|
+
// Ordinary checkout: `.git` is a directory and `HEAD` sits directly inside.
|
|
66
|
+
if (readText(join(dotGit, "HEAD")) != null) {
|
|
67
|
+
return { gitDir: dotGit, commonDir: dotGit };
|
|
68
|
+
}
|
|
69
|
+
// Linked worktree / submodule: `.git` is a file pointing at the real git dir.
|
|
70
|
+
const pointer = readText(dotGit);
|
|
71
|
+
const match = pointer ? /^gitdir:\s*(.+?)\s*$/m.exec(pointer) : null;
|
|
72
|
+
if (!match) return null;
|
|
73
|
+
const target = match[1].trim();
|
|
74
|
+
const gitDir = isAbsolute(target) ? target : resolve(REPO_ROOT, target);
|
|
75
|
+
// A linked worktree keeps its own HEAD in `gitDir` but shares refs via the common dir, named by
|
|
76
|
+
// the `commondir` file (e.g. "../..").
|
|
77
|
+
const common = readText(join(gitDir, "commondir"));
|
|
78
|
+
const commonDir = common?.trim()
|
|
79
|
+
? (isAbsolute(common.trim()) ? common.trim() : resolve(gitDir, common.trim()))
|
|
80
|
+
: gitDir;
|
|
81
|
+
return { gitDir, commonDir };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The app's own `package.json` (read once, reused by name + version). */
|
|
85
|
+
function appPackage(): Record<string, unknown> | null {
|
|
86
|
+
return readJson(join(REPO_ROOT, "package.json"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The app's name from `package.json`, scope-stripped (e.g. "@foo/bar" → "bar"). */
|
|
90
|
+
function appName(pkg: Record<string, unknown> | null): string {
|
|
91
|
+
const raw = typeof pkg?.name === "string" ? pkg.name.trim() : "";
|
|
92
|
+
if (!raw) return "nano-workforce";
|
|
93
|
+
const unscoped = raw.startsWith("@") ? raw.slice(raw.indexOf("/") + 1) : raw;
|
|
94
|
+
return unscoped || "nano-workforce";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The app's own version from its `package.json`. */
|
|
98
|
+
function appVersion(pkg: Record<string, unknown> | null): string | null {
|
|
99
|
+
return typeof pkg?.version === "string" ? pkg.version : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The installed `@nanobpm/urban` version (the runtime that materializes the whole app). */
|
|
103
|
+
function urbanVersion(): string | null {
|
|
104
|
+
const pkg = readJson(join(REPO_ROOT, "node_modules", "@nanobpm", "urban", "package.json"));
|
|
105
|
+
return typeof pkg?.version === "string" ? pkg.version : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The git commit the working tree is on, read straight from `.git` so it reflects the ACTUAL
|
|
110
|
+
* checked-out code — not a value baked at some earlier build. Resolves a symbolic `HEAD`
|
|
111
|
+
* (`ref: refs/heads/…`) via the loose ref file (checking both the per-worktree and common dirs),
|
|
112
|
+
* falling back to `packed-refs`. An explicit `NANO_WORKFORCE_GIT_SHA` env var wins (for deploys
|
|
113
|
+
* that ship without a `.git` directory).
|
|
114
|
+
*/
|
|
115
|
+
function gitSha(): string | null {
|
|
116
|
+
const env = envVar("NANO_WORKFORCE_GIT_SHA");
|
|
117
|
+
if (env) return env;
|
|
118
|
+
|
|
119
|
+
const dirs = resolveGitDirs();
|
|
120
|
+
if (dirs == null) return null;
|
|
121
|
+
const head = readText(join(dirs.gitDir, "HEAD"));
|
|
122
|
+
if (head == null) return null;
|
|
123
|
+
|
|
124
|
+
const ref = head.trim();
|
|
125
|
+
if (!ref.startsWith("ref:")) {
|
|
126
|
+
// Detached HEAD — the file already holds the commit sha.
|
|
127
|
+
return ref || null;
|
|
128
|
+
}
|
|
129
|
+
const refPath = ref.slice(4).trim(); // e.g. "refs/heads/main"
|
|
130
|
+
// A loose ref may live in the per-worktree dir or the common dir; check both.
|
|
131
|
+
const loose = readText(join(dirs.gitDir, refPath)) ?? readText(join(dirs.commonDir, refPath));
|
|
132
|
+
if (loose != null && loose.trim()) return loose.trim();
|
|
133
|
+
|
|
134
|
+
// Packed refs fallback (always in the common dir): lines of "<sha> <refname>".
|
|
135
|
+
const packed = readText(join(dirs.commonDir, "packed-refs"));
|
|
136
|
+
if (packed != null) {
|
|
137
|
+
for (const line of packed.split("\n")) {
|
|
138
|
+
const trimmed = line.trim();
|
|
139
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("^")) continue;
|
|
140
|
+
const [sha, name] = trimmed.split(/\s+/, 2);
|
|
141
|
+
if (name === refPath) return sha;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The current branch name from `HEAD`, or `null` when detached / unavailable. */
|
|
148
|
+
function gitBranch(): string | null {
|
|
149
|
+
const dirs = resolveGitDirs();
|
|
150
|
+
if (dirs == null) return null;
|
|
151
|
+
const head = readText(join(dirs.gitDir, "HEAD"));
|
|
152
|
+
if (head == null) return null;
|
|
153
|
+
const ref = head.trim();
|
|
154
|
+
if (!ref.startsWith("ref:")) return null;
|
|
155
|
+
const refPath = ref.slice(4).trim();
|
|
156
|
+
return refPath.startsWith("refs/heads/") ? refPath.slice("refs/heads/".length) : refPath;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function runtime(): string {
|
|
160
|
+
const proc = globalThis.process;
|
|
161
|
+
// Deno exposes `Deno.version.deno`; Node exposes `process.version` (e.g. "v24.15.0").
|
|
162
|
+
const deno = (globalThis as { Deno?: { version?: { deno?: string } } }).Deno;
|
|
163
|
+
if (deno?.version?.deno) return `deno ${deno.version.deno}`;
|
|
164
|
+
if (proc?.version) return `node ${proc.version}`;
|
|
165
|
+
return "unknown";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface VersionInfo {
|
|
169
|
+
name: string;
|
|
170
|
+
version: string | null;
|
|
171
|
+
urbanVersion: string | null;
|
|
172
|
+
gitSha: string | null;
|
|
173
|
+
gitBranch: string | null;
|
|
174
|
+
runtime: string;
|
|
175
|
+
pid: number | null;
|
|
176
|
+
startedAt: string;
|
|
177
|
+
uptimeSeconds: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Everything except `uptimeSeconds` is fixed for the life of the process, so probe the working
|
|
182
|
+
* tree ONCE at module load rather than re-reading files (`.git`, package.jsons) on every request.
|
|
183
|
+
*/
|
|
184
|
+
const STATIC: Omit<VersionInfo, "uptimeSeconds"> = (() => {
|
|
185
|
+
const proc = globalThis.process;
|
|
186
|
+
const pkg = appPackage();
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
name: appName(pkg),
|
|
189
|
+
version: appVersion(pkg),
|
|
190
|
+
urbanVersion: urbanVersion(),
|
|
191
|
+
gitSha: gitSha(),
|
|
192
|
+
gitBranch: gitBranch(),
|
|
193
|
+
runtime: runtime(),
|
|
194
|
+
pid: typeof proc?.pid === "number" ? proc.pid : null,
|
|
195
|
+
startedAt: STARTED_AT.toISOString(),
|
|
196
|
+
});
|
|
197
|
+
})();
|
|
198
|
+
|
|
199
|
+
/** Gather the running app's identity. Cheap and side-effect-free — safe to call per request. */
|
|
200
|
+
export function buildVersionInfo(): VersionInfo {
|
|
201
|
+
return {
|
|
202
|
+
...STATIC,
|
|
203
|
+
uptimeSeconds: Math.round((Date.now() - STARTED_AT.getTime()) / 1000),
|
|
204
|
+
};
|
|
205
|
+
}
|
package/nano.app.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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/epic.page.json
CHANGED
|
@@ -23,10 +23,22 @@
|
|
|
23
23
|
"type": "text",
|
|
24
24
|
"id": "subtitle",
|
|
25
25
|
"props": {
|
|
26
|
-
"text": "
|
|
26
|
+
"text": "Hand an issue to the fleet to plan and implement it, then track each plan's review trace, wave state, merge-exclusion graph, coordination notes, and trial-merge gate results.",
|
|
27
27
|
"variant": "sub"
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
"type": "actionForm",
|
|
32
|
+
"id": "plan-submit",
|
|
33
|
+
"props": {
|
|
34
|
+
"title": "Hand an issue to the fleet",
|
|
35
|
+
"submitLabel": "Plan & implement",
|
|
36
|
+
"action": { "kind": "startProcess", "process": "plan-fanout" },
|
|
37
|
+
"fields": [
|
|
38
|
+
{ "key": "issue", "label": "owner/repo#123 or a GitHub issue URL", "type": "text" }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
30
42
|
{
|
|
31
43
|
"type": "dataGrid",
|
|
32
44
|
"id": "epic-plans",
|
|
@@ -54,7 +66,7 @@
|
|
|
54
66
|
],
|
|
55
67
|
"columns": [
|
|
56
68
|
{ "field": "plan_key", "header": "Issue", "linkField": "issue_url" },
|
|
57
|
-
{ "field": "status", "header": "Status" },
|
|
69
|
+
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
58
70
|
{ "field": "task_count", "header": "Tasks" },
|
|
59
71
|
{ "field": "open_task_id", "header": "Open escalation" },
|
|
60
72
|
{ "field": "updated_at", "header": "Updated" }
|
package/pages/home.page.json
CHANGED
|
@@ -40,18 +40,6 @@
|
|
|
40
40
|
]
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
|
-
{
|
|
44
|
-
"type": "actionForm",
|
|
45
|
-
"id": "plan-submit",
|
|
46
|
-
"props": {
|
|
47
|
-
"title": "Hand an issue to the fleet",
|
|
48
|
-
"submitLabel": "Plan & implement",
|
|
49
|
-
"action": { "kind": "startProcess", "process": "plan-fanout" },
|
|
50
|
-
"fields": [
|
|
51
|
-
{ "key": "issue", "label": "owner/repo#123 or a GitHub issue URL", "type": "text" }
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
43
|
{
|
|
56
44
|
"type": "dataGrid",
|
|
57
45
|
"id": "prs",
|
|
@@ -83,7 +71,7 @@
|
|
|
83
71
|
],
|
|
84
72
|
"columns": [
|
|
85
73
|
{ "field": "pr_key", "header": "PR", "linkField": "url" },
|
|
86
|
-
{ "field": "status", "header": "Status" },
|
|
74
|
+
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
87
75
|
{ "field": "incident_message", "header": "Incident" },
|
|
88
76
|
{ "field": "current_round", "header": "Round" },
|
|
89
77
|
{ "field": "active_worker", "header": "Agent" },
|
|
@@ -195,7 +183,7 @@
|
|
|
195
183
|
],
|
|
196
184
|
"columns": [
|
|
197
185
|
{ "field": "plan_key", "header": "Issue" },
|
|
198
|
-
{ "field": "status", "header": "Status" },
|
|
186
|
+
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
199
187
|
{ "field": "task_count", "header": "Tasks" },
|
|
200
188
|
{ "field": "updated_at", "header": "Updated" }
|
|
201
189
|
],
|