@nanobpm/nano-workforce 0.50.0 → 0.52.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.
@@ -0,0 +1,167 @@
1
+ # ADR 0002 — Escalations are user tasks + forms
2
+
3
+ Status: **Proposed.**
4
+ Date: 2026-08-13.
5
+
6
+ > **Scope note.** This is a **nano-workforce-local** ADR — it governs how *this app's* agent workforce
7
+ > models human (and agent) decision points. Platform-wide ADRs live in `Magikcraft/nano-bpm/docs/adr`
8
+ > (referenced by number + repo, e.g. "nano-bpm ADR 0026"). nano-workforce's own series continues here
9
+ > after ADR 0001.
10
+
11
+ Relates to:
12
+ nano-bpm **ADR 0026** (Urban human surfaces + run model — the `taskInbox` surface this ADR builds on:
13
+ a hosted task list backed by the engine's user-task search that renders a linked `.form` and posts
14
+ completion),
15
+ nano-bpm **ADR 0037** (execution + task listeners — the user-task lifecycle hooks this ADR leans on),
16
+ nano-bpm **ADR 0046** (agent-as-worker vs agent-in-the-node — the duality that lets an **agent** be a
17
+ task assignee, answering the same form a human would),
18
+ nano-bpm **ADR 0051** (nano-workforce — the crew orchestrator whose escalations this reshapes),
19
+ nano-bpm **ADR 0056** (the Nano agentic protocol — this ADR is the **durable** human-in-the-loop lane,
20
+ complementary to that ADR's **ephemeral** live-steering cockpit),
21
+ nano-workforce **ADR 0001** (this repo's ADR series),
22
+ and the current bespoke escalation subsystem in this repo: `app/plan.ts` (`plan_escalations`,
23
+ `plan_review_escalations`, `answerTaskEscalation`, `answerPlanEscalation`, `refreshOpenTaskEscalation`),
24
+ `app/service.ts` (the `open_escalation_*` pointer on `pull_requests`), the `pr.persist-*-escalation`
25
+ service workers, and the `feature-escalation-answered` / `plan-escalation-answered` resume messages in
26
+ `resources/processes/plan-fanout.bpmn`.
27
+
28
+ ## Context
29
+
30
+ When a fanned-out agent task cannot proceed on its own — an open question, a trial-merge conflict, a
31
+ plan-review budget cap, a stuck PR-review loop — nano-workforce **escalates**: it parks the process and
32
+ waits for a human decision. Today that is a hand-rolled subsystem, and the same shape recurs three times:
33
+
34
+ 1. **Task escalation** (`plan_escalations`, issue #25) — a fanned-out task's open question. In
35
+ `plan-fanout.bpmn`: an `exclusiveGateway` (`escalated?`) routes to a **service task**
36
+ `persist-task-escalation` (`pr.persist-task-escalation`) which writes the row + a denormalised
37
+ `open_task_escalation_id` pointer on the plan, then an **intermediate message-catch**
38
+ `wait-feature-answer` parks on `feature-escalation-answered` (correlationKey `=escalationCorrKey`).
39
+ 2. **Plan-review escalation** (`plan_review_escalations`) — a plan-review cap; a human returns a
40
+ `proceed | revise` directive. Same persist-service-task → message-catch shape
41
+ (`plan-escalation-answered`); the table is **append-only** and the review **epoch** is derived from
42
+ the count of answered rows.
43
+ 3. **PR review-loop escalation** (`open_escalation_*` columns on `pull_requests`, #597/#599) — a review
44
+ convergence that will not settle; surfaced via denormalised columns on the PR row.
45
+
46
+ Answering, in every case, means: an app worker records the answer, **mirrors** it onto the task/PR row,
47
+ **publishes the resume message**, and **re-surfaces** the next open escalation by rewriting a denormalised
48
+ "oldest open" pointer. The "form" is a bespoke Urban page that fires when a pointer is set and prints the
49
+ free-text `question`; the answer is a free-text string.
50
+
51
+ This is a **user task + form, re-implemented by hand** — and the bug tail proves it. Every incident is a
52
+ denormalised-pointer or free-text-contract failure: stale rows resurfacing a *dead* form after a re-plan
53
+ (`refreshOpenTaskEscalation`), the "addressed-escalation paradox," `blank question fabricates an
54
+ answerable escalation` (a hack to avoid an incident on an empty question), and per-run one-by-one row
55
+ cleanup. None of these can occur under a single-source-of-truth user-task lifecycle.
56
+
57
+ Crucially, **the primitives already exist**:
58
+
59
+ - The engine has **native user tasks** — `UserTaskProps`, `Command::CompleteUserTask` / `UpdateUserTask`,
60
+ task listeners (ADR 0037), and `zeebe:assignmentDefinition` / priority / schedule parsed off the
61
+ `userTask` element (`engine-core/src/bpmn.rs`, `model.rs`).
62
+ - Urban ships the **`taskInbox` surface** (ADR 0026): `GET /tasks` (list), `GET /tasks/api/tasks`
63
+ (`engine.searchUserTasks`), `POST /tasks/api/complete` (`engine.completeUserTask(key, variables)`),
64
+ rendering the linked **`.form`**. It is manifest-enabled (`surfaces.taskInbox`) and unused by nwf today.
65
+ - Forms are `.form` assets; the Urban **form editor** (the "Delphi" authoring surface) is the tool that
66
+ authors them. This ADR is that editor's **first real internal customer**.
67
+
68
+ ## Decision
69
+
70
+ **Model every decision-required escalation in nano-workforce as a native BPMN `userTask` with a linked
71
+ `.form`, surfaced through Urban's `taskInbox`, completed with typed variables that resume the process.**
72
+ Retire the bespoke `persist-escalation` service task → message-catch → resume-publish → denormalised-pointer
73
+ machinery.
74
+
75
+ ### 1. A tiered taxonomy — not everything is a task
76
+
77
+ The current code conflates three tiers; draw the line explicitly at each raise site:
78
+
79
+ | Tier | Example | Mechanism |
80
+ | --- | --- | --- |
81
+ | **Transient** | empty-status backstop, re-request a review, a retriable step | stays **in-process** (retry / default arm) — **no task** |
82
+ | **Advisory** | a hint, a note for the next agent | the **blackboard** (`app/blackboard.ts`) — never gates a flow |
83
+ | **Decision-required** | proceed/revise, answer an open question, resolve a conflict, abandon | **user task + form** |
84
+
85
+ Only the third tier becomes a user task. This retires the "fabricate a blank answerable escalation" hack:
86
+ an empty question is a *non-escalation*, not a task.
87
+
88
+ ### 2. `serviceTask(persist) + message-catch(wait)` → one `userTask`
89
+
90
+ Each `persist-*-escalation` service task and its paired intermediate message-catch collapse into a single
91
+ `userTask` bearing a `zeebe:formDefinition` (linked `.form`) and a `zeebe:assignmentDefinition`. The engine
92
+ owns the wait, the correlation, and the work-item state — so `escalationCorrKey`, the
93
+ `feature-escalation-answered` / `plan-escalation-answered` messages, and the `pr.persist-*-escalation`
94
+ workers are deleted. Completing the task carries typed variables straight back into the process.
95
+
96
+ ### 3. Forms are the typed escalation contract
97
+
98
+ Each escalation kind gets a `.form` whose schema *is* its interface — replacing free-text question/answer:
99
+
100
+ - **Task escalation** → `{ resolution: "answer" | "abandon", answer?: string }`.
101
+ - **Plan-review escalation** → `{ directive: "proceed" | "revise", notes?: string }` — deleting the
102
+ hand-rolled `parsePlanEscalationDirective`; the enum + required-field validation live in the form/FEEL.
103
+ - **Trial-merge escalation** → `{ action: "proceed" | "rebase" | "abandon", notes?: string }`.
104
+ - **PR review-loop escalation** → `{ answer: string }` (or a kind-specific action enum).
105
+
106
+ ### 4. One queryable task list replaces three denormalised pointers
107
+
108
+ `open_task_escalation_id`, `open_plan_escalation_id`, and the `open_escalation_*` columns on
109
+ `pull_requests` all collapse into `engine.searchUserTasks(...)` — filterable by assignee, candidate group,
110
+ process instance, element, age. There is **no "surfaced" field to go stale**, so the resurface / dead-form
111
+ bug class is eliminated at the root. The plans page and any inbox read the live task search; the
112
+ `inbox_entries` seed is the natural home for the cross-plan view.
113
+
114
+ ### 5. The assignee may be a human **or** an agent
115
+
116
+ `zeebe:assignmentDefinition` routes a task to a specific human, a **candidate group** (e.g. the operator /
117
+ crew leads), or — per ADR 0046 — an **agent**. An LLM worker can complete the *same* form a human would,
118
+ via the `chat`/agent surface or a job-worker-style completer. This makes "auto-resolve with a
119
+ slower/smarter model, else route to a human" a single lifecycle with one contract — something the bespoke
120
+ subsystem cannot express. Agent-answered completion is still a first-class, audited task completion.
121
+
122
+ ### 6. SLA via a timer boundary
123
+
124
+ A user task carries a due date; a **timer boundary event** provides escalation-of-the-escalation —
125
+ reassign, notify, or auto-proceed on a default — the durable replacement for the review poller's ad-hoc
126
+ nudge. A decision no longer hangs forever with no deadline.
127
+
128
+ ### 7. Audit trail from user-task history
129
+
130
+ `plan_review_escalations` is append-only because the **review epoch** = count of answered plan-review
131
+ escalations. Under this ADR the epoch is derived from **completed plan-review user tasks** (native user-task
132
+ history / completion events), so the dedicated audit table is retired without losing the audit.
133
+
134
+ ## Consequences
135
+
136
+ - **A whole bug class disappears.** No denormalised "surfaced" pointer ⇒ no stale/dead-form resurfacing, no
137
+ addressed-escalation paradox, no blank-question fabrication. The engine's single-source-of-truth
138
+ user-task lifecycle replaces three hand-maintained mirrors.
139
+ - **Less code.** Delete `pr.persist-*-escalation` workers, the two resume messages + their catch events,
140
+ `escalationCorrKey`, `answerTaskEscalation`/`answerPlanEscalation`/`refreshOpenTaskEscalation`, the
141
+ denormalised columns, and the bespoke answer page — replaced by `userTask` nodes + `.form`s + the
142
+ existing `taskInbox` surface.
143
+ - **Dogfoods the Delphi vision.** nwf becomes the first real consumer of the Urban form editor + user-task
144
+ inbox, exercising forms end to end on a live app.
145
+ - **The third human-in-the-loop lane.** Enrolment (#152) = what work exists; visibility (#142) =
146
+ watch/nudge a *live* agent (ephemeral); **escalation-as-user-task** = decide *durably* when blocked. The
147
+ cockpit can list a worker's open escalation tasks; the two planes reinforce each other.
148
+ - **Migration is a real refactor, not a rename.** The bespoke tables encode edge cases (epoch-from-count,
149
+ re-plan cleanup, trial-merge "proceed" override). The migration must preserve those semantics on the new
150
+ substrate and run behind tests, phased kind-by-kind.
151
+ - **New dependency on engine user-task depth.** Assignment, candidate groups, task listeners, and timer
152
+ boundaries on user tasks must be exercised (some may surface gaps to file against the engine). Form
153
+ rendering richness is bounded by the `taskInbox`/form-editor state of the art.
154
+
155
+ ## Open questions
156
+
157
+ - **Form-rendering fidelity.** The current `taskInbox` page is minimal (lists key/element). How rich a
158
+ `.form` render is needed before the answer page can be deleted — and is that the form editor's job or a
159
+ `taskInbox` upgrade (an nano-ide concern)?
160
+ - **Agent-answer policy (§5).** When may an agent auto-complete vs must-route-to-human — a per-kind policy,
161
+ a confidence gate, or an operator toggle? How is an agent completion attributed and reversible?
162
+ - **Assignment model.** Candidate group vs named assignee for each kind; where the operator's routing
163
+ preference is persisted (manifest vs app state).
164
+ - **Cross-plan inbox surface.** Does nwf embed `taskInbox` directly, or render its own plan-aware inbox
165
+ page over `searchUserTasks` (matching the existing plans page), keyed through `inbox_entries`?
166
+ - **Back-compat window.** Do in-flight escalations at migration time drain on the old path, or are they
167
+ re-issued as user tasks? (Prefer drain-old, issue-new, per kind.)
@@ -0,0 +1,123 @@
1
+ # ADR 0003 — Epic base-branch admission: explicit, auto-created, and guarded
2
+
3
+ Status: **Proposed.**
4
+ Date: 2026-08-13.
5
+
6
+ > **Scope note.** A **nano-workforce-local** ADR — it governs how *this app* admits an epic for
7
+ > execution. Platform-wide ADRs live in `Magikcraft/nano-bpm/docs/adr` (referenced by number + repo).
8
+ > Continues nano-workforce's series after ADR 0001 (ADR 0002 is planned but not yet written; see below).
9
+
10
+ Relates to:
11
+ nano-workforce **ADR 0001** (cross-repo epics + integration branches — this ADR hardens *how* an epic's
12
+ integration branch is chosen, created, and protected),
13
+ nano-workforce **ADR 0002** *(planned — not yet written in this repo; escalations as user tasks)*: the
14
+ confirm / shared-base gates are human decisions that *could* later surface as user tasks; see Open questions,
15
+ nano-bpm **ADR 0058** (the OpenAPI endpoint surface — `startPlanFanout` is a spec operation whose request
16
+ schema this ADR changes),
17
+ migration `019_plan_base_branch.sql` (the `plans.base_branch` column, whose example is `epic/agent-protocol`),
18
+ and in this repo: `operations/startPlanFanout.ts` (the launch operation), `app/plan.ts`
19
+ (`startPlan`, `normalizeBaseBranch`, `renderBaseBranchBrief`, `PLAN_TERMINAL_STATUSES`), `app/baseGuard.ts`
20
+ + `app/github.ts` (`baseBranchLanded`, `fetchDefaultBranch`), and `resources/processes/plan-fanout.bpmn`.
21
+
22
+ ## Context
23
+
24
+ An epic is launched through the `startPlanFanout` operation, which calls `startPlan(data, engine, parsed,
25
+ baseBranch)`. The `base_branch` (migration 019) tells every fanned-out task agent to branch off it and open
26
+ its PR against it (`renderBaseBranchBrief`), landing the whole epic on a long-lived integration branch that
27
+ reaches the default branch — and any merge-to-default side effect such as auto-publishing — only when the
28
+ integration branch is deliberately merged.
29
+
30
+ Four gaps make this a footgun surface:
31
+
32
+ 1. **Implicit default.** `normalizeBaseBranch` maps a blank/absent value to `null`, silently meaning "target
33
+ the repository default branch." An operator who *meant* to name an integration branch but omitted it
34
+ lands every task straight onto the default branch (e.g. `main`) with **no integration buffer** — and
35
+ any merge-to-default side effect fires per task.
36
+ 2. **No branch creation.** Nothing creates the integration branch. `baseGuard`/`baseBranchLanded` only
37
+ *read* (`gh pr list`, `fetchPrBase`, `fetchDefaultBranch`). So if the named branch doesn't exist, the
38
+ **first task's** `git fetch origin <branch>` / `gh pr create --base <branch>` fails — a late, per-task
39
+ failure instead of a clean admission error.
40
+ 3. **No typo guard.** A mistyped branch name is indistinguishable from an intended new one; without
41
+ creation it fails late, and *with* naive creation it would silently spawn a wrong-rooted branch.
42
+ 4. **No collision guard.** Two in-flight epics can target the **same** integration branch, interleaving
43
+ commits and poisoning each other's base — with no warning.
44
+
45
+ The pieces to fix this already exist: `fetchDefaultBranch` (default-branch identity), `PLAN_TERMINAL_STATUSES`
46
+ (`done|failed|abandoned` → the complement is "active"), a strict `isPlausibleBranchName` allowlist, and the
47
+ `019` example convention `epic/*`.
48
+
49
+ ## Decision
50
+
51
+ **Every epic launch must state its base branch explicitly, and `startPlanFanout` admits it through one
52
+ fail-fast gate** — `admitPlan(...)` — run before any task fans out, backed by a durable `ensure-base-branch`
53
+ head step in `plan-fanout.bpmn`. The gate has four ordered rules:
54
+
55
+ ### 1. Required + explicit — no implicit default
56
+
57
+ `baseBranch` becomes a **required** field of `StartPlanFanoutRequest`. `normalizeBaseBranch` **rejects** a
58
+ blank/absent value (`MissingBaseBranchError` → HTTP 400) instead of returning `null`. "Land on the default
59
+ branch" is now a **conscious, named, confirmed** choice (rule 3), never a silent fallback. The
60
+ `base_branch == null ? default : brief` fork in the launch/prompt path is removed; `renderBaseBranchBrief`
61
+ is always rendered. (`plans.base_branch` stays nullable in the DB **only** to grandfather pre-migration
62
+ rows; new launches always set it.)
63
+
64
+ ### 2. Create-if-missing, idempotently — with an `epic/*` guard
65
+
66
+ `ensureBaseBranch(repo, branch, token)`:
67
+ - **Exists** → no-op (never reset — a reset would nuke in-flight task PRs stacked on it; a stacked epic's
68
+ base already exists and is left alone).
69
+ - **Missing and matches `epic/*`** → create `refs/heads/<branch>` off the **default branch HEAD**.
70
+ - **Missing and *not* `epic/*`** → **reject** (`BaseBranchMustExistError`): a non-`epic/*` branch must
71
+ already exist, so a typo can't silently spawn a wrong-rooted branch. (`epic/*` is the `019` convention.)
72
+
73
+ Runs at admission (fail fast) **and** as a head `ensure-base-branch` service task in `plan-fanout.bpmn` so
74
+ it is durable + retriable even on a re-plan.
75
+
76
+ ### 3. Confirm-default — naming the default branch is deliberate
77
+
78
+ If the explicit target **equals the repository default** (via `fetchDefaultBranch`), admission requires an
79
+ explicit `confirmDefaultBase: true`, else **reject** (`DefaultBaseNotConfirmedError` → 400) with a message
80
+ spelling out the consequence ("every task lands directly on `<default>` with no integration branch; any
81
+ merge-to-default side effect fires per task"). This is the single guardrail on the one dangerous explicit
82
+ value.
83
+
84
+ ### 4. Shared-base guard — one integration branch, one epic
85
+
86
+ If another plan whose `status ∉ PLAN_TERMINAL_STATUSES` (i.e. **active**) targets the **same repo + same
87
+ base branch**, admission **rejects** (`SharedBaseError` → 409) unless `allowSharedBase: true`. **Exempt: the
88
+ default branch** — many epics target the default concurrently and don't collide (each task PR is
89
+ independent). The guard fires only for a **shared custom integration branch**, the genuinely dangerous case.
90
+
91
+ ### Recommended defaults (confirm before build)
92
+
93
+ - **Hard-require flags**, not soft warnings, for rules 3 and 4: a warning in a headless submit flow is
94
+ ignorable; a required `confirmDefaultBase` / `allowSharedBase` is a "warn you can't skip."
95
+ - **`epic/*` auto-create guard** (rule 2): auto-create only `epic/*`; any other non-existent name is a
96
+ 400. Matches the `019` convention.
97
+
98
+ ## Consequences
99
+
100
+ - **A footgun class disappears:** no silent land-on-main, no first-task-fails-on-missing-branch, no typo'd
101
+ wrong-rooted branch, no two-epics-one-branch interleave — all become clean admission errors.
102
+ - **Breaking API + launch-path change.** `StartPlanFanoutRequest.baseBranch` is now required; any
103
+ fire-and-forget caller/CLI/epic template that omitted it will (intentionally) 400. The OpenAPI spec +
104
+ generated types (`nano-generated/api-io.d.ts`, controller) regenerate; the submit surface + docs update.
105
+ - **Back-compat:** pre-migration / in-flight `base_branch = null` plans are **grandfathered** (the column
106
+ stays nullable; the required-ness is enforced at *admission* of new launches, not by a DB `NOT NULL`).
107
+ - **New write permission exercised:** ref creation (already held by the token that pushes task branches).
108
+ - **Ties into ADR 0001/0002:** this is the admission half of the integration-branch story (0001), and its
109
+ human gates could later be modeled as user tasks (planned ADR 0002) rather than flags — see Open questions.
110
+
111
+ ## Open questions
112
+
113
+ - **Gates as user tasks (planned ADR 0002)?** Should `confirmDefaultBase` / shared-base become an inbox **user
114
+ task** ("Epic X wants to target `main` / share `epic/y` — approve?") instead of a submit-time flag? For
115
+ *pre-fan-out* admission a synchronous flag is simpler and fail-fast; a user task fits only if we want a
116
+ human in the launch loop. Deferred.
117
+ - **Auto-create root.** Always off default HEAD — correct for a fresh integration branch. Should a stacked
118
+ epic be able to declare `stackOn: <branch>` so its base is auto-created off *another* epic's branch
119
+ rather than default? (Today: that base must pre-exist.)
120
+ - **Convention scope.** Is `epic/*` the only auto-createable prefix, or should the app config own the
121
+ allowlist?
122
+ - **Grandfathered nulls.** Leave historical `null` rows as-is, or backfill them to the (then-current)
123
+ default branch for a uniform read model?
package/main.ts CHANGED
@@ -16,8 +16,11 @@
16
16
  // The reviewer agent (job type `senior:pr-review`) is deliberately NOT hosted here — it is an
17
17
  // EXTERNAL worker. Point a coding-agent harness at that job type (the same one that services
18
18
  // the code-first twin) so the automated review stays decoupled from the orchestration.
19
+ import { Server } from "node:http";
19
20
  import { createNanoSdkEngineClient, runFromEnv, selectHost } from "@nanobpm/urban";
21
+ import { type AgenticChannelHandle, mountAgenticChannel } from "./app/agentic/channel.ts";
20
22
  import { MAX_ROUNDS, pollOnce } from "./app/service.ts";
23
+ import { envVar } from "./app/version.ts";
21
24
 
22
25
  const PORT = Number(process.env.PR_REVIEW_PORT ?? 3000);
23
26
  const POLL_MS = Number(process.env.NANO_PR_POLL_MS ?? 60_000);
@@ -40,6 +43,30 @@ const engine = await createNanoSdkEngineClient({
40
43
  // signal handler would only stop the HTTP server, leaving the poller keeping us alive).
41
44
  const app = await runFromEnv({ engine, host, port: PORT, handleSignals: false });
42
45
 
46
+ // Agentic visibility channel (ADR 0056, epic #142). Ride the app's OWN HTTP server so the channel
47
+ // shares the app port (no sidecar). This is the ONLY main.ts wiring for the whole epic — sibling
48
+ // slices (H1/H3/H4) extend it by dropping a family module under `app/agentic/families/`, never here.
49
+ // Mount only when a shared identity secret is configured, so the app never exposes an
50
+ // unauthenticated upgrade; `app.httpServer` is a `node:http` Server once started (undefined on hosts
51
+ // that don't surface one, e.g. Deno).
52
+ let agentic: AgenticChannelHandle | undefined;
53
+ const agenticSecret = envVar("NANO_AGENTIC_SECRET") ?? envVar("NANO_PR_WEBHOOK_SECRET");
54
+ const httpServer = app.httpServer;
55
+ if (httpServer instanceof Server) {
56
+ if (agenticSecret) {
57
+ agentic = await mountAgenticChannel({
58
+ server: httpServer,
59
+ secret: agenticSecret,
60
+ data: app.data,
61
+ log: app.log,
62
+ });
63
+ } else {
64
+ app.log.warn("agentic channel not mounted: set NANO_AGENTIC_SECRET (or NANO_PR_WEBHOOK_SECRET)");
65
+ }
66
+ } else if (agenticSecret) {
67
+ app.log.warn("agentic channel not mounted: app.httpServer is not a node:http Server on this host");
68
+ }
69
+
43
70
  // Review-ready poller. Self-scheduling (not setInterval) so a slow GitHub call can never
44
71
  // overlap two passes (which could double-signal `review-ready`); the next pass is scheduled
45
72
  // only after the previous one settles.
@@ -59,6 +86,13 @@ async function drainAndExit(): Promise<void> {
59
86
  if (shuttingDown) return;
60
87
  shuttingDown = true;
61
88
  if (pollTimer) clearTimeout(pollTimer);
89
+ // Tear the agentic families + hub down (releases the WS clients) before the app stops its HTTP
90
+ // server, which the channel shares.
91
+ if (agentic) {
92
+ try {
93
+ await agentic.teardown();
94
+ } catch { /* best-effort channel shutdown */ }
95
+ }
62
96
  try {
63
97
  await app.stop();
64
98
  } catch { /* already stopped */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.50.0",
3
+ "version": "0.52.0",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",
@@ -46,7 +46,8 @@
46
46
  "lint:fix": "biome check --write app operations workers pages components scripts e2e main.ts"
47
47
  },
48
48
  "dependencies": {
49
- "@nanobpm/urban": "^0.45.0"
49
+ "@nanobpm/agentic": "^0.1.0",
50
+ "@nanobpm/urban": "^0.46.0"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@biomejs/biome": "^2.4.11",
@@ -40,6 +40,19 @@ decomposition **from the issues**, then test the plan against it.
40
40
  `dependsOn` it. Reject a `dependsOn` edge added purely to **serialise the landing** of otherwise
41
41
  parallel work — that is not a fix, it just needlessly serialises implementation; name the pair,
42
42
  the shared surface, and which of (a)/(b) the planner should apply.
43
+ - **Package fragmentation (Conway artifact).** The plan gives a cohesive body of work its own
44
+ published unit **per task** — N tasks ⇒ N npm packages / crates / services — where one library
45
+ with the slices as **subpath exports / subdirectories** would serve the same consumers. This is
46
+ the task decomposition leaking into the artifact's module boundaries: separate packages are the
47
+ frictionless maximum of independence, so they get chosen by default, then have to be
48
+ unfragmented by hand (and each extra published unit is a publish/credentials bootstrap +
49
+ changelog + version cadence forever). Try to disprove that each **new** published-package
50
+ boundary is **consumer-driven**: is there a distinct external consumer of *it* alone, an
51
+ intentional independent release cadence, or a different runtime tier? If not for a given
52
+ package, flag it and demand the remedy: **coarsen the siblings into one package** exposing
53
+ subpaths, landing a **wave-0 scaffold task** (manifest with the full exports map
54
+ pre-declared + one empty subdirectory per slice) first if the shared manifest would otherwise be
55
+ a merge collision. Name the packages that lack a consumer-facing justification.
43
56
  - **Non-self-contained prompt.** A task's `prompt` can't be executed without reasoning the planner
44
57
  kept to itself.
45
58
  - **Sequencing intent violated.** If the issues state an ordering (e.g. "audit the foundation
package/prompts/plan.md CHANGED
@@ -157,6 +157,41 @@ Choose (1) when the surface *is* the task; choose (2) when the surface is shared
157
157
  infrastructure several distinct tasks sit on top of. Reserve plain parallel tasks
158
158
  (no shared surface) for genuinely disjoint work.
159
159
 
160
+ ### Packaging cohesion → one library, subpaths, not a package per task
161
+
162
+ The shared-surface rule above pushes toward independence, and independence has a
163
+ seductive failure mode: giving each task its **own published unit** (npm package,
164
+ crate, service) is the *frictionless maximum* of independence — a separate
165
+ manifest, separate exports, separate directory mean zero shared surface and zero
166
+ merge collision. So a plan that slices a single cohesive library into N tasks will,
167
+ left alone, tend to emit **N packages** — one per task. That is not a design; it is
168
+ your task decomposition leaking into the artifact's module boundaries (Conway's
169
+ Law). It has to be unfragmented by hand later, and each extra published unit is a
170
+ one-time publish/credentials bootstrap plus a changelog and version cadence forever.
171
+
172
+ So, before you slice: **a new published unit requires a consumer-facing
173
+ justification, not merely "this is an independent task."** A new package/crate/
174
+ service is warranted only when at least one is true:
175
+
176
+ - a **distinct external consumer** imports it on its own (something outside the
177
+ family depends on *it*, not on its siblings);
178
+ - it needs an **independent release cadence** (versioned and shipped separately on
179
+ purpose); or
180
+ - it is a **different runtime tier** (e.g. a browser bundle vs. a server library vs.
181
+ a worker client) that consumers install separately.
182
+
183
+ Absent one of those, the default is **one library, with the slices as subpath
184
+ exports / subdirectories inside it** (the shape of a package that exposes several
185
+ surfaces — e.g. `./runtime`, `./toolkit`, `./worker` — from a single manifest). The
186
+ slices stay independent to *write*: use the
187
+ **wave-0 scaffold task** (option 2 above) to land the library skeleton first — its
188
+ manifest with the **full exports map pre-declared** and an empty subdirectory per
189
+ slice — so every sibling only **adds files inside its own subdirectory** and never
190
+ touches the shared manifest or barrel. That buys parallel-merge independence **and**
191
+ a cohesive published artifact at the same time. Reserve genuinely separate packages
192
+ for the consumer-facing cases above, and say in the task prompt which consumer
193
+ justifies the split.
194
+
160
195
  ## Output contract
161
196
 
162
197
  Write a JSON object of **result variables** to the file named by the