@nanobpm/nano-workforce 0.123.2 → 0.125.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 +20 -0
- package/README.md +9 -5
- package/app/deliveryGraphDispatch.test.ts +143 -0
- package/app/deliveryGraphDispatch.ts +168 -0
- package/app/deliveryGraphProposals.test.ts +267 -0
- package/app/deliveryGraphProposals.ts +269 -0
- package/app/deliveryGraphRun.test.ts +6 -52
- package/app/deliveryGraphRun.ts +21 -76
- package/app/deliveryGraphText.ts +3 -3
- package/app/deliveryRunner.ts +4 -3
- package/app/featureReadModel.test.ts +97 -54
- package/app/featureReadModel.ts +152 -0
- package/app/service.ts +15 -0
- package/app/stage.ts +71 -77
- package/db/migrations/075_delivery_graph_proposals.sql +48 -0
- package/db/migrations/076_feature_read_model_declare_once.sql +53 -0
- package/docs/adr/0005-agent-authored-delivery-graphs.md +18 -0
- package/docs/adr/0006-delivery-units-one-representation.md +221 -0
- package/docs/agent-guide.md +50 -58
- package/e2e/delivery-graph-dispatch.e2e.ts +155 -0
- package/openapi.yaml +118 -161
- package/operations/compileDeliveryGraph.test.ts +100 -37
- package/operations/compileDeliveryGraph.ts +64 -18
- package/operations/dispatchDeliveryGraph.test.ts +171 -152
- package/operations/dispatchDeliveryGraph.ts +79 -99
- package/operations/getAgentInstructions.test.ts +10 -6
- package/operations/previewDeliveryGraph.test.ts +90 -51
- package/operations/previewDeliveryGraph.ts +45 -18
- package/package.json +2 -2
- package/pages/cockpit/mount.js +19 -12
- package/pages/delivery-graphs/mount.js +37 -137
- package/pages/delivery-graphs.page.json +50 -3
- package/scripts/check-migrations.test.ts +31 -0
- package/scripts/check-migrations.ts +40 -6
- package/test/cockpit-embed-endpoints.test.ts +59 -36
- package/test/delivery-graphs-embed.test.ts +36 -34
- package/e2e/delivery-graph-start.e2e.ts +0 -145
- package/operations/startDeliveryGraph.integration.test.ts +0 -316
- package/operations/startDeliveryGraph.ts +0 -222
package/app/stage.ts
CHANGED
|
@@ -1,41 +1,36 @@
|
|
|
1
|
-
// Canonical feature-run pipeline stage model (issue #254 §1) — the
|
|
2
|
-
// derived pipeline surface the Feature view
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// exactly as `delivery_label` is projected — so the declarative dataGrid page consumes ready, stored
|
|
7
|
-
// columns (bound by `{"field":…}`) and never has to call TS or express OR/null in its flat filter DSL.
|
|
1
|
+
// Canonical feature-run pipeline stage model (issue #254 §1) — the ergonomic TS façade over the ONE
|
|
2
|
+
// derived-read-model declaration in app/featureReadModel.ts. The pipeline surface the Feature view
|
|
3
|
+
// renders (`stage`/`stage_state`/`stage_skipped`/`attention`/`list_bucket`) is DERIVED, never ground
|
|
4
|
+
// truth: each column is a pure function of a feature run's own `status`/`pr_key`/`converge`/
|
|
5
|
+
// `auto_merge`/`acknowledged_at` plus, for `attention`, the engine's OPEN-user-task set.
|
|
8
6
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
7
|
+
// SINGLE SOURCE OF TRUTH (ADR-0065, nano-ide#452). These derivations are declared ONCE, in Urban's
|
|
8
|
+
// closed expression DSL (`featureReadModel`, app/featureReadModel.ts), and compiled to BOTH the SQLite
|
|
9
|
+
// VIEW (migration 076) and the runtime TS functions used here. `deriveStage`/`deriveListBucket` are
|
|
10
|
+
// now thin ADAPTERS that evaluate that declaration's compiled functions (`fnFor`) — they do NOT
|
|
11
|
+
// re-express the CASE/EXISTS logic in TypeScript, so the SQL and TS lowerings cannot drift (the former
|
|
12
|
+
// hand-written parity test becomes the framework-owned `assertReadModelParity` regression guard). The
|
|
13
|
+
// mapping remains TOTAL and DETERMINISTIC over all 11 FEATURE_RUN_STATUSES. Do NOT duplicate it
|
|
14
|
+
// anywhere (not in SQL, not in the page, not in each poller/worker): every reader flows through the
|
|
15
|
+
// VIEW or these adapters, both sourced from the one declaration.
|
|
16
|
+
|
|
17
|
+
import { type FeatureReadModelDerivedColumn, featureReadModel, STAGE_DONE_STATUSES, USER_TASKS_PROJECTION } from "./featureReadModel.ts";
|
|
18
|
+
|
|
19
|
+
// Re-exported for back-compat with existing importers (operations/acknowledgeDone.ts). Its canonical
|
|
20
|
+
// home is now app/featureReadModel.ts, where it feeds the terminal tier of the derived columns.
|
|
21
|
+
export { STAGE_DONE_STATUSES };
|
|
14
22
|
|
|
15
23
|
/** The canonical pipeline stage keys, in path order. `Merging` is a path/visual stage the renderer
|
|
16
24
|
* fills as upcoming — no status maps to it as the ACTIVE stage (intentional). */
|
|
17
25
|
export const STAGE_KEYS = ["Requested", "Implementing", "PR open", "Converging", "Merging", "Done"] as const;
|
|
18
26
|
export type StageKey = (typeof STAGE_KEYS)[number];
|
|
19
27
|
|
|
20
|
-
/** The active stage's render state, in the urban
|
|
28
|
+
/** The active stage's render state, in the urban `kind:"pipeline"` column's EXACT vocabulary:
|
|
21
29
|
* `ok` (Done ✓ success), `failed` (Done ✕ failure), `blocked` (blocked glyph), or `null` (in-progress
|
|
22
30
|
* → the renderer treats it as `active`). Any OTHER string silently degrades to `active` in the
|
|
23
31
|
* renderer, so a failed run MUST emit `'failed'` (not `'fail'`) to render as a failure. */
|
|
24
32
|
export type StageState = "ok" | "failed" | "blocked" | null;
|
|
25
33
|
|
|
26
|
-
/** The 6 TRULY-terminal statuses that map to the `Done` stage. Distinct from
|
|
27
|
-
* `FEATURE_TERMINAL_STATUSES` (app/feature.ts), which is the redispatch-settled set and also counts
|
|
28
|
-
* `opened`/`converging` as terminal — those are LIVE pipeline stages (`PR open`/`Converging`), NOT
|
|
29
|
-
* Done, so this list must stay separate. Also the basis of the `list_bucket` history partition. */
|
|
30
|
-
export const STAGE_DONE_STATUSES: readonly string[] = [
|
|
31
|
-
"merged",
|
|
32
|
-
"converged",
|
|
33
|
-
"blocked",
|
|
34
|
-
"failed",
|
|
35
|
-
"skipped",
|
|
36
|
-
"abandoned",
|
|
37
|
-
];
|
|
38
|
-
|
|
39
34
|
/** The subset of a FeatureRun `deriveStage` reads. FeatureRun (app/feature.ts) structurally satisfies
|
|
40
35
|
* this; keeping the input structural avoids a stage.ts ↔ feature.ts import cycle. */
|
|
41
36
|
export interface StageInput {
|
|
@@ -48,7 +43,8 @@ export interface StageInput {
|
|
|
48
43
|
* authoritative "who is waiting on a human" set). `attention` derives from THESE, never from the
|
|
49
44
|
* drift-prone `status` variable — so once an escalation is answered (its `user_tasks` row deleted)
|
|
50
45
|
* the badge clears immediately even while `status` still reads a stale `"escalated"`. Omitted/false
|
|
51
|
-
* ⇒ no open task ⇒ no badge.
|
|
46
|
+
* ⇒ no open task ⇒ no badge. These booleans are lowered into synthetic `user_tasks` projection rows
|
|
47
|
+
* so this façade evaluates the SAME `exists(...)` derivation the VIEW compiles. */
|
|
52
48
|
hasOpenBlockedTask?: boolean | null;
|
|
53
49
|
hasOpenEscalationTask?: boolean | null;
|
|
54
50
|
}
|
|
@@ -62,60 +58,58 @@ export interface DerivedStage {
|
|
|
62
58
|
attention: string | null;
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
const { status } = run;
|
|
61
|
+
/** The synthetic correlation key threaded through the `attention` derivation: the base row's
|
|
62
|
+
* `feature_key` and each synthesised open-task row's `subject_key` share this value so the compiled
|
|
63
|
+
* `exists(... WHERE subject_key = feature_key ...)` predicate matches. Its concrete value is
|
|
64
|
+
* immaterial (it never leaves this function); it only has to be consistent between the two. */
|
|
65
|
+
const SELF_KEY = "self";
|
|
71
66
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
: status === "blocked"
|
|
82
|
-
? "blocked"
|
|
83
|
-
: "failed"; // failed / skipped / abandoned
|
|
84
|
-
} else {
|
|
85
|
-
// LIVE/PARKED tier — one shared rule for every non-terminal status. `escalated`/`awaiting_operator`
|
|
86
|
-
// are parked but their stored fields still describe WHERE in the pipeline they stalled, so they run
|
|
87
|
-
// through the same rule as a live run; their attention comes from the badge (below), not the stage.
|
|
88
|
-
if (status === "converging") stage = "Converging";
|
|
89
|
-
else if ((run.pr_key ?? "") !== "" || status === "opened") stage = "PR open";
|
|
90
|
-
else if (status === "running" || status === "escalated" || status === "awaiting_operator") stage = "Implementing";
|
|
91
|
-
else stage = "Requested";
|
|
92
|
-
state = null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// `skipped`: stages not in this row's path, purely from converge/auto_merge.
|
|
96
|
-
const converge = truthy(run.converge);
|
|
97
|
-
const autoMerge = truthy(run.auto_merge);
|
|
98
|
-
const skippedKeys: StageKey[] = !converge ? ["Converging", "Merging"] : !autoMerge ? ["Merging"] : [];
|
|
67
|
+
/** The `user_tasks` projection rows a `StageInput`'s open-task booleans stand for — one row per OPEN
|
|
68
|
+
* operator user task, keyed exactly as `pollUserTasks` records them, so the compiled `attention`
|
|
69
|
+
* derivation sees the same engine-truth shape at runtime and here. */
|
|
70
|
+
function openTaskRows(run: StageInput): Array<Record<string, unknown>> {
|
|
71
|
+
const rows: Array<Record<string, unknown>> = [];
|
|
72
|
+
if (run.hasOpenBlockedTask === true) rows.push({ subject_type: "feature", subject_key: SELF_KEY, element_id: "feature-blocked" });
|
|
73
|
+
if (run.hasOpenEscalationTask === true) rows.push({ subject_type: "feature", subject_key: SELF_KEY, element_id: "feature-escalation" });
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
99
76
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// IFF the task is open and deletes it the moment it is answered — so a run shows the blocked glyph
|
|
109
|
-
// IFF an open `feature-blocked` task exists, and ⚠ IFF an open `feature-escalation` task exists.
|
|
110
|
-
// Once answered, the row is gone and the badge clears regardless of the stale `status`.
|
|
111
|
-
const attention = truthy(run.hasOpenBlockedTask) ? "blocked" : truthy(run.hasOpenEscalationTask) ? "⚠" : null;
|
|
77
|
+
/** Evaluate one derived column from the ONE `featureReadModel` declaration for a base row (plus any
|
|
78
|
+
* projection rows the derivation correlates against). This is the single seam where the framework's
|
|
79
|
+
* compiled-function boundary is crossed: `fnFor(column)` returns `unknown` (a derivation can compile to
|
|
80
|
+
* any DSL value), while the caller statically knows the declared `lit(...)` shape of each column. */
|
|
81
|
+
function evalDerived<T>(column: FeatureReadModelDerivedColumn, baseRow: Record<string, unknown>, projections?: Record<string, Array<Record<string, unknown>>>): T {
|
|
82
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary — `fnFor` returns `unknown`; each derived column yields one of its declared DSL `lit(...)` values, whose type the caller knows.
|
|
83
|
+
return featureReadModel.fnFor(column)(baseRow, projections) as T;
|
|
84
|
+
}
|
|
112
85
|
|
|
113
|
-
|
|
86
|
+
/** Derive the canonical pipeline stage, its render state, its not-in-path set, and its attention badge
|
|
87
|
+
* for one feature run. Pure and read-only — TOTAL over all 11 statuses (never returns undefined). The
|
|
88
|
+
* four columns are evaluated from the ONE `featureReadModel` declaration (app/featureReadModel.ts), so
|
|
89
|
+
* this façade and the SQLite VIEW compute byte-identical values by construction. */
|
|
90
|
+
export function deriveStage(run: StageInput): DerivedStage {
|
|
91
|
+
const baseRow = {
|
|
92
|
+
feature_key: SELF_KEY,
|
|
93
|
+
status: run.status,
|
|
94
|
+
pr_key: run.pr_key ?? null,
|
|
95
|
+
converge: run.converge ?? null,
|
|
96
|
+
auto_merge: run.auto_merge ?? null,
|
|
97
|
+
};
|
|
98
|
+
const projections = { [USER_TASKS_PROJECTION]: openTaskRows(run) };
|
|
99
|
+
return {
|
|
100
|
+
stage: evalDerived<StageKey>("stage", baseRow, projections),
|
|
101
|
+
state: evalDerived<StageState>("stage_state", baseRow, projections),
|
|
102
|
+
skipped: evalDerived<string>("stage_skipped", baseRow, projections),
|
|
103
|
+
attention: evalDerived<string | null>("attention", baseRow, projections),
|
|
104
|
+
};
|
|
114
105
|
}
|
|
115
106
|
|
|
116
|
-
/** The Active/History partition label (§5)
|
|
117
|
-
*
|
|
118
|
-
*
|
|
107
|
+
/** The Active/History partition label (§5): `history` iff the row is in a truly-terminal status AND
|
|
108
|
+
* acknowledged; otherwise `active` (live runs + terminal-but-UNACKNOWLEDGED runs). DERIVED on read from
|
|
109
|
+
* the ONE `featureReadModel` `list_bucket` declaration — the same AST the VIEW compiles (migration 076),
|
|
110
|
+
* NOT a write-time projection: the page's tabs filter the VIEW's derived `list_bucket`, and the stored
|
|
111
|
+
* `feature_runs.list_bucket` base column is vestigial (retired as a write projection, issue #439). This
|
|
112
|
+
* adapter is the TS lowering of that derivation, used off the write path (redispatch gating, tests). */
|
|
119
113
|
export function deriveListBucket(status: string, acknowledgedAt: string | null | undefined): "active" | "history" {
|
|
120
|
-
return
|
|
114
|
+
return evalDerived<"active" | "history">("list_bucket", { status, acknowledged_at: acknowledgedAt ?? null });
|
|
121
115
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- The `staged` delivery-graph proposal store (ADR 0005 Decision 7, issue #460). This realises
|
|
2
|
+
-- `propose → preview → approve → dispatch` as intended: the agent-facing surface ends at
|
|
3
|
+
-- propose → compile → STAGE, and a HUMAN dispatches the staged proposal from the cockpit. The old
|
|
4
|
+
-- `approvalToken` was a REPLAYABLE content digest handed back to the same caller, so any holder of
|
|
5
|
+
-- the API credential self-approved. Removing the dispatch affordance from the agent surface (there is
|
|
6
|
+
-- no `start` endpoint) dissolves that hole: the compile door persists the compiled graph HERE as a
|
|
7
|
+
-- `staged` proposal and returns only a preview + a navigational `reviewUrl` — nothing that can trigger
|
|
8
|
+
-- a run. The cockpit lists these rows, renders the preview, and dispatches the one the operator picks.
|
|
9
|
+
--
|
|
10
|
+
-- • digest (PK) — the content address of the compiled graph (`sha256(compiled.bpmn)[:12]`), the
|
|
11
|
+
-- SAME digest the runner uses for the content-addressed deploy id. It NAMES the proposal so the
|
|
12
|
+
-- agent can hand the operator an unambiguous "dispatch <digest>" and the operator dispatches
|
|
13
|
+
-- EXACTLY the digest they previewed. A re-compile of the same bytes is idempotent (same PK).
|
|
14
|
+
-- • logical_key — the LOGICAL graph identity (the graph's `name`, else the digest) used to
|
|
15
|
+
-- SUPERSEDE: staging a changed graph (new digest) for the same logical key retires the prior
|
|
16
|
+
-- staged proposal, so the cockpit shows one live proposal per logical graph, not every recompile.
|
|
17
|
+
-- • graph — the original `DeliveryGraph` JSON, retained so the cockpit dispatch action can run the
|
|
18
|
+
-- runner for the previewed digest without the agent re-submitting anything.
|
|
19
|
+
-- • preview — the rendered preview JSON (`{ diagram, sideEffects, humanNodes }`) the cockpit shows,
|
|
20
|
+
-- stamped at stage time so the list renders without recompiling.
|
|
21
|
+
-- • status — `staged` (awaiting operator review), `superseded` (replaced by a newer digest for its
|
|
22
|
+
-- logical key), `dispatched` (the operator launched it), or `expired` (aged out of its TTL before
|
|
23
|
+
-- dispatch). Only `staged` rows show in the cockpit; the poller sweeps aged-out `staged` rows to
|
|
24
|
+
-- `expired` (the grid's datasource filter is equality-only, so expiry is realised by that status
|
|
25
|
+
-- flip, not an `expires_at > now` clause).
|
|
26
|
+
-- • expires_at — the TTL horizon. Staged proposals age out of the cockpit list so a stale entry an
|
|
27
|
+
-- operator never dispatched does not linger; the poller flips an aged-out `staged` row to `expired`.
|
|
28
|
+
CREATE TABLE IF NOT EXISTS delivery_graph_proposals (
|
|
29
|
+
digest TEXT PRIMARY KEY,
|
|
30
|
+
logical_key TEXT NOT NULL,
|
|
31
|
+
title TEXT,
|
|
32
|
+
graph TEXT NOT NULL,
|
|
33
|
+
preview TEXT NOT NULL,
|
|
34
|
+
node_count INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
human_node_count INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
side_effect_count INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
side_effecting INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
status TEXT NOT NULL DEFAULT 'staged',
|
|
39
|
+
created_at TEXT NOT NULL,
|
|
40
|
+
updated_at TEXT NOT NULL,
|
|
41
|
+
expires_at TEXT NOT NULL
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
-- Supersede scans by logical_key; the cockpit list filters by status + expiry.
|
|
45
|
+
CREATE INDEX IF NOT EXISTS ix_delivery_graph_proposals_logical
|
|
46
|
+
ON delivery_graph_proposals (logical_key);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS ix_delivery_graph_proposals_status
|
|
48
|
+
ON delivery_graph_proposals (status);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- Feature-run read model: DECLARE ONCE, compile to BOTH backends (ADR-0065, nano-ide#452).
|
|
2
|
+
--
|
|
3
|
+
-- This migration SUPERSEDES the hand-wired `feature_read_model` VIEW of 073_feature_read_model.sql and
|
|
4
|
+
-- 075_feature_read_model_attention_from_user_tasks.sql. Those authored each derived column TWICE — the
|
|
5
|
+
-- SQL CASE/EXISTS here AND the TypeScript oracle (deriveStage/deriveListBucket, app/stage.ts) — kept in
|
|
6
|
+
-- lockstep by a hand-written parity test (drift surface #2, ADR-0065). This VIEW is no longer
|
|
7
|
+
-- hand-written: every DERIVED column body below is emitted VERBATIM from the ONE declaration in
|
|
8
|
+
-- app/featureReadModel.ts (`featureReadModel.sqlSelectFor(col, { baseAlias: "fr" })`), which ALSO drives the
|
|
9
|
+
-- runtime TS via `fnFor` — the two lowerings fall out of the same closed-DSL AST and cannot diverge.
|
|
10
|
+
-- A drift guard (app/featureReadModel.test.ts) fails if this file stops matching the declaration, and
|
|
11
|
+
-- `assertReadModelParity` proves the SQL and TS lowerings agree.
|
|
12
|
+
--
|
|
13
|
+
-- SEMANTICS ARE UNCHANGED from 075: `attention` still derives from ENGINE TRUTH — an OPEN native user
|
|
14
|
+
-- task in the `user_tasks` inbox (a `feature-blocked` row => blocked glyph, a `feature-escalation` row
|
|
15
|
+
-- => `⚠`), NOT the drift-prone `status` variable (issue #422); `stage`/`stage_state`/`stage_skipped`/
|
|
16
|
+
-- `list_bucket` are the same pure functions of the row. Only the AUTHORING moves to the framework
|
|
17
|
+
-- primitive; the projected values are identical.
|
|
18
|
+
--
|
|
19
|
+
-- Forward-only VIEW redefinition (DROP then CREATE). 073/075 are MERGED, IMMUTABLE migrations — never
|
|
20
|
+
-- edited; this is a NEW migration superseding their VIEW body. `feature_runs` (028+) and `user_tasks`
|
|
21
|
+
-- (034) already exist earlier in the chain, and 075 already created the supporting
|
|
22
|
+
-- `idx_user_tasks_subject_element` index the correlated attention EXISTS lookups seek, so it is not
|
|
23
|
+
-- re-created here. Base columns are plain identity pass-throughs (aliased so the static pages<->schema
|
|
24
|
+
-- contract guard sees the VIEW columns); `feature_runs fr` stays the sole top-level FROM (the user_tasks
|
|
25
|
+
-- lookups are nested EXISTS subqueries at paren depth >= 1). The runner wraps each file in its own
|
|
26
|
+
-- transaction, so this file must NOT contain BEGIN/COMMIT.
|
|
27
|
+
|
|
28
|
+
DROP VIEW IF EXISTS feature_read_model;
|
|
29
|
+
|
|
30
|
+
CREATE VIEW feature_read_model AS
|
|
31
|
+
SELECT
|
|
32
|
+
fr.feature_key AS feature_key,
|
|
33
|
+
fr.repo AS repo,
|
|
34
|
+
fr.issue_number AS issue_number,
|
|
35
|
+
fr.issue_url AS issue_url,
|
|
36
|
+
fr.title AS title,
|
|
37
|
+
fr.base_branch AS base_branch,
|
|
38
|
+
fr.status AS status,
|
|
39
|
+
fr.process_key AS process_key,
|
|
40
|
+
fr.pr_key AS pr_key,
|
|
41
|
+
fr.converge AS converge,
|
|
42
|
+
fr.auto_merge AS auto_merge,
|
|
43
|
+
fr.outcome AS outcome,
|
|
44
|
+
fr.delivery_label AS delivery_label,
|
|
45
|
+
fr.acknowledged_at AS acknowledged_at,
|
|
46
|
+
fr.created_at AS created_at,
|
|
47
|
+
fr.updated_at AS updated_at,
|
|
48
|
+
CASE WHEN COALESCE((COALESCE(("fr"."status" = 'merged'), 0) OR COALESCE(("fr"."status" = 'converged'), 0) OR COALESCE(("fr"."status" = 'blocked'), 0) OR COALESCE(("fr"."status" = 'failed'), 0) OR COALESCE(("fr"."status" = 'skipped'), 0) OR COALESCE(("fr"."status" = 'abandoned'), 0)), 0) THEN 'Done' WHEN COALESCE(("fr"."status" = 'converging'), 0) THEN 'Converging' WHEN COALESCE((COALESCE(("fr"."pr_key" <> ''), 0) OR COALESCE(("fr"."status" = 'opened'), 0)), 0) THEN 'PR open' WHEN COALESCE((COALESCE(("fr"."status" = 'running'), 0) OR COALESCE(("fr"."status" = 'escalated'), 0) OR COALESCE(("fr"."status" = 'awaiting_operator'), 0)), 0) THEN 'Implementing' ELSE 'Requested' END AS stage,
|
|
49
|
+
CASE WHEN COALESCE((COALESCE(("fr"."status" = 'merged'), 0) OR COALESCE(("fr"."status" = 'converged'), 0)), 0) THEN 'ok' WHEN COALESCE(("fr"."status" = 'blocked'), 0) THEN 'blocked' WHEN COALESCE((COALESCE(("fr"."status" = 'failed'), 0) OR COALESCE(("fr"."status" = 'skipped'), 0) OR COALESCE(("fr"."status" = 'abandoned'), 0)), 0) THEN 'failed' ELSE NULL END AS stage_state,
|
|
50
|
+
CASE WHEN (NOT COALESCE("fr"."converge", 0)) THEN 'Converging Merging' WHEN (NOT COALESCE("fr"."auto_merge", 0)) THEN 'Merging' ELSE '' END AS stage_skipped,
|
|
51
|
+
CASE WHEN EXISTS (SELECT 1 FROM "user_tasks" AS "__urban_proj_0" WHERE COALESCE((COALESCE(("__urban_proj_0"."subject_type" = 'feature'), 0) AND COALESCE(("__urban_proj_0"."subject_key" = "fr"."feature_key"), 0) AND COALESCE(("__urban_proj_0"."element_id" = 'feature-blocked'), 0)), 0)) THEN 'blocked' WHEN EXISTS (SELECT 1 FROM "user_tasks" AS "__urban_proj_0" WHERE COALESCE((COALESCE(("__urban_proj_0"."subject_type" = 'feature'), 0) AND COALESCE(("__urban_proj_0"."subject_key" = "fr"."feature_key"), 0) AND COALESCE(("__urban_proj_0"."element_id" = 'feature-escalation'), 0)), 0)) THEN '⚠' ELSE NULL END AS attention,
|
|
52
|
+
CASE WHEN COALESCE((COALESCE((COALESCE(("fr"."status" = 'merged'), 0) OR COALESCE(("fr"."status" = 'converged'), 0) OR COALESCE(("fr"."status" = 'blocked'), 0) OR COALESCE(("fr"."status" = 'failed'), 0) OR COALESCE(("fr"."status" = 'skipped'), 0) OR COALESCE(("fr"."status" = 'abandoned'), 0)), 0) AND COALESCE(("fr"."acknowledged_at" = "fr"."acknowledged_at"), 0)), 0) THEN 'history' ELSE 'active' END AS list_bucket
|
|
53
|
+
FROM feature_runs fr;
|
|
@@ -160,6 +160,11 @@ shared JSON contract means either can be swapped in later without touching the a
|
|
|
160
160
|
|
|
161
161
|
### 7. Submission is propose → preview → approve → dispatch, idempotent, over the self-describing endpoint
|
|
162
162
|
|
|
163
|
+
> **Superseded — see the *Amendment (issue #460)* at the end of this section.** The `POST
|
|
164
|
+
> /actions/start/delivery-graph` agent endpoint described in the following paragraph was **never
|
|
165
|
+
> shipped and has been removed**; the agent surface ends at propose → compile → stage and dispatch is
|
|
166
|
+
> operator-only. The paragraph below is retained as the original (Proposed) decision record.
|
|
167
|
+
|
|
163
168
|
Graphs are submitted exactly as epics are today — via a **new (proposed)** `POST
|
|
164
169
|
/actions/start/delivery-graph` endpoint (paths are relative to the agent guide's `__BASE__` prefix,
|
|
165
170
|
matching the guide's style) with the JSON body, discovered via the agent guide (which already
|
|
@@ -174,6 +179,19 @@ at-least-once execution (mirroring the release workflow's `npx semantic-release`
|
|
|
174
179
|
"skip already-published" discipline — `.github/workflows/release.yml`) so a
|
|
175
180
|
resume cannot double-fire.
|
|
176
181
|
|
|
182
|
+
> **Amendment (issue #460): dispatch is operator-only.** As implemented, the "approve → dispatch"
|
|
183
|
+
> half of this decision is **not** an agent endpoint. The agent surface ends at **propose → compile →
|
|
184
|
+
> stage**: the `POST /actions/compile-delivery-graph` door validates + previews the graph and, on
|
|
185
|
+
> success, **stages** it as a proposal (a durable `delivery_graph_proposals` row, content-addressed by
|
|
186
|
+
> `digest`, superseded per logical graph + TTL-bounded), returning only a preview + a navigational
|
|
187
|
+
> `reviewUrl` — no run key, token, or PIK. A **human dispatches** the staged proposal from the cockpit's
|
|
188
|
+
> Delivery Graphs page (`POST /actions/delivery-graph/dispatch` by `digest`, an operator route). The
|
|
189
|
+
> originally-proposed agent `POST /actions/start/delivery-graph` door — where the same caller was handed
|
|
190
|
+
> a content-addressed `approvalToken` to re-submit with — was **removed**: that "approval" was a
|
|
191
|
+
> **replayable** digest returned to the approver, so any holder of the API credential self-approved.
|
|
192
|
+
> Dispatch-by-absence (there is no agent start door) closes that hole categorically; the idempotent
|
|
193
|
+
> at-most-once launch fence is retained on the operator dispatch path.
|
|
194
|
+
|
|
177
195
|
## Consequences
|
|
178
196
|
|
|
179
197
|
- nwf gains a **generic delivery-graph runner** that composes its existing primitives; the motivating
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# ADR 0006 — Delivery units: one representation for feature / epic / delivery-graph
|
|
2
|
+
|
|
3
|
+
Status: **Proposed.**
|
|
4
|
+
Date: 2026-08-22.
|
|
5
|
+
|
|
6
|
+
> **Scope note.** This is a **nano-workforce-local** ADR — it governs how *this app* represents a
|
|
7
|
+
> unit of delivery work internally. Platform-wide ADRs live in `Magikcraft/nano-bpm/docs/adr`
|
|
8
|
+
> (referenced by number + repo). nano-workforce's own series continues here after ADR 0005.
|
|
9
|
+
|
|
10
|
+
Relates to:
|
|
11
|
+
nano-workforce **ADR 0005** (agent-authored delivery graphs — this ADR carries out 0005's
|
|
12
|
+
already-stated framing that the delivery graph is the *general* form, of which an epic is a waved DAG
|
|
13
|
+
and a feature a degenerate 1-node graph, by converging the data and process encodings onto it),
|
|
14
|
+
nano-workforce **ADR 0001** (cross-repo epics + the generic `ReadinessProbe` wait-gate — the epic
|
|
15
|
+
substrate being consolidated),
|
|
16
|
+
nano-workforce **ADR 0002** (escalations are user tasks + forms — the human-escalation cell that is
|
|
17
|
+
one of the copy-pasted subprocesses),
|
|
18
|
+
nano-bpm **ADR 0065** (reconciling read models / `defineReadModel` — the derivation mechanism S1 uses
|
|
19
|
+
to collapse three bespoke status unions into one),
|
|
20
|
+
nano-ide **#424** (datasource can read a SQL VIEW — the *data-level* unlock),
|
|
21
|
+
nano-workforce **#416** (the PR bumping the testkit to engine-wasm 0.7.2, which executes `callActivity`
|
|
22
|
+
— the *process-level* unlock),
|
|
23
|
+
nano-workforce **#464** (the tracking issue with slices S1–S5),
|
|
24
|
+
nano-workforce **#305** (consolidate escalations on native `user_tasks` — a natural sub-step of S1/S3).
|
|
25
|
+
|
|
26
|
+
## Context
|
|
27
|
+
|
|
28
|
+
### One aggregate, encoded three times
|
|
29
|
+
|
|
30
|
+
nano-workforce models the same real-world thing — **a scheduled unit of work driven to a delivery
|
|
31
|
+
outcome** (typically an agent taking one slice to one merged PR, but a unit may instead `wait`, ask a
|
|
32
|
+
`human`, call a `connector`, or — for a feature — terminate `opened`/`converged` without a merged PR;
|
|
33
|
+
and an epic aggregates *many* such units/PRs) — in three separate representations, each with its own
|
|
34
|
+
table, status union, operator read-surface, `instanceTracking` binding, and dispatch door:
|
|
35
|
+
|
|
36
|
+
| Representation | Data | Process | Shape |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| **Feature** | `feature_runs` (mig. 028) | `resources/processes/feature.bpmn` | one issue → one PR (1-node) |
|
|
39
|
+
| **Epic** | `plans` + `plan_tasks` (mig. 004) | `resources/processes/plan-fanout.bpmn` | fan-out of slices → waves (N-node) |
|
|
40
|
+
| **Delivery graph** | `delivery_graph_runs` (mig. 058) + compiled nodes | compiled BPMN (`app/deliveryGraphCompiler.ts`) | arbitrary DAG |
|
|
41
|
+
|
|
42
|
+
All three are keyed by an issue/run key and carry their own status union. Feature and epic each also
|
|
43
|
+
project a dedicated display read-model VIEW; delivery-graph pages instead bind **directly** to
|
|
44
|
+
`delivery_graph_runs` (`pages/delivery-graphs.page.json`, `pages/delivery-graph-detail.page.json`).
|
|
45
|
+
Feature and epic execute through **hand-authored** BPMN (`feature.bpmn`, `plan-fanout.bpmn`) and
|
|
46
|
+
dispatch hard-coded `senior:*` implementation jobs that funnel downstream to the *same* `pull_requests`
|
|
47
|
+
table (keyed by `pr_key`) — the convergence/merge loop, which they correctly do **not** duplicate.
|
|
48
|
+
Delivery graphs are the exception: only they are **compiled** to BPMN from JSON at runtime, they accept
|
|
49
|
+
the submitted node kind and `agent.jobType` (including `wait`, `human`, and `connector` nodes), and
|
|
50
|
+
`delivery_graph_runs` carries no `pr_key`, so a graph is not inherently PR-producing. So for the
|
|
51
|
+
feature/epic implementation path the *downstream* half of the aggregate is already factored to a single
|
|
52
|
+
source of truth; only the *upstream* "unit of work" half is triplicated.
|
|
53
|
+
|
|
54
|
+
ADR 0005 already names this: "plan-fanout is an epic — a `RecordPlanTask[]` + `dependsOn[]` DAG with
|
|
55
|
+
waves", "convergence-loop is one PR", and a feature is the degenerate one-node graph. The delivery
|
|
56
|
+
graph is the general form. What 0005 did *not* do is converge the data and process encodings onto that
|
|
57
|
+
general form — so we still carry three near-duplicate sources of truth for one aggregate. That is
|
|
58
|
+
precisely the "no drift surfaces / derivation over duplication" hazard this project treats as a defect
|
|
59
|
+
class: a change to the meaning of "a unit of work" has to be made, by hand, in three places that can
|
|
60
|
+
silently drift.
|
|
61
|
+
|
|
62
|
+
### The duplication was *forced* by two "can't-reference" constraints — both now lifted
|
|
63
|
+
|
|
64
|
+
The triplication is not a design preference; it was compelled by two symmetric constraints, one on
|
|
65
|
+
each encoding. Both have now been removed on `main`, which is why consolidation becomes possible now
|
|
66
|
+
rather than earlier.
|
|
67
|
+
|
|
68
|
+
**Data — "the datasource can't read a SQL VIEW."** Because the read layer could not read a VIEW, every
|
|
69
|
+
*display projection* had to be a physically **denormalized table** — or denormalized columns
|
|
70
|
+
hand-maintained on a source table (e.g. migrations 022/029 on `plans`) — rather than a VIEW derived
|
|
71
|
+
from its source. That is what made a *shared* display projection impossible: each representation grew
|
|
72
|
+
its own hand-maintained projection. (The delivery-unit source tables themselves —
|
|
73
|
+
`feature_runs`/028, `plans`+`plan_tasks`/004, `delivery_graph_runs`/058 — are domain stores, not
|
|
74
|
+
projection artifacts.) **Unlocked by nano-ide#424** (the datasource can now read a VIEW) → the derived
|
|
75
|
+
read-model VIEWs added in migrations 059–062, 064, and 073 (later refined by 074/075), with 070–072
|
|
76
|
+
dropping the now-redundant denormalized columns/tables. **Live today.**
|
|
77
|
+
|
|
78
|
+
**Process — "the pinned WASM engine no-ops `callActivity`."** `app/deliveryGraphCompiler.ts:47-51`
|
|
79
|
+
(and again at 604-607) records the constraint verbatim: `callActivity` is "a no-op on the pinned WASM
|
|
80
|
+
engine (the child is never instantiated)", so the compiler — and every hand-written process —
|
|
81
|
+
**inlines** the subprocess body instead of referencing it. The consequence is that the atomic
|
|
82
|
+
*"agent-implement cell"* (`implement-task (senior:*) → "escalated?" gateway → record-escalation →
|
|
83
|
+
user-task → SLA boundary → answer gateway`) exists as **two hand-authored copies** — `feature.bpmn` and
|
|
84
|
+
the multi-instance `implement` subprocess in `plan-fanout.bpmn` — plus a **third generator** in the
|
|
85
|
+
compiler that re-emits it once per graph node. Sibling cells
|
|
86
|
+
(readiness-poll, human-escalation) duplicate the same way. **No `callActivity` exists in any diagram**
|
|
87
|
+
because it did nothing. **Unlocked by #416** (engine-wasm 0.4.0 → **0.7.2**). engine-core executes
|
|
88
|
+
`callActivity` by inline-expanding the called process at deploy (`engine-core/src/model.rs:1217/1255`).
|
|
89
|
+
**Verified live:** a `callActivity` parent+child model deployed through engine-wasm 0.7.2 runs to
|
|
90
|
+
`COMPLETED`. Caveat: #416 bumps only the **dev-only** `@nanobpm/urban-testkit`; the production
|
|
91
|
+
`@nanobpm/urban` broker does not itself pin `engine-wasm`, so this verification proves the in-process
|
|
92
|
+
testkit, not the broker/runtime that will execute future `callActivity` models. S4/S5 therefore also
|
|
93
|
+
carry a **deployment-runtime prerequisite** — the deployed broker's `engine-core` must carry the same
|
|
94
|
+
`callActivity` support — which green testkit CI does not by itself guarantee.
|
|
95
|
+
|
|
96
|
+
### The two constraints are the *same* constraint
|
|
97
|
+
|
|
98
|
+
Both are "an encoding can't *reference* a shared definition, so it *inlines a copy* of it." Data
|
|
99
|
+
inlined projection tables; process inlined subprocess bodies. Both share the same aggregate (the
|
|
100
|
+
delivery unit), the same fix shape (remove the can't-reference constraint, then reference instead of
|
|
101
|
+
copy), and both already have their downstream half factored correctly (`pull_requests`; the
|
|
102
|
+
convergence/merge loop). Removing one constraint without the other would leave the aggregate
|
|
103
|
+
half-consolidated; removing both is what makes a single representation reachable.
|
|
104
|
+
|
|
105
|
+
## Decision
|
|
106
|
+
|
|
107
|
+
Adopt a single internal aggregate — the **delivery unit** — defined around its **nodes**: a node is one
|
|
108
|
+
**scheduled unit of work** whose executor may be an **agent, probe, human, or connector** (ADR 0005
|
|
109
|
+
`wait`/`human`/`connector` nodes are first-class, not exceptions). Its terminal is *typically* one merged
|
|
110
|
+
PR, but PR-less nodes and delivery graphs (`delivery_graph_runs` has no `pr_key`) make PR production
|
|
111
|
+
**optional**.
|
|
112
|
+
It has **two encodings**, each of which now *references* the shared definition rather than inlining
|
|
113
|
+
a copy — expressed here as the **target** state:
|
|
114
|
+
|
|
115
|
+
### 1. Data encoding — one `delivery_unit` aggregate
|
|
116
|
+
|
|
117
|
+
- A `delivery_units` table is the single source of truth for "a unit of work." Feature = a 1-node
|
|
118
|
+
unit; Epic = an N-node waved unit; DeliveryGraph = an arbitrary-DAG unit — a **shape**, not a
|
|
119
|
+
separate table.
|
|
120
|
+
- `feature_runs` / `plans` + `plan_tasks` / `delivery_graph_runs` become **derived VIEWs / rows** over
|
|
121
|
+
`delivery_units` (using the nano-ide#424 VIEW capability), not independent tables. The epic case
|
|
122
|
+
covers **both** levels: the `plans` aggregate row (process key, status, title, lifecycle) becomes a
|
|
123
|
+
row/VIEW over `delivery_units`, and each `plan_tasks` slice becomes a node under it.
|
|
124
|
+
- The three `instanceTracking` bindings and `senior:*` dispatch doors collapse toward one, keyed on
|
|
125
|
+
the delivery unit.
|
|
126
|
+
- **Identity.** `delivery_units` carries a stable `unit_id` plus `(unit_id, node_id)` for the N-node
|
|
127
|
+
cases. The legacy keys are not interchangeable — a feature run and an active epic may share one
|
|
128
|
+
`<owner>/<repo>#<N>` key (`app/feature.ts`), while delivery graphs key on a caller idempotency key or
|
|
129
|
+
content digest (`app/deliveryGraphRun.ts` `computeRunKey`) — so S2's compatibility VIEWs map each
|
|
130
|
+
legacy key onto the new identity, ensuring unrelated runs are never merged onto one row.
|
|
131
|
+
|
|
132
|
+
### 2. Process encoding — shared cells composed by `callActivity`
|
|
133
|
+
|
|
134
|
+
- Extract the atomic *implement-cell* (and its sibling wait-gate and human-escalation cells) into
|
|
135
|
+
standalone processes (`resources/processes/implement-cell.bpmn`, …).
|
|
136
|
+
- Compose them by reference — replacing only the inlined *implement/escalation segment*, not the
|
|
137
|
+
surrounding orchestration: in **feature** (`feature.bpmn`) the readiness preflight, base-branch setup,
|
|
138
|
+
`record-feature`, and convergence handoff are retained; only the implement-cell segment becomes one
|
|
139
|
+
`callActivity`. **Epic** = the multi-instance `implement` body is a `callActivity`; **delivery graph**
|
|
140
|
+
= the compiler *emits* `callActivity` references, not inlined subprocess copies.
|
|
141
|
+
- This is gated on the engine-wasm 0.7.2 unlock, which is now live on `main`.
|
|
142
|
+
|
|
143
|
+
### 3. Status lifecycle — one derived union
|
|
144
|
+
|
|
145
|
+
The three bespoke status unions collapse into **one derived union** via ADR 0065's `defineReadModel`,
|
|
146
|
+
so a change to lifecycle semantics is made once and derived everywhere, not re-declared per
|
|
147
|
+
representation. These unions are **not** identical today — features use
|
|
148
|
+
`running`/`escalated`/`awaiting_operator`/…, plans use `planning`/`dispatched`/`done`, and graphs use
|
|
149
|
+
`running`/`done` with a reserved `awaiting-approval` (`app/feature.ts`, `app/plan.ts`,
|
|
150
|
+
`app/deliveryGraphRun.ts`); §1 additionally makes each `plan_tasks` row a **node**, which carries its own
|
|
151
|
+
`PlanTaskStatus` (`pending`/`waiting-for-lane`/…, `app/plan.ts`). So S1 owns defining the canonical
|
|
152
|
+
**aggregate** state set *and* explicitly deciding whether **node** status is part of that union or a
|
|
153
|
+
separate node contract — plus the per-shape mapping and precedence and the write/`instanceTracking`
|
|
154
|
+
behavior — not merely projecting an existing value.
|
|
155
|
+
|
|
156
|
+
### 4. Preserve — the static-vs-adaptive execution axis (do NOT bundle it)
|
|
157
|
+
|
|
158
|
+
This ADR consolidates the *representation*, not the *execution strategy*. ADR 0005's deliberate
|
|
159
|
+
distinction stays intact: **plan-fanout remains adaptive** (agent-discovered slices, waves that adapt),
|
|
160
|
+
**delivery graphs remain static/compiled**. Both are still *delivery units*; they differ only in how
|
|
161
|
+
their topology is produced. Unifying that axis is explicitly out of scope here.
|
|
162
|
+
|
|
163
|
+
## Consequences
|
|
164
|
+
|
|
165
|
+
- **Single source of truth for a unit of work.** A change to the meaning of "a delivery unit" — a new
|
|
166
|
+
status, a lifecycle rule, a step in the implement cell — is made once and derived into every
|
|
167
|
+
representation, eliminating the three-way drift surface this project treats as a defect class.
|
|
168
|
+
- **Renderability + executability both improve.** One implement-cell process is one thing to keep
|
|
169
|
+
deploy-valid and lay out, instead of **two hand-authored copies plus a compiler generator** that can
|
|
170
|
+
silently diverge (a graph can render and still fail deploy — the copies are exactly where that
|
|
171
|
+
divergence hides).
|
|
172
|
+
- **Migration is incremental and forward-only.** The VIEWs preserve every current read shape while the
|
|
173
|
+
physical model consolidates underneath, and each slice below is independently shippable. Consistent
|
|
174
|
+
with this repo's forward-only, expand-and-contract migration contract (see
|
|
175
|
+
`070_drop_plan_projection_columns.sql`, which treats dropping projection columns as a later contract
|
|
176
|
+
phase), a slice is **not** reverted by reverting the app: rolling back a writer-repointing or
|
|
177
|
+
table-to-VIEW slice requires a **separately designed recovery/compatibility migration**, not a plain
|
|
178
|
+
revert.
|
|
179
|
+
- **Cost.** A backfill/migration for `delivery_units`; a one-time extraction of the shared cells; and
|
|
180
|
+
the process slices are sequenced behind the (now-live) engine-wasm unlock. No behaviour change is
|
|
181
|
+
intended — this is a representation consolidation, guarded by parity tests against the existing VIEWs
|
|
182
|
+
and by the deploy+run engine tests.
|
|
183
|
+
|
|
184
|
+
## Rollout (see #464 for the live checklist)
|
|
185
|
+
|
|
186
|
+
Each slice is independently shippable; the process slices (S4/S5) are sequenced behind the engine-wasm
|
|
187
|
+
0.7.2 unlock. The **dev-testkit** side of that unlock has landed (#416, verified in-process above); S4/S5
|
|
188
|
+
additionally gate on the **deployed broker/runtime** carrying verified `callActivity` support (the
|
|
189
|
+
deployment-runtime prerequisite noted above), not on #416 alone.
|
|
190
|
+
|
|
191
|
+
- **S0 · ADR** — this record.
|
|
192
|
+
- **S1 · status lifecycle** — one derived status union via ADR 0065 `defineReadModel`, replacing the
|
|
193
|
+
three bespoke unions. This **depends on and overlaps** #305 (consolidate escalations on native
|
|
194
|
+
`user_tasks`) but does not subsume it: #305 additionally retires the `feature_runs` escalation
|
|
195
|
+
columns and the bespoke completion doors and updates the escalation UI/forms, which remain #305's
|
|
196
|
+
scope (an adjacent sub-step of S1/S3 per #464).
|
|
197
|
+
- **S2 · `delivery_units` table** — the aggregate. Because current code still **writes**
|
|
198
|
+
`feature_runs` / `plans` / `plan_tasks` / `delivery_graph_runs` directly (`app/feature.ts`,
|
|
199
|
+
`app/plan.ts`, `app/deliveryGraphRun.ts`) — **and** the framework's `instanceTracking` bindings in
|
|
200
|
+
`nano.app.json` write termination status to `feature_runs`, `plans`, and `delivery_graph_runs` — and a
|
|
201
|
+
SQLite VIEW is read-only, follow expand/contract **order**: (a) add `delivery_units` and dual-write it
|
|
202
|
+
alongside the legacy tables; (b) backfill existing/legacy rows; (c) repoint reads to VIEWs/rows derived
|
|
203
|
+
from `delivery_units`, guarded by read-model parity tests. The legacy tables stay **physical
|
|
204
|
+
(writable) through S2** — they must **not** become read-only VIEWs while any writer, including the
|
|
205
|
+
`instanceTracking` termination-reconciliation bindings, still targets them, or reconciliation fails
|
|
206
|
+
with no writable target left for S3 to move. (d) Only after **S3** has moved those `instanceTracking`
|
|
207
|
+
bindings and every other writer off the legacy tables does the table-to-VIEW contract phase retire the
|
|
208
|
+
legacy write paths.
|
|
209
|
+
- **S3 · collapse doors** — unify the three `instanceTracking` bindings + `senior:*` dispatch doors.
|
|
210
|
+
- **S4 · shared cells** — extract the atomic `implement-cell.bpmn` **and its sibling wait-gate and
|
|
211
|
+
human-escalation cells** (Decision §2) into standalone processes; `feature.bpmn` + the `plan-fanout`
|
|
212
|
+
MI body compose them via `callActivity`.
|
|
213
|
+
- **S5 · compiler emits calls** — `deliveryGraphCompiler` references shared cells instead of inlining
|
|
214
|
+
per-node copies.
|
|
215
|
+
|
|
216
|
+
## Non-goals / deferred
|
|
217
|
+
|
|
218
|
+
- **Unifying the static-vs-adaptive execution axis** (see Decision §4) — preserved deliberately.
|
|
219
|
+
- **Changing the downstream PR/convergence loop** — already single-sourced (`pull_requests`); untouched.
|
|
220
|
+
- **Cross-repo/platform representation** — this ADR is nano-workforce-local; any platform-wide delivery
|
|
221
|
+
aggregate would be a separate nano-bpm ADR.
|