@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.
Files changed (82) hide show
  1. package/.github/workflows/ci.yml +7 -0
  2. package/AGENTS.md +83 -1
  3. package/CHANGELOG.md +7 -0
  4. package/README.md +1 -1
  5. package/SPEC.md +21 -22
  6. package/app/agentCompletion.test.ts +337 -0
  7. package/app/agentCompletion.ts +219 -0
  8. package/app/answer-escalation.test.ts +106 -0
  9. package/app/answerEscalation.test.ts +67 -0
  10. package/app/baseGuard.test.ts +9 -1
  11. package/app/baseGuard.ts +11 -0
  12. package/app/escalationSla.test.ts +39 -0
  13. package/app/escalationSla.ts +28 -0
  14. package/app/escalationTaxonomy.test.ts +115 -0
  15. package/app/escalationTaxonomy.ts +115 -0
  16. package/app/feature.test.ts +161 -0
  17. package/app/feature.ts +173 -0
  18. package/app/mergeProtocol.test.ts +15 -0
  19. package/app/mergeProtocol.ts +10 -0
  20. package/app/persist-escalation.test.ts +34 -36
  21. package/app/plan.test.ts +0 -294
  22. package/app/plan.ts +26 -216
  23. package/app/reviewWait.ts +12 -4
  24. package/app/roundResultDefault.test.ts +111 -2
  25. package/app/roundResultDefault.ts +35 -0
  26. package/app/service.test.ts +6 -7
  27. package/app/service.ts +52 -35
  28. package/db/migrations/026_agent_completion.sql +36 -0
  29. package/db/migrations/027_retire_escalation_subsystem.sql +43 -0
  30. package/db/migrations/028_feature_runs.sql +28 -0
  31. package/e2e/agent-answerable.e2e.ts +185 -0
  32. package/e2e/convergence-escalation.e2e.ts +180 -0
  33. package/e2e/convergence-loop.e2e.ts +1 -1
  34. package/e2e/feature-run.e2e.ts +231 -0
  35. package/e2e/plan-fanout-sla.e2e.ts +238 -0
  36. package/e2e/plan-fanout.e2e.ts +303 -0
  37. package/e2e/retire-escalation-subsystem.e2e.ts +223 -0
  38. package/e2e/support/github-admit.ts +99 -0
  39. package/e2e/user-task-spine.e2e.ts +155 -0
  40. package/nano.app.json +37 -11
  41. package/openapi.yaml +181 -73
  42. package/operations/agentCompleteEscalation.ts +53 -0
  43. package/operations/listActivePrs.test.ts +39 -6
  44. package/operations/postMessage.ts +10 -41
  45. package/operations/revertEscalationCompletion.ts +44 -0
  46. package/operations/startAndMessage.test.ts +0 -58
  47. package/operations/startFeature.ts +127 -0
  48. package/package.json +4 -1
  49. package/pages/cockpit.page.json +1 -0
  50. package/pages/epic-detail.page.json +11 -37
  51. package/pages/epic.page.json +1 -1
  52. package/pages/feature.page.json +82 -0
  53. package/pages/home.page.json +6 -18
  54. package/resources/agent-guide.md +52 -24
  55. package/resources/forms/feature-escalation.form +27 -0
  56. package/resources/forms/plan-review-decision.form +27 -0
  57. package/resources/forms/pr-escalation.form +23 -0
  58. package/resources/forms/spine-demo.form +15 -0
  59. package/resources/forms/trial-merge-decision.form +25 -0
  60. package/resources/processes/convergence-loop.bpmn +127 -75
  61. package/resources/processes/feature.bpmn +240 -0
  62. package/resources/processes/plan-fanout.bpmn +304 -223
  63. package/resources/processes/spine-demo.bpmn +72 -0
  64. package/scripts/check-migrations.ts +68 -0
  65. package/workers/answer-escalation/worker.ts +78 -0
  66. package/workers/converge-feature/worker.ts +51 -0
  67. package/workers/finalize/worker.ts +0 -2
  68. package/workers/mark-merged/worker.ts +0 -2
  69. package/workers/merge/worker.ts +6 -5
  70. package/workers/persist-escalation/worker.ts +28 -32
  71. package/workers/record-feature/worker.ts +61 -0
  72. package/workers/record-plan-review/worker.test.ts +9 -10
  73. package/workers/record-plan-review/worker.ts +15 -5
  74. package/workers/resolve-trial-attention/worker.test.ts +77 -0
  75. package/workers/resolve-trial-attention/worker.ts +43 -0
  76. package/operations/answerFeatureEscalation.test.ts +0 -112
  77. package/operations/answerFeatureEscalation.ts +0 -58
  78. package/operations/answerPlanEscalation.test.ts +0 -115
  79. package/operations/answerPlanEscalation.ts +0 -41
  80. package/workers/persist-plan-escalation/worker.test.ts +0 -80
  81. package/workers/persist-plan-escalation/worker.ts +0 -73
  82. 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;