@nanobpm/nano-workforce 0.201.5 → 0.203.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 +12 -0
- package/SPEC.md +48 -0
- package/app/admittedStaging.test.ts +71 -0
- package/app/agentActivationLedger.ts +5 -0
- package/app/agentCompletion.test.ts +33 -0
- package/app/agentCompletion.ts +108 -2
- package/app/agentic/README.md +48 -0
- package/app/agentic/claim-registry.test.ts +61 -0
- package/app/agentic/claim-registry.ts +35 -1
- package/app/agentic/families/claim.family.test.ts +216 -0
- package/app/agentic/families/claim.family.ts +100 -4
- package/app/agentic/vocab/agent-sla-boundary.test.ts +6 -1
- package/app/contracts.ts +31 -0
- package/app/deliveryGraphDispatch.ts +6 -0
- package/app/deliveryGraphReadModel.test.ts +2 -2
- package/app/deliveryGraphRun.ts +18 -0
- package/app/deliveryRunner.ts +9 -1
- package/app/escalationSteward.test.ts +452 -0
- package/app/escalationSteward.ts +537 -0
- package/app/escalationStewardPoll.test.ts +123 -0
- package/app/feature.test.ts +16 -0
- package/app/feature.ts +11 -0
- package/app/featureDelivery.test.ts +23 -0
- package/app/mcpToolSurface.ts +17 -1
- package/app/plan.ts +49 -6
- package/app/planLowering.test.ts +26 -1
- package/app/planLowering.ts +5 -0
- package/app/promotionPoll.test.ts +19 -0
- package/app/pullRequestReadModel.test.ts +1 -1
- package/app/service.test.ts +36 -0
- package/app/service.ts +139 -5
- package/db/migrations/120_escalation_auto_resolutions.sql +229 -0
- package/db/migrations/121_delivery_graph_read_model_auto_resolve.sql +54 -0
- package/db/migrations/122_pull_requests_read_model_auto_resolve.sql +56 -0
- package/db/migrations/123_escalation_auto_resolutions_selected_option.sql +47 -0
- package/db/migrations/124_admitted_epics_auto_resolve.sql +21 -0
- package/docs/agent-guide.md +69 -30
- package/docs/agentic-cockpit.md +34 -0
- package/docs/mcp-runbook.md +2 -0
- package/e2e/escalation-steward.e2e.ts +211 -0
- package/nano.app.json +10 -1
- package/openapi.yaml +365 -0
- package/operations/dispatchDeliveryGraph.ts +6 -1
- package/operations/listActivePrs.test.ts +60 -1
- package/operations/listActivePrs.ts +3 -3
- package/operations/listAutoResolutions.ts +83 -0
- package/operations/reviewAutoResolution.test.ts +137 -0
- package/operations/reviewAutoResolution.ts +42 -0
- package/operations/startConvergenceLoop.ts +5 -1
- package/operations/startEpicSet.admission.integration.test.ts +2 -2
- package/operations/startEpicSet.ts +8 -2
- package/operations/startFeature.ts +5 -0
- package/operations/startPlanFanout.ts +4 -1
- package/package.json +1 -1
- package/pages/_nav.json +12 -0
- package/pages/auto-resolutions.page.json +191 -0
- package/pages/board.page.json +17 -0
- package/pages/cockpit.page.json +17 -0
- package/pages/delivery-graph-detail.page.json +17 -0
- package/pages/delivery-graphs/staged.mount.js +17 -5
- package/pages/delivery-graphs.page.json +17 -0
- package/pages/epic-detail.page.json +17 -0
- package/pages/epic.page.json +19 -1
- package/pages/feature.page.json +19 -1
- package/pages/home.page.json +22 -0
- package/pages/lineage.page.json +17 -0
- package/pages/mcp.page.json +17 -0
- package/pages/overview.page.json +17 -0
- package/pages/tasks.page.json +17 -0
- package/pages/velocity.page.json +17 -0
- package/resources/processes/escalation-steward.bpmn +162 -0
- package/resources/prompts/escalation-steward.md +87 -0
- package/test/delivery-graphs-staged-embed.test.ts +13 -0
- package/test/derivation-parity/derivation-parity.test.ts +1 -0
- package/test/derivation-parity/flows.ts +12 -0
- package/workers/converge-feature/worker.ts +6 -1
- package/workers/delivery-connector/worker.test.ts +72 -1
- package/workers/delivery-connector/worker.ts +53 -2
- package/workers/escalation-steward-collect/worker.test.ts +180 -0
- package/workers/escalation-steward-collect/worker.ts +99 -0
- package/workers/escalation-steward-record/worker.test.ts +740 -0
- package/workers/escalation-steward-record/worker.ts +428 -0
- package/workers/record-wave/worker.ts +5 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
-- Issue #889: opt-in escalation steward. Two halves, both additive (expand-and-contract):
|
|
2
|
+
--
|
|
3
|
+
-- 1. `auto_resolve_escalations` (0|1, default 0) on each escalation-raising subject table —
|
|
4
|
+
-- `feature_runs`, `plans`, `pull_requests`, `delivery_graph_runs` — recording whether the work
|
|
5
|
+
-- OPTED IN to having its open escalations auto-resolved by the steward. The steward's
|
|
6
|
+
-- eligibility derivation (app/escalationSteward.ts) reads the flag off the SUBJECT row keyed by
|
|
7
|
+
-- the escalation's owning process instance, so an escalation from non-opted-in work is never
|
|
8
|
+
-- touched. Mirrored into the `delivery_units` aggregate (ADR 0006) — the sync triggers (089) and
|
|
9
|
+
-- compat VIEWs (091) are re-created verbatim plus the new column, exactly as migration 116 did
|
|
10
|
+
-- for `human_approval` (parity-tested in app/deliveryUnit.test.ts).
|
|
11
|
+
--
|
|
12
|
+
-- 2. `escalation_auto_resolutions` — the durable, reviewable record of every escalation the steward
|
|
13
|
+
-- ACTED ON or ABSTAINED from (one row each), backing the Auto-resolutions view and the
|
|
14
|
+
-- dismiss/intervene doors. `review_state` starts `pending`; an operator `dismiss`/`intervene`
|
|
15
|
+
-- folds it to `dismissed`/`intervened` with `reviewed_by`/`reviewed_at`/`review_note`.
|
|
16
|
+
|
|
17
|
+
ALTER TABLE feature_runs ADD COLUMN auto_resolve_escalations INTEGER NOT NULL DEFAULT 0;
|
|
18
|
+
ALTER TABLE plans ADD COLUMN auto_resolve_escalations INTEGER NOT NULL DEFAULT 0;
|
|
19
|
+
ALTER TABLE pull_requests ADD COLUMN auto_resolve_escalations INTEGER NOT NULL DEFAULT 0;
|
|
20
|
+
ALTER TABLE delivery_graph_runs ADD COLUMN auto_resolve_escalations INTEGER NOT NULL DEFAULT 0;
|
|
21
|
+
|
|
22
|
+
-- Mirror the new column through the `delivery_units` aggregate so the compat VIEWs stay row-for-row
|
|
23
|
+
-- equal to their base tables (ADR 0006, parity-tested in app/deliveryUnit.test.ts).
|
|
24
|
+
ALTER TABLE delivery_units ADD COLUMN auto_resolve_escalations INTEGER;
|
|
25
|
+
UPDATE delivery_units SET auto_resolve_escalations = 0 WHERE auto_resolve_escalations IS NULL;
|
|
26
|
+
|
|
27
|
+
DROP TRIGGER IF EXISTS feature_runs__du_ai;
|
|
28
|
+
CREATE TRIGGER feature_runs__du_ai AFTER INSERT ON feature_runs
|
|
29
|
+
BEGIN
|
|
30
|
+
INSERT OR REPLACE INTO delivery_units (unit_id, kind, legacy_key, legacy_id, parent_unit_id, node_index, delivery_status, dispatch_status, repo, issue_number, issue_url, title, base_branch, status, process_key, pr_key, converge, auto_merge, human_approval, auto_resolve_escalations, outcome, delivery_label, acknowledged_at, stage, stage_state, stage_skipped, attention, list_bucket, created_at, updated_at)
|
|
31
|
+
VALUES ('feature:' || NEW.feature_key, 'feature', NEW.feature_key, NULL, NULL, NULL, CASE WHEN NEW.status = 'running' THEN 'running' WHEN NEW.status = 'escalated' THEN 'escalated' WHEN NEW.status = 'opened' THEN 'opened' WHEN NEW.status = 'converging' THEN 'converging' WHEN NEW.status = 'awaiting_operator' THEN 'awaiting_operator' WHEN NEW.status = 'merged' THEN 'merged' WHEN NEW.status = 'converged' THEN 'converged' WHEN NEW.status = 'blocked' THEN 'blocked' WHEN NEW.status = 'skipped' THEN 'skipped' WHEN NEW.status = 'failed' THEN 'failed' WHEN NEW.status = 'abandoned' THEN 'abandoned' ELSE NULL END, CASE WHEN NEW.status IN ('opened', 'converging', 'merged', 'converged', 'blocked', 'skipped', 'failed', 'abandoned') THEN 'settled' WHEN NEW.status IN ('running', 'escalated', 'awaiting_operator') THEN 'dispatched' ELSE NULL END, NEW.repo, NEW.issue_number, NEW.issue_url, NEW.title, NEW.base_branch, NEW.status, NEW.process_key, NEW.pr_key, NEW.converge, NEW.auto_merge, NEW.human_approval, NEW.auto_resolve_escalations, NEW.outcome, NEW.delivery_label, NEW.acknowledged_at, NEW.stage, NEW.stage_state, NEW.stage_skipped, NEW.attention, NEW.list_bucket, NEW.created_at, NEW.updated_at);
|
|
32
|
+
END;
|
|
33
|
+
|
|
34
|
+
DROP TRIGGER IF EXISTS feature_runs__du_au;
|
|
35
|
+
CREATE TRIGGER feature_runs__du_au AFTER UPDATE ON feature_runs
|
|
36
|
+
BEGIN
|
|
37
|
+
INSERT OR REPLACE INTO delivery_units (unit_id, kind, legacy_key, legacy_id, parent_unit_id, node_index, delivery_status, dispatch_status, repo, issue_number, issue_url, title, base_branch, status, process_key, pr_key, converge, auto_merge, human_approval, auto_resolve_escalations, outcome, delivery_label, acknowledged_at, stage, stage_state, stage_skipped, attention, list_bucket, created_at, updated_at)
|
|
38
|
+
VALUES ('feature:' || NEW.feature_key, 'feature', NEW.feature_key, NULL, NULL, NULL, CASE WHEN NEW.status = 'running' THEN 'running' WHEN NEW.status = 'escalated' THEN 'escalated' WHEN NEW.status = 'opened' THEN 'opened' WHEN NEW.status = 'converging' THEN 'converging' WHEN NEW.status = 'awaiting_operator' THEN 'awaiting_operator' WHEN NEW.status = 'merged' THEN 'merged' WHEN NEW.status = 'converged' THEN 'converged' WHEN NEW.status = 'blocked' THEN 'blocked' WHEN NEW.status = 'skipped' THEN 'skipped' WHEN NEW.status = 'failed' THEN 'failed' WHEN NEW.status = 'abandoned' THEN 'abandoned' ELSE NULL END, CASE WHEN NEW.status IN ('opened', 'converging', 'merged', 'converged', 'blocked', 'skipped', 'failed', 'abandoned') THEN 'settled' WHEN NEW.status IN ('running', 'escalated', 'awaiting_operator') THEN 'dispatched' ELSE NULL END, NEW.repo, NEW.issue_number, NEW.issue_url, NEW.title, NEW.base_branch, NEW.status, NEW.process_key, NEW.pr_key, NEW.converge, NEW.auto_merge, NEW.human_approval, NEW.auto_resolve_escalations, NEW.outcome, NEW.delivery_label, NEW.acknowledged_at, NEW.stage, NEW.stage_state, NEW.stage_skipped, NEW.attention, NEW.list_bucket, NEW.created_at, NEW.updated_at);
|
|
39
|
+
END;
|
|
40
|
+
|
|
41
|
+
DROP TRIGGER IF EXISTS plans__du_ai;
|
|
42
|
+
CREATE TRIGGER plans__du_ai AFTER INSERT ON plans
|
|
43
|
+
BEGIN
|
|
44
|
+
INSERT OR REPLACE INTO delivery_units (unit_id, kind, legacy_key, legacy_id, parent_unit_id, node_index, delivery_status, dispatch_status, repo, issue_number, issue_url, title, status, task_count, process_key, outcome, gate_wave, blackboard_token, retro_started_at, base_branch, epic_phase, promotion_pr, promotion_state, acknowledged_at, list_bucket, ack_open, wait_gate, wait_gate_label, bound_artifacts, auto_resolve_escalations, created_at, updated_at)
|
|
45
|
+
VALUES ('epic:' || NEW.plan_key, 'epic', NEW.plan_key, NULL, NULL, NULL, CASE WHEN NEW.status = 'planning' THEN 'requested' WHEN NEW.status = 'dispatched' THEN 'running' WHEN NEW.status = 'done' THEN 'done' WHEN NEW.status = 'failed' THEN 'failed' WHEN NEW.status = 'abandoned' THEN 'abandoned' ELSE NULL END, CASE WHEN NEW.status IN ('planning') THEN 'pending' WHEN NEW.status IN ('done', 'failed', 'abandoned') THEN 'settled' WHEN NEW.status IN ('dispatched') THEN 'dispatched' ELSE NULL END, NEW.repo, NEW.issue_number, NEW.issue_url, NEW.title, NEW.status, NEW.task_count, NEW.process_key, NEW.outcome, NEW.gate_wave, NEW.blackboard_token, NEW.retro_started_at, NEW.base_branch, NEW.epic_phase, NEW.promotion_pr, NEW.promotion_state, NEW.acknowledged_at, NEW.list_bucket, NEW.ack_open, NEW.wait_gate, NEW.wait_gate_label, NEW.bound_artifacts, NEW.auto_resolve_escalations, NEW.created_at, NEW.updated_at);
|
|
46
|
+
END;
|
|
47
|
+
|
|
48
|
+
DROP TRIGGER IF EXISTS plans__du_au;
|
|
49
|
+
CREATE TRIGGER plans__du_au AFTER UPDATE ON plans
|
|
50
|
+
BEGIN
|
|
51
|
+
INSERT OR REPLACE INTO delivery_units (unit_id, kind, legacy_key, legacy_id, parent_unit_id, node_index, delivery_status, dispatch_status, repo, issue_number, issue_url, title, status, task_count, process_key, outcome, gate_wave, blackboard_token, retro_started_at, base_branch, epic_phase, promotion_pr, promotion_state, acknowledged_at, list_bucket, ack_open, wait_gate, wait_gate_label, bound_artifacts, auto_resolve_escalations, created_at, updated_at)
|
|
52
|
+
VALUES ('epic:' || NEW.plan_key, 'epic', NEW.plan_key, NULL, NULL, NULL, CASE WHEN NEW.status = 'planning' THEN 'requested' WHEN NEW.status = 'dispatched' THEN 'running' WHEN NEW.status = 'done' THEN 'done' WHEN NEW.status = 'failed' THEN 'failed' WHEN NEW.status = 'abandoned' THEN 'abandoned' ELSE NULL END, CASE WHEN NEW.status IN ('planning') THEN 'pending' WHEN NEW.status IN ('done', 'failed', 'abandoned') THEN 'settled' WHEN NEW.status IN ('dispatched') THEN 'dispatched' ELSE NULL END, NEW.repo, NEW.issue_number, NEW.issue_url, NEW.title, NEW.status, NEW.task_count, NEW.process_key, NEW.outcome, NEW.gate_wave, NEW.blackboard_token, NEW.retro_started_at, NEW.base_branch, NEW.epic_phase, NEW.promotion_pr, NEW.promotion_state, NEW.acknowledged_at, NEW.list_bucket, NEW.ack_open, NEW.wait_gate, NEW.wait_gate_label, NEW.bound_artifacts, NEW.auto_resolve_escalations, NEW.created_at, NEW.updated_at);
|
|
53
|
+
END;
|
|
54
|
+
|
|
55
|
+
DROP TRIGGER IF EXISTS delivery_graph_runs__du_ai;
|
|
56
|
+
CREATE TRIGGER delivery_graph_runs__du_ai AFTER INSERT ON delivery_graph_runs
|
|
57
|
+
BEGIN
|
|
58
|
+
INSERT OR REPLACE INTO delivery_units (unit_id, kind, legacy_key, legacy_id, parent_unit_id, node_index, delivery_status, dispatch_status, process_key, process_definition_id, digest, status, side_effecting, node_count, human_node_count, side_effect_count, title, phase, phase_node_id, human_labels, acknowledged_at, auto_resolve_escalations, created_at, updated_at)
|
|
59
|
+
VALUES ('delivery-graph:' || NEW.run_key, 'delivery-graph', NEW.run_key, NULL, NULL, NULL, CASE WHEN NEW.status = 'awaiting-approval' THEN 'requested' WHEN NEW.status = 'running' THEN 'running' WHEN NEW.status = 'done' THEN 'done' WHEN NEW.status = 'failed' THEN 'failed' WHEN NEW.status = 'abandoned' THEN 'abandoned' ELSE NULL END, CASE WHEN NEW.status IN ('awaiting-approval') THEN 'pending' WHEN NEW.status IN ('done', 'failed', 'abandoned') THEN 'settled' WHEN NEW.status IN ('running') THEN 'dispatched' ELSE NULL END, NEW.process_key, NEW.process_definition_id, NEW.digest, NEW.status, NEW.side_effecting, NEW.node_count, NEW.human_node_count, NEW.side_effect_count, NEW.title, NEW.phase, NEW.phase_node_id, NEW.human_labels, NEW.acknowledged_at, NEW.auto_resolve_escalations, NEW.created_at, NEW.updated_at);
|
|
60
|
+
END;
|
|
61
|
+
|
|
62
|
+
DROP TRIGGER IF EXISTS delivery_graph_runs__du_au;
|
|
63
|
+
CREATE TRIGGER delivery_graph_runs__du_au AFTER UPDATE ON delivery_graph_runs
|
|
64
|
+
BEGIN
|
|
65
|
+
INSERT OR REPLACE INTO delivery_units (unit_id, kind, legacy_key, legacy_id, parent_unit_id, node_index, delivery_status, dispatch_status, process_key, process_definition_id, digest, status, side_effecting, node_count, human_node_count, side_effect_count, title, phase, phase_node_id, human_labels, acknowledged_at, auto_resolve_escalations, created_at, updated_at)
|
|
66
|
+
VALUES ('delivery-graph:' || NEW.run_key, 'delivery-graph', NEW.run_key, NULL, NULL, NULL, CASE WHEN NEW.status = 'awaiting-approval' THEN 'requested' WHEN NEW.status = 'running' THEN 'running' WHEN NEW.status = 'done' THEN 'done' WHEN NEW.status = 'failed' THEN 'failed' WHEN NEW.status = 'abandoned' THEN 'abandoned' ELSE NULL END, CASE WHEN NEW.status IN ('awaiting-approval') THEN 'pending' WHEN NEW.status IN ('done', 'failed', 'abandoned') THEN 'settled' WHEN NEW.status IN ('running') THEN 'dispatched' ELSE NULL END, NEW.process_key, NEW.process_definition_id, NEW.digest, NEW.status, NEW.side_effecting, NEW.node_count, NEW.human_node_count, NEW.side_effect_count, NEW.title, NEW.phase, NEW.phase_node_id, NEW.human_labels, NEW.acknowledged_at, NEW.auto_resolve_escalations, NEW.created_at, NEW.updated_at);
|
|
67
|
+
END;
|
|
68
|
+
|
|
69
|
+
DROP VIEW IF EXISTS feature_runs__units;
|
|
70
|
+
CREATE VIEW feature_runs__units AS
|
|
71
|
+
SELECT
|
|
72
|
+
du.legacy_key AS feature_key,
|
|
73
|
+
du.repo AS repo,
|
|
74
|
+
du.issue_number AS issue_number,
|
|
75
|
+
du.issue_url AS issue_url,
|
|
76
|
+
du.base_branch AS base_branch,
|
|
77
|
+
du.status AS status,
|
|
78
|
+
du.process_key AS process_key,
|
|
79
|
+
du.pr_key AS pr_key,
|
|
80
|
+
du.converge AS converge,
|
|
81
|
+
du.auto_merge AS auto_merge,
|
|
82
|
+
du.human_approval AS human_approval,
|
|
83
|
+
du.auto_resolve_escalations AS auto_resolve_escalations,
|
|
84
|
+
du.outcome AS outcome,
|
|
85
|
+
du.created_at AS created_at,
|
|
86
|
+
du.updated_at AS updated_at,
|
|
87
|
+
du.delivery_label AS delivery_label,
|
|
88
|
+
du.title AS title,
|
|
89
|
+
du.acknowledged_at AS acknowledged_at,
|
|
90
|
+
du.stage AS stage,
|
|
91
|
+
du.stage_state AS stage_state,
|
|
92
|
+
du.stage_skipped AS stage_skipped,
|
|
93
|
+
du.attention AS attention,
|
|
94
|
+
du.list_bucket AS list_bucket
|
|
95
|
+
FROM delivery_units du
|
|
96
|
+
WHERE du.kind = 'feature';
|
|
97
|
+
|
|
98
|
+
DROP VIEW IF EXISTS plans__units;
|
|
99
|
+
CREATE VIEW plans__units AS
|
|
100
|
+
SELECT
|
|
101
|
+
du.legacy_key AS plan_key,
|
|
102
|
+
du.repo AS repo,
|
|
103
|
+
du.issue_number AS issue_number,
|
|
104
|
+
du.issue_url AS issue_url,
|
|
105
|
+
du.title AS title,
|
|
106
|
+
du.status AS status,
|
|
107
|
+
du.task_count AS task_count,
|
|
108
|
+
du.process_key AS process_key,
|
|
109
|
+
du.outcome AS outcome,
|
|
110
|
+
du.created_at AS created_at,
|
|
111
|
+
du.updated_at AS updated_at,
|
|
112
|
+
du.gate_wave AS gate_wave,
|
|
113
|
+
du.blackboard_token AS blackboard_token,
|
|
114
|
+
du.retro_started_at AS retro_started_at,
|
|
115
|
+
du.base_branch AS base_branch,
|
|
116
|
+
du.epic_phase AS epic_phase,
|
|
117
|
+
du.promotion_pr AS promotion_pr,
|
|
118
|
+
du.promotion_state AS promotion_state,
|
|
119
|
+
du.acknowledged_at AS acknowledged_at,
|
|
120
|
+
du.list_bucket AS list_bucket,
|
|
121
|
+
du.ack_open AS ack_open,
|
|
122
|
+
du.wait_gate AS wait_gate,
|
|
123
|
+
du.wait_gate_label AS wait_gate_label,
|
|
124
|
+
du.bound_artifacts AS bound_artifacts,
|
|
125
|
+
du.auto_resolve_escalations AS auto_resolve_escalations
|
|
126
|
+
FROM delivery_units du
|
|
127
|
+
WHERE du.kind = 'epic';
|
|
128
|
+
|
|
129
|
+
DROP VIEW IF EXISTS delivery_graph_runs__units;
|
|
130
|
+
CREATE VIEW delivery_graph_runs__units AS
|
|
131
|
+
SELECT
|
|
132
|
+
du.legacy_key AS run_key,
|
|
133
|
+
du.process_key AS process_key,
|
|
134
|
+
du.process_definition_id AS process_definition_id,
|
|
135
|
+
du.digest AS digest,
|
|
136
|
+
du.status AS status,
|
|
137
|
+
du.side_effecting AS side_effecting,
|
|
138
|
+
du.node_count AS node_count,
|
|
139
|
+
du.human_node_count AS human_node_count,
|
|
140
|
+
du.side_effect_count AS side_effect_count,
|
|
141
|
+
du.title AS title,
|
|
142
|
+
du.phase AS phase,
|
|
143
|
+
du.phase_node_id AS phase_node_id,
|
|
144
|
+
du.human_labels AS human_labels,
|
|
145
|
+
du.created_at AS created_at,
|
|
146
|
+
du.updated_at AS updated_at,
|
|
147
|
+
du.acknowledged_at AS acknowledged_at,
|
|
148
|
+
du.auto_resolve_escalations AS auto_resolve_escalations
|
|
149
|
+
FROM delivery_units du
|
|
150
|
+
WHERE du.kind = 'delivery-graph';
|
|
151
|
+
|
|
152
|
+
-- The durable, reviewable record of every escalation the steward acted on or abstained from.
|
|
153
|
+
CREATE TABLE escalation_auto_resolutions (
|
|
154
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
155
|
+
-- The escalation user task acted on (the engine `userTaskKey`), its BPMN element id, and the
|
|
156
|
+
-- escalation kind label at the time (denormalised so the record survives the task completing).
|
|
157
|
+
user_task_key TEXT NOT NULL,
|
|
158
|
+
escalation_kind TEXT,
|
|
159
|
+
element_id TEXT,
|
|
160
|
+
-- The subject the escalation belongs to (PR key / feature key / plan key / delivery run key).
|
|
161
|
+
subject_type TEXT,
|
|
162
|
+
subject_key TEXT,
|
|
163
|
+
subject_url TEXT,
|
|
164
|
+
process_instance_key TEXT,
|
|
165
|
+
-- Snapshots of what was asked and the choices on offer, so the record is reviewable after the fact.
|
|
166
|
+
question TEXT,
|
|
167
|
+
options TEXT, -- JSON array of the form's option values/labels
|
|
168
|
+
-- `resolved` (the steward submitted an answer) or `abstained` (it deliberately left it for a human).
|
|
169
|
+
outcome TEXT NOT NULL, -- resolved | abstained
|
|
170
|
+
submitted_variables TEXT, -- JSON of the variables submitted (NULL when abstained)
|
|
171
|
+
rationale TEXT, -- one-line why
|
|
172
|
+
evidence TEXT, -- JSON/markdown: what was checked, with links
|
|
173
|
+
-- Provenance of the steward run that produced this record.
|
|
174
|
+
steward_instance_key TEXT,
|
|
175
|
+
agent_job_key TEXT,
|
|
176
|
+
worker TEXT,
|
|
177
|
+
model TEXT,
|
|
178
|
+
created_at TEXT NOT NULL,
|
|
179
|
+
-- Operator review lifecycle: pending → dismissed | intervened.
|
|
180
|
+
review_state TEXT NOT NULL DEFAULT 'pending', -- pending | dismissed | intervened
|
|
181
|
+
reviewed_by TEXT,
|
|
182
|
+
reviewed_at TEXT,
|
|
183
|
+
review_note TEXT,
|
|
184
|
+
-- Claim/lease bookkeeping (not part of the review surface): which steward instance holds the open
|
|
185
|
+
-- claim and until when. NULL once the row is finalised (resolved/abstained recorded).
|
|
186
|
+
claimed_by TEXT,
|
|
187
|
+
claimed_until TEXT
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
CREATE INDEX idx_auto_resolutions_review_state ON escalation_auto_resolutions(review_state, created_at);
|
|
191
|
+
CREATE INDEX idx_auto_resolutions_subject ON escalation_auto_resolutions(subject_key);
|
|
192
|
+
-- The claim/lease fence: at most ONE open (unfinalised, `outcome='claimed'`) claim per user task, so
|
|
193
|
+
-- two overlapping steward ticks can never both act on the same escalation. Final rows
|
|
194
|
+
-- (`resolved`/`abstained`) are excluded so a re-escalation of the SAME task can be recorded afresh.
|
|
195
|
+
CREATE UNIQUE INDEX ux_auto_resolutions_open_claim ON escalation_auto_resolutions(user_task_key) WHERE outcome = 'claimed';
|
|
196
|
+
CREATE INDEX idx_auto_resolutions_task ON escalation_auto_resolutions(user_task_key);
|
|
197
|
+
|
|
198
|
+
-- The Auto-resolutions page read model (ADR 0042): projects each FINAL record (resolved/abstained —
|
|
199
|
+
-- never an in-flight `claimed` placeholder) with a derived `pending_review` flag (1 iff still
|
|
200
|
+
-- `pending`) for the row-action `showWhenField` gate, which reads JS truthiness. Derivation over
|
|
201
|
+
-- duplication: the grid binds this VIEW, never the base table.
|
|
202
|
+
CREATE VIEW escalation_auto_resolutions__read_model AS
|
|
203
|
+
SELECT
|
|
204
|
+
id,
|
|
205
|
+
user_task_key,
|
|
206
|
+
escalation_kind,
|
|
207
|
+
element_id,
|
|
208
|
+
subject_type,
|
|
209
|
+
subject_key,
|
|
210
|
+
subject_url,
|
|
211
|
+
process_instance_key,
|
|
212
|
+
question,
|
|
213
|
+
options,
|
|
214
|
+
outcome,
|
|
215
|
+
submitted_variables,
|
|
216
|
+
rationale,
|
|
217
|
+
evidence,
|
|
218
|
+
steward_instance_key,
|
|
219
|
+
agent_job_key,
|
|
220
|
+
worker,
|
|
221
|
+
model,
|
|
222
|
+
created_at,
|
|
223
|
+
review_state,
|
|
224
|
+
reviewed_by,
|
|
225
|
+
reviewed_at,
|
|
226
|
+
review_note,
|
|
227
|
+
CASE WHEN review_state = 'pending' THEN 1 ELSE 0 END AS pending_review
|
|
228
|
+
FROM escalation_auto_resolutions
|
|
229
|
+
WHERE outcome IN ('resolved', 'abstained');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- Re-create the `delivery_graph_read_model` VIEW so it re-exports the base column added by
|
|
2
|
+
-- 120_escalation_auto_resolutions.sql (`auto_resolve_escalations`). The read-model VIEW must pass
|
|
3
|
+
-- through EVERY base `delivery_graph_runs` column (the static pages↔schema contract guard +
|
|
4
|
+
-- app/deliveryGraphReadModel.test.ts DRIFT GUARD assert it), so adding a base column obliges a fresh
|
|
5
|
+
-- VIEW definition — 087/096 are immutable and cannot be edited in place.
|
|
6
|
+
--
|
|
7
|
+
-- Every DERIVED column below is emitted VERBATIM from the ONE declaration in
|
|
8
|
+
-- app/deliveryGraphReadModel.ts (`deliveryGraphReadModel.sqlSelectFor(col, { baseAlias: "dg" })`) —
|
|
9
|
+
-- the same closed-DSL AST that drives the runtime TS via `fnFor`, so the two lowerings cannot
|
|
10
|
+
-- diverge. This file is a mechanical re-emission of 096 with the one new base pass-through added;
|
|
11
|
+
-- the derived `stage`/`stage_state`/`list_bucket`/`ack_open`/`park_label` expressions are unchanged.
|
|
12
|
+
-- The drift guard now points at THIS migration (the latest VIEW definition).
|
|
13
|
+
--
|
|
14
|
+
-- Forward-only VIEW definition (DROP then CREATE), sourced off the managed
|
|
15
|
+
-- `delivery_graph_runs__tracking` re-export of the base table so a terminated run classifies on
|
|
16
|
+
-- ENGINE TRUTH. The runner wraps each file in its own transaction, so this file must NOT contain
|
|
17
|
+
-- BEGIN/COMMIT. Numbered after 120.
|
|
18
|
+
|
|
19
|
+
DROP VIEW IF EXISTS delivery_graph_read_model;
|
|
20
|
+
DROP VIEW IF EXISTS delivery_graph_pr_counts;
|
|
21
|
+
|
|
22
|
+
CREATE VIEW IF NOT EXISTS "delivery_graph_pr_counts" AS
|
|
23
|
+
SELECT
|
|
24
|
+
"__urban_rollup_src"."root_request_key" AS "root_request_key",
|
|
25
|
+
SUM(CASE WHEN COALESCE(((NOT COALESCE(("__urban_rollup_src"."root_request_key" IS NULL), 0)) AND (NOT COALESCE(COALESCE((COALESCE(("__urban_rollup_src"."derived_status" = 'converged'), 0) OR COALESCE(("__urban_rollup_src"."derived_status" = 'merged'), 0) OR COALESCE(("__urban_rollup_src"."derived_status" = 'abandoned'), 0)), 0), 0))), 0) THEN 1 ELSE 0 END) AS "prs_in_flight"
|
|
26
|
+
FROM "pull_requests__tracking" "__urban_rollup_src"
|
|
27
|
+
GROUP BY "__urban_rollup_src"."root_request_key";
|
|
28
|
+
|
|
29
|
+
CREATE VIEW delivery_graph_read_model AS
|
|
30
|
+
SELECT
|
|
31
|
+
dg.run_key AS run_key,
|
|
32
|
+
COALESCE(dg.derived_status, dg.status) AS status,
|
|
33
|
+
dg.process_key AS process_key,
|
|
34
|
+
dg.process_definition_id AS process_definition_id,
|
|
35
|
+
dg.digest AS digest,
|
|
36
|
+
dg.side_effecting AS side_effecting,
|
|
37
|
+
dg.node_count AS node_count,
|
|
38
|
+
dg.human_node_count AS human_node_count,
|
|
39
|
+
dg.side_effect_count AS side_effect_count,
|
|
40
|
+
dg.title AS title,
|
|
41
|
+
dg.phase AS phase,
|
|
42
|
+
dg.phase_node_id AS phase_node_id,
|
|
43
|
+
dg.human_labels AS human_labels,
|
|
44
|
+
dg.created_at AS created_at,
|
|
45
|
+
dg.updated_at AS updated_at,
|
|
46
|
+
dg.acknowledged_at AS acknowledged_at,
|
|
47
|
+
dg.auto_resolve_escalations AS auto_resolve_escalations,
|
|
48
|
+
CASE WHEN COALESCE(("dg"."derived_status" = 'done'), 0) THEN 'Done' WHEN COALESCE((COALESCE(("dg"."derived_status" = 'failed'), 0) OR COALESCE(("dg"."derived_status" = 'abandoned'), 0)), 0) THEN 'Done' WHEN COALESCE(("dg"."derived_status" = 'awaiting-approval'), 0) THEN 'Requested' WHEN COALESCE((COALESCE("pc"."prs_in_flight", 0) > 0), 0) THEN 'Converging' ELSE 'Implementing' END AS stage,
|
|
49
|
+
CASE WHEN COALESCE(("dg"."derived_status" = 'done'), 0) THEN 'ok' WHEN COALESCE((COALESCE(("dg"."derived_status" = 'failed'), 0) OR COALESCE(("dg"."derived_status" = 'abandoned'), 0)), 0) THEN 'failed' ELSE NULL END AS stage_state,
|
|
50
|
+
CASE WHEN COALESCE((COALESCE((COALESCE(("dg"."derived_status" = 'done'), 0) OR COALESCE(("dg"."derived_status" = 'failed'), 0) OR COALESCE(("dg"."derived_status" = 'abandoned'), 0)), 0) AND COALESCE(("dg"."acknowledged_at" = "dg"."acknowledged_at"), 0)), 0) THEN 'history' ELSE 'active' END AS list_bucket,
|
|
51
|
+
CASE WHEN COALESCE((COALESCE((COALESCE(("dg"."derived_status" = 'done'), 0) OR COALESCE(("dg"."derived_status" = 'failed'), 0) OR COALESCE(("dg"."derived_status" = 'abandoned'), 0)), 0) AND (NOT COALESCE(COALESCE(("dg"."acknowledged_at" = "dg"."acknowledged_at"), 0), 0))), 0) THEN 1 ELSE 0 END AS ack_open,
|
|
52
|
+
CASE WHEN dg.phase_node_id IS NOT NULL THEN dg.phase ELSE NULL END AS park_label
|
|
53
|
+
FROM delivery_graph_runs__tracking dg
|
|
54
|
+
LEFT JOIN delivery_graph_pr_counts pc ON dg.run_key = pc.root_request_key;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
-- Re-create the `pull_requests_read_model` VIEW so it re-exports the base column added by
|
|
2
|
+
-- 120_escalation_auto_resolutions.sql (`auto_resolve_escalations`). The read-model VIEW must pass
|
|
3
|
+
-- through EVERY base `pull_requests` column (the static pages↔schema contract guard +
|
|
4
|
+
-- app/pullRequestReadModel.test.ts DRIFT GUARD assert it), so adding a base column obliges a fresh
|
|
5
|
+
-- VIEW definition — 094/104/118 are immutable and cannot be edited in place.
|
|
6
|
+
--
|
|
7
|
+
-- Every DERIVED column below is emitted VERBATIM from the ONE declaration in
|
|
8
|
+
-- app/pullRequestReadModel.ts (`pullRequestReadModel.sqlSelectFor(col, { baseAlias: "pr" })`) — the
|
|
9
|
+
-- same closed-DSL AST that drives the runtime TS via `fnFor`, so the two lowerings cannot diverge.
|
|
10
|
+
-- This file is a mechanical re-emission of 118 with the one new base pass-through added; the
|
|
11
|
+
-- derived `list_bucket`/`ack_open` expressions are unchanged. The drift guard now points at THIS
|
|
12
|
+
-- migration (the latest VIEW definition).
|
|
13
|
+
--
|
|
14
|
+
-- Forward-only VIEW definition (DROP then CREATE), sourced off the managed `pull_requests__tracking`
|
|
15
|
+
-- re-export of the base table so a terminated PR classifies on ENGINE TRUTH. SQLite does not validate
|
|
16
|
+
-- a view body at CREATE time. The runner wraps each file in its own transaction, so this file must
|
|
17
|
+
-- NOT contain BEGIN/COMMIT. Numbered after 121.
|
|
18
|
+
|
|
19
|
+
DROP VIEW IF EXISTS pull_requests_read_model;
|
|
20
|
+
|
|
21
|
+
CREATE VIEW pull_requests_read_model AS
|
|
22
|
+
SELECT
|
|
23
|
+
pr.pr_key AS pr_key,
|
|
24
|
+
pr.repo AS repo,
|
|
25
|
+
pr.number AS number,
|
|
26
|
+
pr.url AS url,
|
|
27
|
+
pr.title AS title,
|
|
28
|
+
COALESCE(pr.derived_status, pr.status) AS status,
|
|
29
|
+
pr.current_round AS current_round,
|
|
30
|
+
pr.process_key AS process_key,
|
|
31
|
+
pr.waiting_since AS waiting_since,
|
|
32
|
+
pr.last_review_id AS last_review_id,
|
|
33
|
+
pr.outcome AS outcome,
|
|
34
|
+
pr.created_at AS created_at,
|
|
35
|
+
pr.updated_at AS updated_at,
|
|
36
|
+
pr.converged_at AS converged_at,
|
|
37
|
+
pr.merged_at AS merged_at,
|
|
38
|
+
pr.active_worker AS active_worker,
|
|
39
|
+
pr.lease_until AS lease_until,
|
|
40
|
+
pr.last_nudge_at AS last_nudge_at,
|
|
41
|
+
pr.fresh_head_run_head AS fresh_head_run_head,
|
|
42
|
+
pr.abandon_token AS abandon_token,
|
|
43
|
+
pr.incident_key AS incident_key,
|
|
44
|
+
pr.incident_message AS incident_message,
|
|
45
|
+
pr.last_round_head AS last_round_head,
|
|
46
|
+
pr.last_progress_job_key AS last_progress_job_key,
|
|
47
|
+
pr.last_progress_result AS last_progress_result,
|
|
48
|
+
pr.last_progress_agent_watermark AS last_progress_agent_watermark,
|
|
49
|
+
pr.churn_escalated_round AS churn_escalated_round,
|
|
50
|
+
pr.root_request_key AS root_request_key,
|
|
51
|
+
pr.epic_phase_label AS epic_phase_label,
|
|
52
|
+
pr.acknowledged_at AS acknowledged_at,
|
|
53
|
+
pr.auto_resolve_escalations AS auto_resolve_escalations,
|
|
54
|
+
CASE WHEN COALESCE((COALESCE((COALESCE(("pr"."derived_status" = 'merged'), 0) OR COALESCE(("pr"."derived_status" = 'converged'), 0) OR COALESCE(("pr"."derived_status" = 'abandoned'), 0) OR COALESCE(("pr"."derived_status" = 'closed'), 0) OR COALESCE(("pr"."derived_status" = 'failed'), 0)), 0) AND COALESCE(("pr"."acknowledged_at" = "pr"."acknowledged_at"), 0)), 0) THEN 'history' ELSE 'active' END AS list_bucket,
|
|
55
|
+
CASE WHEN COALESCE((COALESCE((COALESCE(("pr"."derived_status" = 'merged'), 0) OR COALESCE(("pr"."derived_status" = 'converged'), 0) OR COALESCE(("pr"."derived_status" = 'abandoned'), 0) OR COALESCE(("pr"."derived_status" = 'closed'), 0) OR COALESCE(("pr"."derived_status" = 'failed'), 0)), 0) AND (NOT COALESCE(COALESCE(("pr"."acknowledged_at" = "pr"."acknowledged_at"), 0), 0))), 0) THEN 1 ELSE 0 END AS ack_open
|
|
56
|
+
FROM pull_requests__tracking pr;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- Issue #889 review (Copilot "Previously missed"): the Auto-resolutions grid omitted the SELECTED
|
|
2
|
+
-- option, so an operator could not see whether the steward chose retry/continue/etc. without decoding
|
|
3
|
+
-- the raw submitted variables. Add a first-class `selected_option` column (the option value the steward
|
|
4
|
+
-- actually selected, derived host-side by the record worker from the decision's own form variables) and
|
|
5
|
+
-- re-create the read-model VIEW to expose it as a list column.
|
|
6
|
+
--
|
|
7
|
+
-- Expand-and-contract: the column is nullable (existing rows keep NULL — they predate the projection),
|
|
8
|
+
-- and the VIEW is re-created forward-only (DROP then CREATE) to add the new pass-through. Migration 120
|
|
9
|
+
-- is immutable and cannot be edited in place. Numbered after 122.
|
|
10
|
+
|
|
11
|
+
ALTER TABLE escalation_auto_resolutions ADD COLUMN selected_option TEXT;
|
|
12
|
+
|
|
13
|
+
DROP VIEW IF EXISTS escalation_auto_resolutions__read_model;
|
|
14
|
+
CREATE VIEW escalation_auto_resolutions__read_model AS
|
|
15
|
+
SELECT
|
|
16
|
+
id,
|
|
17
|
+
user_task_key,
|
|
18
|
+
escalation_kind,
|
|
19
|
+
element_id,
|
|
20
|
+
subject_type,
|
|
21
|
+
subject_key,
|
|
22
|
+
subject_url,
|
|
23
|
+
process_instance_key,
|
|
24
|
+
-- The cockpit "Agent" transcript link keys on `process_key` (the #833 invariant: every Agent →
|
|
25
|
+
-- cockpit link uses keyField `process_key`). The ANSWERING agent ran under `steward_instance_key`
|
|
26
|
+
-- (the steward process), NOT `process_instance_key` (the escalated SUBJECT's process), so project
|
|
27
|
+
-- the steward key under the canonical `process_key` name for that link (issue #889 review).
|
|
28
|
+
steward_instance_key AS process_key,
|
|
29
|
+
question,
|
|
30
|
+
options,
|
|
31
|
+
outcome,
|
|
32
|
+
submitted_variables,
|
|
33
|
+
selected_option,
|
|
34
|
+
rationale,
|
|
35
|
+
evidence,
|
|
36
|
+
steward_instance_key,
|
|
37
|
+
agent_job_key,
|
|
38
|
+
worker,
|
|
39
|
+
model,
|
|
40
|
+
created_at,
|
|
41
|
+
review_state,
|
|
42
|
+
reviewed_by,
|
|
43
|
+
reviewed_at,
|
|
44
|
+
review_note,
|
|
45
|
+
CASE WHEN review_state = 'pending' THEN 1 ELSE 0 END AS pending_review
|
|
46
|
+
FROM escalation_auto_resolutions
|
|
47
|
+
WHERE outcome IN ('resolved', 'abstained');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- 124_admitted_epics_auto_resolve.sql — issue #889: carry the escalation-steward opt-in through the
|
|
2
|
+
-- set/batch admission door (`startEpicSet`).
|
|
3
|
+
--
|
|
4
|
+
-- The single-issue door (`startPlanFanout`) already seeds `plans.auto_resolve_escalations` (added by
|
|
5
|
+
-- 120) from the request body's `autoResolveEscalations`. The set/batch door dropped it: `startEpicSet`
|
|
6
|
+
-- stages each admitted epic into `admitted_epics` and `lowerAdmittedSet` later reads that staging to
|
|
7
|
+
-- call `startPlan` — but the staging row had no column for the flag, so every batch-admitted epic (and
|
|
8
|
+
-- every child convergence/merge loop it spawns) stayed opted OUT even when the caller opted in. That
|
|
9
|
+
-- contradicts the issue #889 contract that the opt-in reach "every initiation pathway".
|
|
10
|
+
--
|
|
11
|
+
-- Fix the class at the staging seam: persist the per-epic opt-in on the staged row so the lowering
|
|
12
|
+
-- reads it back and forwards it to `startPlan`. Additive (nullable-free `NOT NULL DEFAULT 0`), so an
|
|
13
|
+
-- existing install's already-staged rows read as opted-out (the safe default) and fresh admissions
|
|
14
|
+
-- record the caller's choice. No read-model view selects from `admitted_epics`, so no VIEW re-emission
|
|
15
|
+
-- is needed (unlike `pull_requests` — cf. 121/122).
|
|
16
|
+
--
|
|
17
|
+
-- Numbered 124: above this branch's highest committed prefix (123) and origin/main's (119), so a
|
|
18
|
+
-- fan-out sibling forking at either point cannot collide. The runner wraps each file in its own
|
|
19
|
+
-- transaction, so this file must NOT contain BEGIN/COMMIT.
|
|
20
|
+
|
|
21
|
+
ALTER TABLE admitted_epics ADD COLUMN auto_resolve_escalations INTEGER NOT NULL DEFAULT 0;
|
package/docs/agent-guide.md
CHANGED
|
@@ -76,6 +76,7 @@ The body is flat. Fields:
|
|
|
76
76
|
| `autoMerge` | boolean | **Positive per-request setting.** `false` = review only, stopping at `converged`, regardless of the global default; `true` = converge **then merge** when `NANO_PR_AUTO_MERGE` is enabled. |
|
|
77
77
|
| `maxRounds` | integer | per-submit cap before escalating (clamped 1–100; default from `NANO_PR_MAX_ROUNDS`, 20). |
|
|
78
78
|
| `dependsOn` | string[] | other `prKey`s that must land before this one merges (merge-loop barrier). |
|
|
79
|
+
| `autoResolveEscalations` | boolean | **Opt in to the escalation steward** (issue #889). When `true`, this loop's open escalations become eligible for the steward to review and auto-resolve every 30 min (recorded reviewably in **Auto-resolutions**, §3). Default `false` — escalations stay human-only. |
|
|
79
80
|
|
|
80
81
|
**Converge-only vs. converge-and-merge — choose deliberately:**
|
|
81
82
|
|
|
@@ -116,6 +117,12 @@ blank/absent base is rejected with a 400. Starting a
|
|
|
116
117
|
plan is idempotent on the plan key; an already-running plan short-circuits. The
|
|
117
118
|
response (202) echoes the `planKey` and engine `processKey`.
|
|
118
119
|
|
|
120
|
+
Both epic (`startPlanFanout`) and feature (`startFeature`) accept the optional
|
|
121
|
+
**`autoResolveEscalations`** boolean (issue #889, default `false`). When `true`, the run opts in to
|
|
122
|
+
the escalation steward, and the opt-in **propagates to every child it spawns** — an epic's
|
|
123
|
+
implementation tasks and each enrolled convergence/merge loop, a feature run's handed-off PR. Only
|
|
124
|
+
escalations raised under opted-in work are eligible for the steward (§3).
|
|
125
|
+
|
|
119
126
|
**Single issue, one PR (feature run, `startFeature`)** — no planning or fan-out. Same body and
|
|
120
127
|
base-branch admission, plus `converge` (enrol the PR in §1) and `autoMerge`:
|
|
121
128
|
|
|
@@ -313,6 +320,37 @@ Guidance for the human you assist: read the escalation `question` or plan-review
|
|
|
313
320
|
`findings` first, decide the smallest unblocking answer, and answer it precisely —
|
|
314
321
|
the answer becomes the agent's next-round context.
|
|
315
322
|
|
|
323
|
+
### The escalation steward (opt-in auto-resolution, issue #889)
|
|
324
|
+
|
|
325
|
+
Escalations are **still for humans** — unless the work opted in. Each initiation
|
|
326
|
+
pathway (§1, §2, §9) accepts `autoResolveEscalations` (default off). When a piece of
|
|
327
|
+
work opts in, the **escalation steward** reviews its open escalations every 30 minutes
|
|
328
|
+
(configurable via `NANO_ESCALATION_STEWARD_INTERVAL`; `NANO_ESCALATION_STEWARD=off`
|
|
329
|
+
disables the loop entirely) and submits a recommended choice for each — keeping the
|
|
330
|
+
fleet moving instead of parking for hours on a decision with a clear best option.
|
|
331
|
+
|
|
332
|
+
The steward is deliberately conservative:
|
|
333
|
+
|
|
334
|
+
- It only touches escalations on **opted-in** work, past a short minimum age (so a
|
|
335
|
+
human already answering wins), and never a human-only / irreversible / credential
|
|
336
|
+
kind (merge approval, a permission prompt, a readiness gate, a blocked/conformance
|
|
337
|
+
ack, an *abandon*).
|
|
338
|
+
- It **abstains** — leaving the escalation for a human — when the right choice is
|
|
339
|
+
unclear, and a **loop guard** abstains once a subject has been auto-resolved twice
|
|
340
|
+
and escalates again (the automatic answer isn't working).
|
|
341
|
+
- Every resolution goes through the canonical `completeUserTask` door with **agent**
|
|
342
|
+
attribution (ADR 0046) — never recorded as a human decision.
|
|
343
|
+
|
|
344
|
+
**Reviewing auto-resolutions.** Every resolution *and* abstention is written to a
|
|
345
|
+
durable record and shown in the **Auto-resolutions** view (a pending-count badge sits
|
|
346
|
+
in the nav). The default list shows `pending` records, newest first; open one to see
|
|
347
|
+
the full question, the options offered, the submitted variables, the rationale and the
|
|
348
|
+
evidence checked, plus links to the subject and the steward's transcript. From there
|
|
349
|
+
you can **Dismiss** it (reviewed, no action) or **Intervene** (mark the record and
|
|
350
|
+
record guidance for stepping in after the fact — reopening the completed user task is
|
|
351
|
+
not required). Over MCP / curl, `listAutoResolutions` reads the records and
|
|
352
|
+
`reviewAutoResolution` (`{ id, action: "dismiss" | "intervene", note? }`) reviews one.
|
|
353
|
+
|
|
316
354
|
---
|
|
317
355
|
|
|
318
356
|
## 4. The lifecycle & statuses (so you can reason about state)
|
|
@@ -694,15 +732,13 @@ curl -sS -X POST __BASE__/actions/compile-delivery-graph \
|
|
|
694
732
|
```
|
|
695
733
|
|
|
696
734
|
- `200 { status:"ready", message, digest, preview, reviewUrl }` — the graph compiled and is
|
|
697
|
-
**staged for operator review**. `digest` is the content-address that NAMES the proposal (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
`
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
replay to start a run. Your role ends here — hand the operator the `digest` (or `reviewUrl`)
|
|
705
|
-
and ask them to preview and dispatch it.
|
|
735
|
+
**staged for operator review**. `digest` is the content-address that NAMES the proposal (tell the
|
|
736
|
+
operator exactly which one to dispatch); `preview` is `{ diagram, sideEffects, humanNodes }` —
|
|
737
|
+
`diagram` is a mermaid `flowchart` of the resolved graph, `humanNodes[]` are the stop-points where
|
|
738
|
+
it waits for a person, and `sideEffects[]` are the `agent`/`connector` actions it **will** perform
|
|
739
|
+
once dispatched; `reviewUrl` is a **navigational** cockpit deep-link (a pointer only — **not** a
|
|
740
|
+
dispatch handle). The response carries **no run key, token, or process-instance key**: nothing you
|
|
741
|
+
can replay to start a run. Hand the operator the `digest` (or `reviewUrl`) and ask them to dispatch.
|
|
706
742
|
- `400 { ok:false, errors:[{ path, message }] }` — every error path-qualified
|
|
707
743
|
(`nodes[2].kind`, `edges[1].from`, …) for unknown kind, dangling edge, a cycle, or an
|
|
708
744
|
unresolvable `from` fact. Fix and recompile; nothing is staged.
|
|
@@ -711,13 +747,16 @@ curl -sS -X POST __BASE__/actions/compile-delivery-graph \
|
|
|
711
747
|
`start` endpoint (Decision 5/7, issue #460). Dispatch is a human action: an operator opens
|
|
712
748
|
the **Delivery Graphs** page in the cockpit, reviews the staged proposal's rendered preview
|
|
713
749
|
(its diagram, the human stop-points, and the side effects a dispatch authorises), and clicks
|
|
714
|
-
**Dispatch** on the one they approve.
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
750
|
+
**Dispatch** on the one they approve. That click **is** the approval — content-addressed to the
|
|
751
|
+
exact digest previewed, so it cannot replay some other graph. Once dispatched, the graph deploys +
|
|
752
|
+
runs engine-natively and registers as a run aggregate, so its current phase / parked node shows in
|
|
753
|
+
the cockpit's **Active Delivery Graphs** grid (e.g. *"parked on human node: manual OTP publish"*).
|
|
754
|
+
A `human` node parks on the **Tasks** inbox and is answered exactly as an escalation is (§3) — its
|
|
755
|
+
completion emits any declared facts, which downstream edges bind.
|
|
756
|
+
|
|
757
|
+
The dispatch body accepts the optional **`autoResolveEscalations`** boolean (issue #889, default
|
|
758
|
+
`false`): when `true`, the run opts in to the escalation steward (§3) — its `human` nodes, service-node
|
|
759
|
+
escalation twins, and any `converge` connector's enrolled PR become eligible for auto-resolution.
|
|
721
760
|
|
|
722
761
|
> **Completing a `wait` escalation proceeds *as not-ready* — it does NOT re-arm the gate.**
|
|
723
762
|
> A `wait` node with `onTimeout: "escalate"` (the default) that elapses its bound parks a
|
|
@@ -892,7 +931,7 @@ Sometimes the thing you must wait for is not one PR but a **whole epic** — an
|
|
|
892
931
|
time. The **`wait` kind `epic`** (issue #568) gates on that epic reaching **"fully merged"**
|
|
893
932
|
(every opened slice landed), keyed by its durable **`planKey`** (`owner/repo#NN` — the epic
|
|
894
933
|
issue), so *"start feature B once epic A has fully landed"* is an automated edge, not a human
|
|
895
|
-
babysitting a
|
|
934
|
+
babysitting a gate.
|
|
896
935
|
|
|
897
936
|
```json
|
|
898
937
|
{
|
|
@@ -915,26 +954,24 @@ Semantics:
|
|
|
915
954
|
|
|
916
955
|
- **`target` is the `planKey`** (`owner/repo#NN`, the epic issue) — the *stable business id*,
|
|
917
956
|
not the engine `processInstanceKey` (`64200`), so a resubmit/replay still resolves.
|
|
918
|
-
- **`match.epicState`** is `merged` (default) or
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
gate; the **bounded** wait elapses and routes via **`onTimeout`** (`escalate`/`continue`) —
|
|
923
|
-
it does **not** hang.
|
|
957
|
+
- **`match.epicState`** is `merged` (default) or `done` — "every opened slice landed". The gate reads
|
|
958
|
+
the app's own **aggregate** (the lineage read-model over `NANO_WORKFORCE_BASE_URL`), so it is
|
|
959
|
+
level-triggered like `pr` (no missed edge). A failed/abandoned/mixed epic never reports merged, so
|
|
960
|
+
it never falsely releases the gate — the **bounded** wait routes via **`onTimeout`**, never hanging.
|
|
924
961
|
- **Set `poll.timeoutMs` to a realistic budget.** An epic reaching "fully merged" is a
|
|
925
|
-
multi-day, human-paced event, so the example
|
|
926
|
-
259200000 }`
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
962
|
+
multi-day, human-paced event, so the example budgets 3 days (`poll: { everyMs: 300000, timeoutMs:
|
|
963
|
+
259200000 }`). **Omitting `poll` inherits the 30-minute default** (§9.1) — the gate would escalate
|
|
964
|
+
long before the epic lands. Because this gate binds `prCount`, that escalation is **validated**
|
|
965
|
+
(§9.2): it **re-parks** unless the operator supplies a valid `prCount`, rather than silently
|
|
966
|
+
releasing `start-b` as not-ready.
|
|
930
967
|
- On a fully-merged match it binds **`prCount`** (how many slice PRs the epic landed) as an
|
|
931
968
|
output fact, so a downstream node can consume it (parity with the `pr` kind's `mergedSha`).
|
|
932
969
|
- **It also gates a single-PR *feature run*, not just a plan-fanout epic.** The gate resolves the
|
|
933
970
|
lineage thread whose **`rootRequestKey`** matches `target` **regardless of the thread's `kind`**
|
|
934
971
|
(`feature` | `epic` | `pr` | `delivery`), and `app/lineage.ts` lands a *feature* thread on
|
|
935
972
|
`stage:"merged"` once its PR merges. So `wait[epic]` targeting a feature/epic **root issue**
|
|
936
|
-
|
|
937
|
-
|
|
973
|
+
releases on `stage:"merged" && active:false` either way — see `getDeliveryGraphVocabulary` (the
|
|
974
|
+
`epic` probe entry) for the structured contract.
|
|
938
975
|
|
|
939
976
|
---
|
|
940
977
|
|
|
@@ -956,6 +993,8 @@ until it does, only the curl door in the third column exists on the deployment:
|
|
|
956
993
|
| Every PR in flight (§0/§5) | `listActivePrs` | `curl __BASE__/status` |
|
|
957
994
|
| List open escalations (§3) | `listEscalations` *(projected by sibling #666; curl door until then)* | `curl __BASE__/../../tasks/api/tasks` (the answerable list — carries each `userTaskKey`); `/status` filtered with the jq recipe just below this table is only a quick *indicator* (it surfaces `openEscalation` as a question string, **no `userTaskKey`**, so you can't answer from it) |
|
|
958
995
|
| Answer an escalation (§3) | `completeUserTask` (agent-assignee: `agentCompleteEscalation`) | `curl -X POST __BASE__/actions/complete-user-task` |
|
|
996
|
+
| List auto-resolutions (§3) | `listAutoResolutions` | `curl __BASE__/auto-resolutions` |
|
|
997
|
+
| Review an auto-resolution (dismiss / intervene) (§3) | `reviewAutoResolution` | `curl -X POST __BASE__/auto-resolutions/review` |
|
|
959
998
|
| Cancel an instance (record-consistent) (§7) | `cancelInstance` *(projected by sibling #667; curl door until then)* | `curl -X POST __BASE__/../actions/cancel` *(the `/app/actions/cancel` door, outside `__BASE__`)* |
|
|
960
999
|
| Publish a BPMN message (§7) | `postMessage` | `curl -X POST __BASE__/actions/message` |
|
|
961
1000
|
| The operator guide itself | `getAgentInstructions` (full) / `getAgentGuide(section?)` (addressable) | `curl __BASE__/agent` / `curl __BASE__/agent/guide` |
|
package/docs/agentic-cockpit.md
CHANGED
|
@@ -124,6 +124,40 @@ when a worker ages out past the liveness TTL. On (re)mount the presence family
|
|
|
124
124
|
reconciles the store against live connections, so a worker that vanished while
|
|
125
125
|
the app was down does not linger as a ghost row after a restart.
|
|
126
126
|
|
|
127
|
+
### Job ownership: authoritative reconnect resync
|
|
128
|
+
|
|
129
|
+
A worker's **jobs** are tracked by the explicit job-ownership claim registry
|
|
130
|
+
(`app/agentic/claim-registry.ts`): a supervisor sends a `claim` frame when a
|
|
131
|
+
worker picks up a job and a `release` when it finishes. Each frame names its
|
|
132
|
+
owning `instance` explicitly, so one per-host supervisor connection can carry
|
|
133
|
+
many workers' ownership.
|
|
134
|
+
|
|
135
|
+
On a WS reconnect a supervisor re-`claim`s every active jobKey. Those re-claims
|
|
136
|
+
are **additive** on their own — a claim that is not re-asserted is never dropped
|
|
137
|
+
— so a `release` lost during a hub outage would leave the job shown as *working*
|
|
138
|
+
until the worker exits entirely, forcing the supervisor to buffer every RELEASE
|
|
139
|
+
without limit across the outage.
|
|
140
|
+
|
|
141
|
+
To close that, a supervisor may end its resync with an **authoritative
|
|
142
|
+
snapshot**: a `claim` frame carrying the full set of jobKeys the instance still
|
|
143
|
+
owns (`{ instance, jobKeys: [...] }` — the empty **array** is allowed, meaning
|
|
144
|
+
"this instance now owns nothing", but every **element** must be a non-empty
|
|
145
|
+
string). The hub validates the frame as the snapshot-or-per-job
|
|
146
|
+
union (a malformed snapshot is dropped before it touches the registry — the
|
|
147
|
+
*presence* of `jobKeys`, not its validity, selects the snapshot variant, so a
|
|
148
|
+
present-but-malformed `jobKeys` — a non-array, a non-string element, or an
|
|
149
|
+
empty-string element — is rejected rather than laundered through the
|
|
150
|
+
per-job path; an empty-string element can't masquerade as an "owns nothing"
|
|
151
|
+
snapshot and clear real claims) and then
|
|
152
|
+
**replaces** that instance's claims with the snapshot — dropping any it did not
|
|
153
|
+
re-assert. The supervisor's
|
|
154
|
+
view wins, because ownership is advisory, short-lived visibility it can always
|
|
155
|
+
restate (it never gates a BPMN flow, ADR 0056). A jobKey that has moved to
|
|
156
|
+
another instance is never blanked by the old instance's resync, and a supervisor
|
|
157
|
+
that sends no snapshot keeps the old additive behaviour. This authoritative
|
|
158
|
+
contract lets a hub client (nano-supervisor#63) **bound** its RELEASE buffer
|
|
159
|
+
during an outage.
|
|
160
|
+
|
|
127
161
|
## What you will NOT find here (and where it lives)
|
|
128
162
|
|
|
129
163
|
- **Demand×supply matrix, missing-agent-type reds, diversity-SLO lights** →
|
package/docs/mcp-runbook.md
CHANGED
|
@@ -115,6 +115,8 @@ CI runs) fails if the block below drifts from the source. Never hand-edit it; ed
|
|
|
115
115
|
"listEscalations",
|
|
116
116
|
"getLineage",
|
|
117
117
|
"getPrHistory",
|
|
118
|
+
"listAutoResolutions",
|
|
119
|
+
"reviewAutoResolution",
|
|
118
120
|
"urban_debug_search_process_instances",
|
|
119
121
|
"urban_debug_search_element_instance_wait_states",
|
|
120
122
|
"urban_debug_search_incidents",
|