@nanobpm/nano-workforce 0.126.0 → 0.128.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/invariants.yml +8 -0
- package/.github/workflows/pr-title-lint.yml +9 -1
- package/.github/workflows/release.yml +37 -8
- package/AGENTS.md +37 -0
- package/CHANGELOG.md +14 -0
- package/app/agentCompletion.ts +11 -0
- package/app/agentic/cockpit/cockpit-route.test.ts +21 -0
- package/app/agentic/cockpit/cockpit-route.ts +17 -0
- package/app/agentic/cockpit/index.ts +16 -0
- package/app/agentic/cockpit/supply-boot-past.test.ts +44 -0
- package/app/agentic/cockpit/supply-boot.test.ts +2 -2
- package/app/agentic/cockpit/supply-boot.ts +76 -10
- package/app/agentic/cockpit/supply-render.test.ts +13 -4
- package/app/agentic/cockpit/supply-render.ts +14 -2
- package/app/agentic/cockpit/transcript-render.ts +6 -2
- package/app/agentic/cockpit/transcript-view.ts +9 -0
- package/app/agentic/cockpit/worker-detail-render.test.ts +86 -0
- package/app/agentic/cockpit/worker-detail-render.ts +88 -0
- package/app/agentic/cockpit/worker-detail-view.ts +43 -0
- package/app/agentic/correlation-store.test.ts +99 -0
- package/app/agentic/correlation-store.ts +162 -0
- package/app/agentic/families/presence.family.test.ts +12 -0
- package/app/agentic/families/presence.family.ts +14 -0
- package/app/agentic/families/relay.family.test.ts +72 -0
- package/app/agentic/families/relay.family.ts +130 -1
- package/app/agentic/transcript-read.test.ts +55 -3
- package/app/agentic/transcript-read.ts +49 -9
- package/app/agentic/vocab/demand-report.test.ts +23 -10
- package/app/pollUserTasks.test.ts +66 -2
- package/app/service.ts +21 -8
- package/app/tasksPage.test.ts +44 -0
- package/app/userTasks.test.ts +19 -0
- package/app/userTasks.ts +13 -1
- package/db/migrations/077_user_tasks_form_key.sql +25 -0
- package/db/migrations/078_agentic_correlation.sql +32 -0
- package/docs/adr/0006-delivery-units-one-representation.md +59 -20
- package/e2e/delivery-graph.e2e.ts +7 -0
- package/e2e/feature-preflight.e2e.ts +7 -0
- package/e2e/inter-epic-dependency.e2e.ts +8 -0
- package/e2e/plan-fanout-preflight.e2e.ts +7 -0
- package/e2e/plan-fanout-sla.e2e.ts +4 -3
- package/e2e/plan-fanout.e2e.ts +2 -2
- package/e2e/readiness-gate.e2e.ts +8 -37
- package/e2e/support/probe-exec.test.ts +78 -0
- package/e2e/support/probe-exec.ts +69 -0
- package/e2e/support/time.test.ts +42 -0
- package/e2e/support/time.ts +33 -0
- package/openapi.yaml +26 -0
- package/operations/getAgenticTranscript.ts +3 -2
- package/operations/listAgenticTranscripts.ts +2 -1
- package/package.json +3 -3
- package/pages/cockpit/cockpit.css +65 -2
- package/pages/cockpit/mount.js +187 -16
- package/pages/tasks.page.json +24 -554
|
@@ -23,34 +23,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
23
23
|
import { after, before, describe, test } from "node:test";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { bootTestApp, type TestApp } from "@nanobpm/urban-testkit";
|
|
26
|
-
import
|
|
27
|
-
import { __setProbeExecForTest } from "../workers/readiness-probe/worker.ts";
|
|
28
|
-
|
|
29
|
-
// A synchronous, in-memory ProbeExec so the probe resolves WITHIN the testkit's virtual-clock drain
|
|
30
|
-
// fixpoint instead of spawning a REAL subprocess (real-time work `settle()` cannot deterministically
|
|
31
|
-
// await — issue #450). It maps the hermetic shell builtins these scenarios use to a deterministic
|
|
32
|
-
// `CommandResult` — `true` → exit 0 (green), `false` → exit 1 (never green) — mirroring the real
|
|
33
|
-
// commands' semantics exactly, but with zero real time. Any OTHER command, or any HTTP call, is an
|
|
34
|
-
// unintended probe escape: because `probeSingleShot` catches a thrown/rejected probe error and folds
|
|
35
|
-
// it into a silent "not ready", an escape would otherwise be INVISIBLE and could let a bounded
|
|
36
|
-
// not-ready scenario still pass, masking a regression (reviewer note). So we record every escape and
|
|
37
|
-
// assert none occurred in teardown, failing the suite loudly instead of swallowing it.
|
|
38
|
-
const unexpectedProbeIO: string[] = [];
|
|
39
|
-
const deterministicExec: ProbeExec = {
|
|
40
|
-
run(command: string): Promise<CommandResult> {
|
|
41
|
-
const cmd = command.trim();
|
|
42
|
-
if (cmd !== "true" && cmd !== "false") {
|
|
43
|
-
unexpectedProbeIO.push(`command: ${cmd}`);
|
|
44
|
-
return Promise.resolve({ code: 127, stdout: "", stderr: "" });
|
|
45
|
-
}
|
|
46
|
-
const code = cmd === "true" ? 0 : 1;
|
|
47
|
-
return Promise.resolve({ code, stdout: "", stderr: "" });
|
|
48
|
-
},
|
|
49
|
-
httpGet(url: string): Promise<never> {
|
|
50
|
-
unexpectedProbeIO.push(`http: ${url}`);
|
|
51
|
-
return Promise.reject(new Error(`readiness-gate e2e: unexpected real HTTP probe (command probes only)`));
|
|
52
|
-
},
|
|
53
|
-
};
|
|
26
|
+
import { deterministicProbeSeam } from "./support/probe-exec.ts";
|
|
54
27
|
|
|
55
28
|
const APP_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
56
29
|
let dbSeq = 0;
|
|
@@ -60,7 +33,7 @@ const GITHUB_ENV_OVERRIDES: Record<string, string> = {
|
|
|
60
33
|
GITHUB_TOKEN: "",
|
|
61
34
|
};
|
|
62
35
|
const savedEnv = new Map<string, string | undefined>();
|
|
63
|
-
|
|
36
|
+
const probeSeam = deterministicProbeSeam("readiness-gate e2e");
|
|
64
37
|
|
|
65
38
|
interface TakenFlow {
|
|
66
39
|
from: string;
|
|
@@ -94,7 +67,7 @@ describe("nano-workforce artifact-readiness wait-gate (readiness-gate.bpmn)", ()
|
|
|
94
67
|
// clock (issue #450). Scenario-agnostic: it maps each scenario's command by string. Capture the
|
|
95
68
|
// prior override and restore exactly that in teardown, so the seam is restored to its real prior
|
|
96
69
|
// state rather than assuming production.
|
|
97
|
-
|
|
70
|
+
probeSeam.install();
|
|
98
71
|
});
|
|
99
72
|
|
|
100
73
|
after(() => {
|
|
@@ -102,13 +75,11 @@ describe("nano-workforce artifact-readiness wait-gate (readiness-gate.bpmn)", ()
|
|
|
102
75
|
if (v === undefined) delete process.env[k];
|
|
103
76
|
else process.env[k] = v;
|
|
104
77
|
}
|
|
105
|
-
// Restore the prior exec
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
// still pass, masking a regression.
|
|
111
|
-
assert.deepEqual(unexpectedProbeIO, [], `readiness-gate e2e saw unexpected probe I/O: ${unexpectedProbeIO.join(", ")}`);
|
|
78
|
+
// Restore the prior exec (the seam must never outlive this suite) and fail loudly if the probe
|
|
79
|
+
// ever escaped the hermetic `true`/`false` builtins — `probeSingleShot` folds a probe error into
|
|
80
|
+
// a silent "not ready", so without this an escape would be invisible and could let a bounded
|
|
81
|
+
// not-ready scenario still pass, masking a regression.
|
|
82
|
+
probeSeam.restoreAndAssertHermetic();
|
|
112
83
|
});
|
|
113
84
|
|
|
114
85
|
test("READY: a green probe publishes readiness-ready and the gate releases through wait-ready → gate-ready", async () => {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Negative-path coverage for the deterministic probe seam (e2e/support/probe-exec.ts).
|
|
2
|
+
//
|
|
3
|
+
// The wired readiness-gate suites only ever route the hermetic `true`/`false` builtins through the
|
|
4
|
+
// seam, so a regression in RECORDING or REJECTING an unexpected command/HTTP call — the seam's whole
|
|
5
|
+
// reason to exist — would leave every one of them green while silently swallowing an escape. This
|
|
6
|
+
// exercises the escape contract directly: a non-hermetic command or a real HTTP probe must be
|
|
7
|
+
// recorded and make `restoreAndAssertHermetic()` fail, and the recorded escape must never leak the
|
|
8
|
+
// raw command or a URL's credential material into the teardown assertion (ADR 0004 pinned
|
|
9
|
+
// decision 2 — mirrors production `redactTarget`).
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { test } from "node:test";
|
|
12
|
+
import { __setProbeExecForTest } from "../../workers/readiness-probe/worker.ts";
|
|
13
|
+
import { deterministicProbeSeam } from "./probe-exec.ts";
|
|
14
|
+
|
|
15
|
+
/** Install the seam, then hand back the exact `ProbeExec` it wired into the worker so a test can
|
|
16
|
+
* route probes through the real installed seam (not a private copy). Uses the documented
|
|
17
|
+
* set-returns-previous contract of `__setProbeExecForTest` to read the current override, then puts
|
|
18
|
+
* it straight back so the seam's own restore still returns the DB to the prior exec. */
|
|
19
|
+
function installAndCaptureExec(seam: ReturnType<typeof deterministicProbeSeam>) {
|
|
20
|
+
seam.install();
|
|
21
|
+
const installed = __setProbeExecForTest(undefined);
|
|
22
|
+
assert.ok(installed, "install() must have wired a ProbeExec into the worker");
|
|
23
|
+
__setProbeExecForTest(installed);
|
|
24
|
+
return installed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("hermetic true/false probes leave the seam clean", async () => {
|
|
28
|
+
const seam = deterministicProbeSeam("hermetic");
|
|
29
|
+
const exec = installAndCaptureExec(seam);
|
|
30
|
+
|
|
31
|
+
assert.deepEqual(await exec.run("true", {}), { code: 0, stdout: "", stderr: "" });
|
|
32
|
+
assert.deepEqual(await exec.run("false", {}), { code: 1, stdout: "", stderr: "" });
|
|
33
|
+
|
|
34
|
+
// No escape recorded — teardown passes.
|
|
35
|
+
seam.restoreAndAssertHermetic();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a non-hermetic command escapes, fails teardown, and never leaks the raw command", async () => {
|
|
39
|
+
const seam = deterministicProbeSeam("cmd-escape");
|
|
40
|
+
const exec = installAndCaptureExec(seam);
|
|
41
|
+
|
|
42
|
+
const secret = "curl https://user:supersecret@host/health?token=abc123";
|
|
43
|
+
const result = await exec.run(secret, {});
|
|
44
|
+
// The escaping command still resolves to a non-green result so a bounded not-ready scenario holds.
|
|
45
|
+
assert.equal(result.code, 127);
|
|
46
|
+
|
|
47
|
+
assert.throws(
|
|
48
|
+
() => seam.restoreAndAssertHermetic(),
|
|
49
|
+
(err: unknown) => {
|
|
50
|
+
const msg = String(err);
|
|
51
|
+
assert.match(msg, /cmd-escape saw unexpected probe I\/O/);
|
|
52
|
+
assert.doesNotMatch(msg, /supersecret/, "the credential must never reach teardown output");
|
|
53
|
+
assert.doesNotMatch(msg, /abc123/, "the token must never reach teardown output");
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a real HTTP probe escapes, fails teardown, and redacts the URL's credential material", async () => {
|
|
60
|
+
const seam = deterministicProbeSeam("http-escape");
|
|
61
|
+
const exec = installAndCaptureExec(seam);
|
|
62
|
+
|
|
63
|
+
await assert.rejects(
|
|
64
|
+
exec.httpGet("https://user:tok@host/ready?apikey=zzz999", {}),
|
|
65
|
+
/http-escape: unexpected real HTTP probe/,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
assert.throws(
|
|
69
|
+
() => seam.restoreAndAssertHermetic(),
|
|
70
|
+
(err: unknown) => {
|
|
71
|
+
const msg = String(err);
|
|
72
|
+
assert.match(msg, /http-escape saw unexpected probe I\/O/);
|
|
73
|
+
assert.doesNotMatch(msg, /zzz999/, "the query token must never reach teardown output");
|
|
74
|
+
assert.doesNotMatch(msg, /user:tok/, "the userinfo credential must never reach teardown output");
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Deterministic readiness-probe exec for e2es driven by the testkit's VIRTUAL clock (issue #450).
|
|
2
|
+
//
|
|
3
|
+
// Production `defaultProbeExec` (app/readiness.ts) runs a `command` probe as a REAL
|
|
4
|
+
// `node:child_process` subprocess. That subprocess resolves on the wall clock, spanning macrotasks
|
|
5
|
+
// the urban-testkit's virtual-clock `settle()`/`drain()` fixpoint cannot deterministically await —
|
|
6
|
+
// so `settle()` can return BEFORE the probe publishes `readiness-ready`, and a gate-flow assertion
|
|
7
|
+
// (e.g. `pf_gw->pf_end`) races the subprocess. That race is the flake behind feature-preflight /
|
|
8
|
+
// plan-fanout-preflight failing intermittently (green probe logged, gate flow not yet taken).
|
|
9
|
+
//
|
|
10
|
+
// This seam injects a synchronous, in-memory `ProbeExec` (via the worker's `__setProbeExecForTest`)
|
|
11
|
+
// so the probe resolves WITHIN the drain fixpoint — no real spawn, no wall-clock race. It maps the
|
|
12
|
+
// hermetic shell builtins the gate e2es use to a deterministic `CommandResult` — `true` → exit 0
|
|
13
|
+
// (green), `false` → exit 1 (never green) — mirroring the real commands exactly, with zero real time.
|
|
14
|
+
//
|
|
15
|
+
// Any OTHER command, or any HTTP call, is an unintended probe escape: `probeSingleShot` folds a
|
|
16
|
+
// thrown/rejected probe error into a silent "not ready", so an escape would be INVISIBLE and could
|
|
17
|
+
// let a bounded not-ready scenario still pass, masking a regression. Every escape is recorded and
|
|
18
|
+
// asserted-none in teardown, failing the suite loudly instead of swallowing it.
|
|
19
|
+
//
|
|
20
|
+
// Single source of truth shared by every readiness-gate e2e (readiness-gate, feature-preflight,
|
|
21
|
+
// plan-fanout-preflight, delivery-graph, inter-epic-dependency) so the deterministic-exec contract
|
|
22
|
+
// can never drift between them.
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { type CommandResult, type ProbeExec, redactString } from "../../app/readiness.ts";
|
|
25
|
+
import { __setProbeExecForTest } from "../../workers/readiness-probe/worker.ts";
|
|
26
|
+
|
|
27
|
+
export interface DeterministicProbeSeam {
|
|
28
|
+
/** Install the deterministic exec — call once in the suite's `before`. */
|
|
29
|
+
install(): void;
|
|
30
|
+
/** Restore the prior exec and assert no probe escaped the hermetic `true`/`false` builtins — call
|
|
31
|
+
* once in the suite's `after`. */
|
|
32
|
+
restoreAndAssertHermetic(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Build a deterministic probe seam scoped to one suite. `label` is used in the escape assertion
|
|
36
|
+
* message and the unexpected-HTTP error, so a failure names the offending suite. */
|
|
37
|
+
export function deterministicProbeSeam(label: string): DeterministicProbeSeam {
|
|
38
|
+
const escapes: string[] = [];
|
|
39
|
+
const exec: ProbeExec = {
|
|
40
|
+
run(command: string): Promise<CommandResult> {
|
|
41
|
+
const cmd = command.trim();
|
|
42
|
+
if (cmd !== "true" && cmd !== "false") {
|
|
43
|
+
// A `command` target is an arbitrary shell snippet that can embed a secret, so — exactly as
|
|
44
|
+
// production `redactTarget` does (app/readiness.ts, ADR 0004 pinned decision 2) — record only
|
|
45
|
+
// a fixed placeholder, never the raw command, so an escape can't leak credentials into the
|
|
46
|
+
// teardown assertion at `restoreAndAssertHermetic()`.
|
|
47
|
+
escapes.push("command: <redacted>");
|
|
48
|
+
return Promise.resolve({ code: 127, stdout: "", stderr: "" });
|
|
49
|
+
}
|
|
50
|
+
return Promise.resolve({ code: cmd === "true" ? 0 : 1, stdout: "", stderr: "" });
|
|
51
|
+
},
|
|
52
|
+
httpGet(url: string): Promise<never> {
|
|
53
|
+
// A probe URL can carry a token in its userinfo or query string; strip those before recording.
|
|
54
|
+
escapes.push(`http: ${redactString(url)}`);
|
|
55
|
+
return Promise.reject(new Error(`${label}: unexpected real HTTP probe (command probes only)`));
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
let saved: ProbeExec | undefined;
|
|
59
|
+
return {
|
|
60
|
+
install() {
|
|
61
|
+
saved = __setProbeExecForTest(exec);
|
|
62
|
+
},
|
|
63
|
+
restoreAndAssertHermetic() {
|
|
64
|
+
// Restore the prior exec first — the seam must never outlive this suite.
|
|
65
|
+
__setProbeExecForTest(saved);
|
|
66
|
+
assert.deepEqual(escapes, [], `${label} saw unexpected probe I/O: ${escapes.join(", ")}`);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { advancePastTimer } from "./time.ts";
|
|
4
|
+
|
|
5
|
+
// Guards the defect class behind issue #474: a long BPMN business-wait must be crossed by advancing
|
|
6
|
+
// the ENGINE clock (fire the boundary timer) + one reconcile settle — NOT by `app.advanceTime`,
|
|
7
|
+
// which steps the scheduler in lockstep and replays every 5s runtime-cadence poll across the window
|
|
8
|
+
// (~18,000× for a 25h jump; ~123s of no-op churn). This pins the mechanism so a regression back to
|
|
9
|
+
// the lockstep path — or an added `scheduler.advance` — fails here instead of silently re-slowing CI.
|
|
10
|
+
test("advancePastTimer advances the engine clock then settles once, never replaying scheduler cadence", async () => {
|
|
11
|
+
const PAST_SLA_MS = 25 * 60 * 60 * 1000;
|
|
12
|
+
const calls: string[] = [];
|
|
13
|
+
let engineMs: number | undefined;
|
|
14
|
+
|
|
15
|
+
const app = {
|
|
16
|
+
engine: {
|
|
17
|
+
advanceTime: async (ms: number) => {
|
|
18
|
+
engineMs = ms;
|
|
19
|
+
calls.push("engine.advanceTime");
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
settle: async () => {
|
|
23
|
+
calls.push("settle");
|
|
24
|
+
},
|
|
25
|
+
// Present only to catch a regression: touching the scheduler's own `advance` is the slow
|
|
26
|
+
// lockstep replay path the helper exists to avoid, so it must never be called.
|
|
27
|
+
scheduler: {
|
|
28
|
+
advance: async () => {
|
|
29
|
+
calls.push("scheduler.advance");
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
} as unknown as Parameters<typeof advancePastTimer>[0];
|
|
33
|
+
|
|
34
|
+
await advancePastTimer(app, PAST_SLA_MS);
|
|
35
|
+
|
|
36
|
+
assert.deepEqual(
|
|
37
|
+
calls,
|
|
38
|
+
["engine.advanceTime", "settle"],
|
|
39
|
+
"the engine boundary fires, then exactly one reconcile settle — the scheduler cadence is never replayed",
|
|
40
|
+
);
|
|
41
|
+
assert.equal(engineMs, PAST_SLA_MS, "the full business-wait window is applied to the engine clock");
|
|
42
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { TestApp } from "@nanobpm/urban-testkit";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advance a long BPMN **business-wait** boundary timer and reconcile once.
|
|
5
|
+
*
|
|
6
|
+
* A business wait — "park at this task until the 24h SLA elapses, then auto-escalate" — is
|
|
7
|
+
* modelled as a BPMN interrupting boundary timer on the ENGINE clock: durable, engine-owned, and
|
|
8
|
+
* already virtual under the engine clock. This is the line camunda/orchestration-cluster-api-js#450
|
|
9
|
+
* draws: *"Long/business waits are BPMN timer events … not [runtime-cadence] `sleep`/poll."* To
|
|
10
|
+
* cross such a wait we advance the ENGINE clock so the boundary fires, then {@link TestApp.settle}
|
|
11
|
+
* once to drain the follow-on token flow.
|
|
12
|
+
*
|
|
13
|
+
* This deliberately does **not** call {@link TestApp.advanceTime}, which steps the virtual-clock
|
|
14
|
+
* scheduler in lockstep with the engine and therefore REPLAYS every short runtime-cadence poll —
|
|
15
|
+
* the 5s `instanceTracking` reconcilers — once per interval across the whole window. A 25h jump
|
|
16
|
+
* replays each poller ~18,000× (measured: ~123s of pure no-op reconcile churn *per call*, which is
|
|
17
|
+
* essentially the entire e2e wall-clock). Those replays are runtime cadence, not the business wait,
|
|
18
|
+
* and #450 treats such busy-replays as a bug to *surface*, not to coalesce away in the scheduler.
|
|
19
|
+
* Advancing engine time + one settle fires the same boundary in ~2ms.
|
|
20
|
+
*
|
|
21
|
+
* Use ONLY when the assertions target **engine state** — taken sequence-flows (`app.snapshot()`) or
|
|
22
|
+
* instance state — which is populated by `engine.advanceTime` + `engine.drain` (run inside
|
|
23
|
+
* `settle`). Do NOT use it when an assertion depends on a **read model the reconcile pollers
|
|
24
|
+
* project**: for that the pollers must actually run, so use {@link TestApp.advanceTime}. The
|
|
25
|
+
* scheduler's virtual clock intentionally does not track this jump.
|
|
26
|
+
*/
|
|
27
|
+
export async function advancePastTimer(
|
|
28
|
+
app: Pick<TestApp, "engine" | "settle">,
|
|
29
|
+
ms: number,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
await app.engine.advanceTime(ms);
|
|
32
|
+
await app.settle();
|
|
33
|
+
}
|
package/openapi.yaml
CHANGED
|
@@ -646,6 +646,16 @@ components:
|
|
|
646
646
|
planKey:
|
|
647
647
|
type: string
|
|
648
648
|
description: The plan / epic key this job was part of (e.g. owner/repo#142), when still known (advisory).
|
|
649
|
+
instance:
|
|
650
|
+
type: string
|
|
651
|
+
description: The worker instance that ran the session, recovered from durable attribution — present even
|
|
652
|
+
after the worker exited or the process restarted (advisory).
|
|
653
|
+
identity:
|
|
654
|
+
type: string
|
|
655
|
+
description: The worker's durable identity (presence identity), when recorded (advisory).
|
|
656
|
+
host:
|
|
657
|
+
type: string
|
|
658
|
+
description: The worker's host, when recorded (advisory).
|
|
649
659
|
AgenticTranscriptList:
|
|
650
660
|
type: object
|
|
651
661
|
description: The list of captured agent sessions (past + open) — the cockpit "past sessions" feed.
|
|
@@ -742,6 +752,15 @@ components:
|
|
|
742
752
|
planKey:
|
|
743
753
|
type: string
|
|
744
754
|
description: The plan / epic key, when still known (advisory).
|
|
755
|
+
instance:
|
|
756
|
+
type: string
|
|
757
|
+
description: The worker instance that ran the session, from durable attribution (advisory).
|
|
758
|
+
identity:
|
|
759
|
+
type: string
|
|
760
|
+
description: The worker's durable identity, when recorded (advisory).
|
|
761
|
+
host:
|
|
762
|
+
type: string
|
|
763
|
+
description: The worker's host, when recorded (advisory).
|
|
745
764
|
entries:
|
|
746
765
|
type: array
|
|
747
766
|
description: The retained chunks with `offset >= from`, in offset order.
|
|
@@ -2448,6 +2467,13 @@ paths:
|
|
|
2448
2467
|
schema:
|
|
2449
2468
|
type: string
|
|
2450
2469
|
description: Return only transcripts whose (still-known) correlation names this plan / epic key.
|
|
2470
|
+
- name: instance
|
|
2471
|
+
in: query
|
|
2472
|
+
required: false
|
|
2473
|
+
schema:
|
|
2474
|
+
type: string
|
|
2475
|
+
description: Return only sessions run by this worker instance (durable attribution) — powers the
|
|
2476
|
+
per-worker history view. Survives worker exit / process restart.
|
|
2451
2477
|
- name: since
|
|
2452
2478
|
in: query
|
|
2453
2479
|
required: false
|
|
@@ -28,13 +28,14 @@ export default defineOperation("getAgenticTranscript", async ({ params, query, r
|
|
|
28
28
|
return { status: 400, body: { error: "invalid from: expected a non-negative integer offset" } };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const service = currentRelayTranscriptService();
|
|
32
|
+
const store = service?.store;
|
|
32
33
|
if (!store) {
|
|
33
34
|
// No transcript store mounted (relay unmounted or unpersisted) - nothing to replay.
|
|
34
35
|
return { status: 404, body: { error: "no transcript for stream" } };
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
const data = readTranscriptFrom(params.stream, from, store, currentCorrelation());
|
|
38
|
+
const data = readTranscriptFrom(params.stream, from, store, currentCorrelation(), service?.correlationStore);
|
|
38
39
|
if (data === undefined) {
|
|
39
40
|
return { status: 404, body: { error: "no transcript for stream" } };
|
|
40
41
|
}
|
|
@@ -47,10 +47,11 @@ export default defineOperation("listAgenticTranscripts", async ({ query, req },
|
|
|
47
47
|
...(query.jobKey !== undefined ? { jobKey: query.jobKey } : {}),
|
|
48
48
|
...(query.processInstanceKey !== undefined ? { processInstanceKey: query.processInstanceKey } : {}),
|
|
49
49
|
...(query.planKey !== undefined ? { planKey: query.planKey } : {}),
|
|
50
|
+
...(query.instance !== undefined ? { instance: query.instance } : {}),
|
|
50
51
|
...(query.since !== undefined ? { since: query.since } : {}),
|
|
51
52
|
...(query.until !== undefined ? { until: query.until } : {}),
|
|
52
53
|
};
|
|
53
|
-
const transcripts = listTranscripts(store, currentCorrelation(), filter);
|
|
54
|
+
const transcripts = listTranscripts(store, currentCorrelation(), filter, service?.correlationStore);
|
|
54
55
|
const body: AgenticTranscriptList = {
|
|
55
56
|
count: transcripts.length,
|
|
56
57
|
generatedAt: new Date().toISOString(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.128.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",
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"lint:fix": "biome check --write app operations workers pages components scripts e2e main.ts"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@nanobpm/agentic": "^0.
|
|
61
|
+
"@nanobpm/agentic": "^0.4.0",
|
|
62
62
|
"@nanobpm/urban": "^0.80.0",
|
|
63
63
|
"bpmn-auto-layout": "^2.0.0-alpha.2"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@biomejs/biome": "^2.4.11",
|
|
67
|
-
"@nanobpm/urban-testkit": "^0.
|
|
67
|
+
"@nanobpm/urban-testkit": "^0.12.16",
|
|
68
68
|
"@nanobpm/workflow": "^0.14.0",
|
|
69
69
|
"@semantic-release/changelog": "^6.0.3",
|
|
70
70
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -119,7 +119,10 @@
|
|
|
119
119
|
.cockpit-dot[data-liveness="stale"] { background: var(--cockpit-amber); }
|
|
120
120
|
.cockpit-dot[data-liveness="down"] { background: var(--cockpit-red); }
|
|
121
121
|
|
|
122
|
-
.cockpit-worker
|
|
122
|
+
.cockpit-worker,
|
|
123
|
+
.cockpit-worker-drill,
|
|
124
|
+
.cockpit-worker-detail-back,
|
|
125
|
+
.cockpit-worker-current-job {
|
|
123
126
|
background: none;
|
|
124
127
|
border: none;
|
|
125
128
|
color: var(--cockpit-text);
|
|
@@ -130,7 +133,15 @@
|
|
|
130
133
|
text-underline-offset: 2px;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
.cockpit-worker
|
|
136
|
+
.cockpit-worker-drill {
|
|
137
|
+
color: var(--cockpit-muted);
|
|
138
|
+
font-size: 12px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.cockpit-worker:hover,
|
|
142
|
+
.cockpit-worker-drill:hover,
|
|
143
|
+
.cockpit-worker-detail-back:hover,
|
|
144
|
+
.cockpit-worker-current-job:hover { color: #58a6ff; }
|
|
134
145
|
|
|
135
146
|
.cockpit-supply-process { color: var(--cockpit-muted); }
|
|
136
147
|
|
|
@@ -220,6 +231,58 @@
|
|
|
220
231
|
padding: 8px 0;
|
|
221
232
|
}
|
|
222
233
|
|
|
234
|
+
/* ── Worker detail route (#/cockpit/worker/<instance>): header + current job + filtered history. ── */
|
|
235
|
+
|
|
236
|
+
.cockpit-worker-detail {
|
|
237
|
+
display: grid;
|
|
238
|
+
gap: 12px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.cockpit-worker-detail-back {
|
|
242
|
+
justify-self: start;
|
|
243
|
+
color: var(--cockpit-muted);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.cockpit-worker-detail-header {
|
|
247
|
+
display: grid;
|
|
248
|
+
gap: 8px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.cockpit-worker-detail-meta {
|
|
252
|
+
display: grid;
|
|
253
|
+
gap: 8px;
|
|
254
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
255
|
+
margin: 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.cockpit-worker-detail-meta-item {
|
|
259
|
+
border: 1px solid rgba(34, 48, 65, 0.7);
|
|
260
|
+
border-radius: 6px;
|
|
261
|
+
padding: 8px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.cockpit-worker-detail-meta-item dt {
|
|
265
|
+
color: var(--cockpit-muted);
|
|
266
|
+
font-size: 11px;
|
|
267
|
+
text-transform: uppercase;
|
|
268
|
+
letter-spacing: 0.04em;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.cockpit-worker-detail-meta-item dd {
|
|
272
|
+
margin: 2px 0 0;
|
|
273
|
+
overflow-wrap: anywhere;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.cockpit-worker-current {
|
|
277
|
+
border-top: 1px solid rgba(34, 48, 65, 0.7);
|
|
278
|
+
padding-top: 10px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.cockpit-worker-current-empty,
|
|
282
|
+
.cockpit-worker-detail-empty {
|
|
283
|
+
color: var(--cockpit-muted);
|
|
284
|
+
}
|
|
285
|
+
|
|
223
286
|
/* Distinguish a live terminal from a replayed (static) past session at a glance. */
|
|
224
287
|
.cockpit-terminal[data-terminal-mode="replay"] {
|
|
225
288
|
border-color: #8957e5;
|