@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.
Files changed (54) hide show
  1. package/.github/workflows/invariants.yml +8 -0
  2. package/.github/workflows/pr-title-lint.yml +9 -1
  3. package/.github/workflows/release.yml +37 -8
  4. package/AGENTS.md +37 -0
  5. package/CHANGELOG.md +14 -0
  6. package/app/agentCompletion.ts +11 -0
  7. package/app/agentic/cockpit/cockpit-route.test.ts +21 -0
  8. package/app/agentic/cockpit/cockpit-route.ts +17 -0
  9. package/app/agentic/cockpit/index.ts +16 -0
  10. package/app/agentic/cockpit/supply-boot-past.test.ts +44 -0
  11. package/app/agentic/cockpit/supply-boot.test.ts +2 -2
  12. package/app/agentic/cockpit/supply-boot.ts +76 -10
  13. package/app/agentic/cockpit/supply-render.test.ts +13 -4
  14. package/app/agentic/cockpit/supply-render.ts +14 -2
  15. package/app/agentic/cockpit/transcript-render.ts +6 -2
  16. package/app/agentic/cockpit/transcript-view.ts +9 -0
  17. package/app/agentic/cockpit/worker-detail-render.test.ts +86 -0
  18. package/app/agentic/cockpit/worker-detail-render.ts +88 -0
  19. package/app/agentic/cockpit/worker-detail-view.ts +43 -0
  20. package/app/agentic/correlation-store.test.ts +99 -0
  21. package/app/agentic/correlation-store.ts +162 -0
  22. package/app/agentic/families/presence.family.test.ts +12 -0
  23. package/app/agentic/families/presence.family.ts +14 -0
  24. package/app/agentic/families/relay.family.test.ts +72 -0
  25. package/app/agentic/families/relay.family.ts +130 -1
  26. package/app/agentic/transcript-read.test.ts +55 -3
  27. package/app/agentic/transcript-read.ts +49 -9
  28. package/app/agentic/vocab/demand-report.test.ts +23 -10
  29. package/app/pollUserTasks.test.ts +66 -2
  30. package/app/service.ts +21 -8
  31. package/app/tasksPage.test.ts +44 -0
  32. package/app/userTasks.test.ts +19 -0
  33. package/app/userTasks.ts +13 -1
  34. package/db/migrations/077_user_tasks_form_key.sql +25 -0
  35. package/db/migrations/078_agentic_correlation.sql +32 -0
  36. package/docs/adr/0006-delivery-units-one-representation.md +59 -20
  37. package/e2e/delivery-graph.e2e.ts +7 -0
  38. package/e2e/feature-preflight.e2e.ts +7 -0
  39. package/e2e/inter-epic-dependency.e2e.ts +8 -0
  40. package/e2e/plan-fanout-preflight.e2e.ts +7 -0
  41. package/e2e/plan-fanout-sla.e2e.ts +4 -3
  42. package/e2e/plan-fanout.e2e.ts +2 -2
  43. package/e2e/readiness-gate.e2e.ts +8 -37
  44. package/e2e/support/probe-exec.test.ts +78 -0
  45. package/e2e/support/probe-exec.ts +69 -0
  46. package/e2e/support/time.test.ts +42 -0
  47. package/e2e/support/time.ts +33 -0
  48. package/openapi.yaml +26 -0
  49. package/operations/getAgenticTranscript.ts +3 -2
  50. package/operations/listAgenticTranscripts.ts +2 -1
  51. package/package.json +3 -3
  52. package/pages/cockpit/cockpit.css +65 -2
  53. package/pages/cockpit/mount.js +187 -16
  54. 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 type { CommandResult, ProbeExec } from "../app/readiness.ts";
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
- let savedProbeExec: ProbeExec | undefined;
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
- savedProbeExec = __setProbeExecForTest(deterministicExec);
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 the seam must never outlive this suite.
106
- __setProbeExecForTest(savedProbeExec);
107
- // Fail loudly if the probe ever escaped the hermetic `true`/`false` builtins (an unexpected
108
- // command or any HTTP call). `probeSingleShot` folds a probe error into a silent "not ready", so
109
- // without this assertion an escape would be invisible and could let a bounded not-ready scenario
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 store = currentRelayTranscriptService()?.store;
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.126.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.1.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.11.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:hover { color: #58a6ff; }
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;