@nanobpm/nano-workforce 0.57.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +7 -0
- package/AGENTS.md +83 -1
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/SPEC.md +21 -22
- package/app/agentCompletion.test.ts +337 -0
- package/app/agentCompletion.ts +219 -0
- package/app/answer-escalation.test.ts +106 -0
- package/app/answerEscalation.test.ts +67 -0
- package/app/baseGuard.test.ts +9 -1
- package/app/baseGuard.ts +11 -0
- package/app/escalationSla.test.ts +39 -0
- package/app/escalationSla.ts +28 -0
- package/app/escalationTaxonomy.test.ts +115 -0
- package/app/escalationTaxonomy.ts +115 -0
- package/app/feature.test.ts +161 -0
- package/app/feature.ts +173 -0
- package/app/mergeProtocol.test.ts +15 -0
- package/app/mergeProtocol.ts +10 -0
- package/app/persist-escalation.test.ts +34 -36
- package/app/plan.test.ts +0 -294
- package/app/plan.ts +26 -216
- package/app/reviewWait.ts +12 -4
- package/app/roundResultDefault.test.ts +111 -2
- package/app/roundResultDefault.ts +35 -0
- package/app/service.test.ts +6 -7
- package/app/service.ts +52 -35
- package/db/migrations/026_agent_completion.sql +36 -0
- package/db/migrations/027_retire_escalation_subsystem.sql +43 -0
- package/db/migrations/028_feature_runs.sql +28 -0
- package/e2e/agent-answerable.e2e.ts +185 -0
- package/e2e/convergence-escalation.e2e.ts +180 -0
- package/e2e/convergence-loop.e2e.ts +1 -1
- package/e2e/feature-run.e2e.ts +231 -0
- package/e2e/plan-fanout-sla.e2e.ts +238 -0
- package/e2e/plan-fanout.e2e.ts +303 -0
- package/e2e/retire-escalation-subsystem.e2e.ts +223 -0
- package/e2e/support/github-admit.ts +99 -0
- package/e2e/user-task-spine.e2e.ts +155 -0
- package/nano.app.json +37 -11
- package/openapi.yaml +181 -73
- package/operations/agentCompleteEscalation.ts +53 -0
- package/operations/listActivePrs.test.ts +39 -6
- package/operations/postMessage.ts +10 -41
- package/operations/revertEscalationCompletion.ts +44 -0
- package/operations/startAndMessage.test.ts +0 -58
- package/operations/startFeature.ts +127 -0
- package/package.json +4 -1
- package/pages/cockpit.page.json +1 -0
- package/pages/epic-detail.page.json +11 -37
- package/pages/epic.page.json +1 -1
- package/pages/feature.page.json +82 -0
- package/pages/home.page.json +6 -18
- package/resources/agent-guide.md +52 -24
- package/resources/forms/feature-escalation.form +27 -0
- package/resources/forms/plan-review-decision.form +27 -0
- package/resources/forms/pr-escalation.form +23 -0
- package/resources/forms/spine-demo.form +15 -0
- package/resources/forms/trial-merge-decision.form +25 -0
- package/resources/processes/convergence-loop.bpmn +127 -75
- package/resources/processes/feature.bpmn +240 -0
- package/resources/processes/plan-fanout.bpmn +304 -223
- package/resources/processes/spine-demo.bpmn +72 -0
- package/scripts/check-migrations.ts +68 -0
- package/workers/answer-escalation/worker.ts +78 -0
- package/workers/converge-feature/worker.ts +51 -0
- package/workers/finalize/worker.ts +0 -2
- package/workers/mark-merged/worker.ts +0 -2
- package/workers/merge/worker.ts +6 -5
- package/workers/persist-escalation/worker.ts +28 -32
- package/workers/record-feature/worker.ts +61 -0
- package/workers/record-plan-review/worker.test.ts +9 -10
- package/workers/record-plan-review/worker.ts +15 -5
- package/workers/resolve-trial-attention/worker.test.ts +77 -0
- package/workers/resolve-trial-attention/worker.ts +43 -0
- package/operations/answerFeatureEscalation.test.ts +0 -112
- package/operations/answerFeatureEscalation.ts +0 -58
- package/operations/answerPlanEscalation.test.ts +0 -115
- package/operations/answerPlanEscalation.ts +0 -41
- package/workers/persist-plan-escalation/worker.test.ts +0 -80
- package/workers/persist-plan-escalation/worker.ts +0 -73
- package/workers/persist-task-escalation/worker.ts +0 -120
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// pr.persist-task-escalation — an implementation agent escalated a task during the
|
|
2
|
-
// fan-out (issue #25). It reported `status = "escalated"` with a `question` and,
|
|
3
|
-
// ideally, a work-preserving DRAFT PR, then completed its job. This worker records
|
|
4
|
-
// that escalation and parks the plan on a per-task human answer:
|
|
5
|
-
// • opens (or refreshes) a `plan_escalations` row (status = open),
|
|
6
|
-
// • marks the `plan_tasks` row `escalated` with the question / draft PR / corr key,
|
|
7
|
-
// • re-points the plan's denormalised "open task escalation" fields at the
|
|
8
|
-
// oldest still-open escalation, so the page's single answer form surfaces it.
|
|
9
|
-
//
|
|
10
|
-
// The process then parks the child at the `feature-escalation-answered` message
|
|
11
|
-
// catch (correlationKey `<plan_key>:<task_id>`). Answering it (page form or
|
|
12
|
-
// `/app/api/hooks/feature-answer`) resumes the child, which re-dispatches the SAME task.
|
|
13
|
-
//
|
|
14
|
-
// Retry-safe: if an open escalation already exists for this corr key (a worker
|
|
15
|
-
// re-activation before the wait subscription opened), it is UPDATED, not
|
|
16
|
-
// duplicated — a fresh escalation row is only created after the previous one was
|
|
17
|
-
// answered.
|
|
18
|
-
import type { AppJobHandler } from "@nanobpm/urban";
|
|
19
|
-
import {
|
|
20
|
-
featureCorrKey,
|
|
21
|
-
planEscalations,
|
|
22
|
-
planTasks,
|
|
23
|
-
refreshOpenTaskEscalation,
|
|
24
|
-
} from "../../app/plan.ts";
|
|
25
|
-
|
|
26
|
-
interface TaskIn {
|
|
27
|
-
id?: unknown;
|
|
28
|
-
title?: unknown;
|
|
29
|
-
}
|
|
30
|
-
interface In extends Record<string, unknown> {
|
|
31
|
-
planKey: string;
|
|
32
|
-
task?: TaskIn;
|
|
33
|
-
question?: unknown;
|
|
34
|
-
pr?: unknown;
|
|
35
|
-
summary?: unknown;
|
|
36
|
-
}
|
|
37
|
-
interface Out extends Record<string, unknown> {
|
|
38
|
-
escalationId: number;
|
|
39
|
-
escalationCorrKey: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// A non-blank trimmed string, else undefined. A blank question/PR must not reach
|
|
43
|
-
// the UI answer form or masquerade as preserved work.
|
|
44
|
-
const str = (v: unknown): string | undefined =>
|
|
45
|
-
typeof v === "string" && v.trim() !== "" ? v.trim() : undefined;
|
|
46
|
-
|
|
47
|
-
const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
48
|
-
const planKey = job.variables.planKey;
|
|
49
|
-
const taskId = str(job.variables.task?.id);
|
|
50
|
-
if (!taskId) {
|
|
51
|
-
// No task binding means we cannot correlate a resume — fail loudly rather
|
|
52
|
-
// than silently parking a token that can never be answered.
|
|
53
|
-
throw new Error("persist-task-escalation: missing task.id in child scope");
|
|
54
|
-
}
|
|
55
|
-
const corrKey = featureCorrKey(planKey, taskId);
|
|
56
|
-
const question = str(job.variables.question);
|
|
57
|
-
if (!question) {
|
|
58
|
-
// A blank question would surface a non-actionable placeholder in the answer
|
|
59
|
-
// form and, on a retry, overwrite a previously recorded question. The output
|
|
60
|
-
// contract requires `question` for an escalation — fail loudly, like the
|
|
61
|
-
// missing-task.id guard above, rather than park an unanswerable escalation.
|
|
62
|
-
throw new Error("persist-task-escalation: missing question for escalated task");
|
|
63
|
-
}
|
|
64
|
-
const draftPr = str(job.variables.pr) ?? null;
|
|
65
|
-
// Prior-attempt context the escalating agent reported. Persist it so the UI
|
|
66
|
-
// and any later resume/debugging keep the task's summary instead of NULL.
|
|
67
|
-
const summary = str(job.variables.summary) ?? null;
|
|
68
|
-
const ts = new Date().toISOString();
|
|
69
|
-
|
|
70
|
-
const escTable = planEscalations(app.data);
|
|
71
|
-
const existing = (await escTable.find({ corr_key: corrKey, status: "open" }))
|
|
72
|
-
.sort((a, b) => b.id - a.id)[0];
|
|
73
|
-
let escalationId: number;
|
|
74
|
-
if (existing) {
|
|
75
|
-
await escTable.update(existing.id, {
|
|
76
|
-
question,
|
|
77
|
-
draft_pr_key: draftPr ?? existing.draft_pr_key,
|
|
78
|
-
});
|
|
79
|
-
escalationId = existing.id;
|
|
80
|
-
} else {
|
|
81
|
-
escalationId = Number(
|
|
82
|
-
await escTable.insert({
|
|
83
|
-
plan_key: planKey,
|
|
84
|
-
task_id: taskId,
|
|
85
|
-
corr_key: corrKey,
|
|
86
|
-
question,
|
|
87
|
-
draft_pr_key: draftPr,
|
|
88
|
-
status: "open",
|
|
89
|
-
asked_at: ts,
|
|
90
|
-
}),
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
for (const t of await planTasks(app.data).find({ plan_key: planKey, task_id: taskId })) {
|
|
95
|
-
await planTasks(app.data).update(t.id, {
|
|
96
|
-
status: "escalated",
|
|
97
|
-
open_question: question,
|
|
98
|
-
// Clear any answer from a prior (already-answered) escalation so the task
|
|
99
|
-
// row stays aligned with the currently-open question — otherwise a
|
|
100
|
-
// re-escalated task would show a stale answer next to the new question.
|
|
101
|
-
answer: null,
|
|
102
|
-
draft_pr_key: draftPr ?? t.draft_pr_key,
|
|
103
|
-
summary: summary ?? t.summary,
|
|
104
|
-
corr_key: corrKey,
|
|
105
|
-
updated_at: ts,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
await refreshOpenTaskEscalation(app.data, planKey);
|
|
110
|
-
// Emit the correlation key as a single scalar so the downstream
|
|
111
|
-
// `feature-escalation-answered` catch subscribes on `=escalationCorrKey`
|
|
112
|
-
// (freshly set, in scope) rather than re-deriving `=planKey + ":" + task.id`.
|
|
113
|
-
// The concatenation form errored to an empty, unmatchable key whenever `task`
|
|
114
|
-
// was not a Map at subscription-open, parking the token forever (see the
|
|
115
|
-
// engine incident fix). The value is identical to `featureCorrKey(planKey,
|
|
116
|
-
// taskId)`, so the app's answer-publish path still matches.
|
|
117
|
-
return { escalationId, escalationCorrKey: corrKey };
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
export default handler;
|