@nanobpm/nano-workforce 0.200.7 → 0.200.9

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 (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/SPEC.md +41 -0
  3. package/app/agentSla.test.ts +48 -0
  4. package/app/agentSla.ts +36 -11
  5. package/app/agentic/vocab/agent-sla-boundary.test.ts +170 -0
  6. package/app/agentic/vocab/job-types.ts +60 -0
  7. package/app/conformance.test.ts +114 -0
  8. package/app/conformance.ts +72 -0
  9. package/app/contracts.ts +2 -2
  10. package/app/deliveryRunner.ts +5 -0
  11. package/app/feature.ts +5 -0
  12. package/app/implementEscalationReason.test.ts +216 -0
  13. package/app/implementEscalationReason.ts +213 -0
  14. package/app/implementReconcile.test.ts +168 -11
  15. package/app/implementReconcile.ts +112 -18
  16. package/app/plan.ts +12 -0
  17. package/app/retro.test.ts +5 -0
  18. package/app/retro.ts +4 -0
  19. package/app/service.ts +27 -8
  20. package/e2e/agent-sla-boundary.e2e.ts +598 -0
  21. package/e2e/feature-run.e2e.ts +8 -1
  22. package/e2e/plan-fanout.e2e.ts +75 -2
  23. package/e2e/support/pin-agent-sla.ts +47 -0
  24. package/package.json +1 -1
  25. package/resources/processes/feature.bpmn +1 -0
  26. package/resources/processes/implement-cell.bpmn +122 -47
  27. package/resources/processes/merge-cell.bpmn +60 -12
  28. package/resources/processes/plan-fanout.bpmn +237 -211
  29. package/resources/processes/retro.bpmn +100 -3
  30. package/resources/prompts/feature.md +16 -1
  31. package/test/derivation-parity/derivation-parity.test.ts +35 -11
  32. package/test/derivation-parity/flows.ts +14 -1
  33. package/workers/conformance-record/worker.test.ts +87 -1
  34. package/workers/conformance-record/worker.ts +34 -7
  35. package/workers/reconcile-implement/worker.test.ts +31 -3
  36. package/workers/reconcile-implement/worker.ts +5 -0
  37. package/workers/record-feature-escalation/worker.test.ts +58 -3
  38. package/workers/record-feature-escalation/worker.ts +24 -10
@@ -25,6 +25,7 @@
25
25
  import type { AppJobHandler } from "@nanobpm/urban";
26
26
  import { classifyEscalation } from "../../app/escalationTaxonomy.ts";
27
27
  import { featureRuns, recordFeatureEscalation } from "../../app/feature.ts";
28
+ import { implementEscalationQuestion, NO_RESULT_QUESTION } from "../../app/implementEscalationReason.ts";
28
29
  import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
29
30
 
30
31
  // Input typed off the model data envelope (`RecordEscalationIn` in implement-cell.bpmn) — ADR 0040.
@@ -36,12 +37,6 @@ interface Out extends Record<string, unknown> {
36
37
  const str = (v: unknown): string | undefined =>
37
38
  typeof v === "string" && v.trim().length > 0 ? v.trim() : undefined;
38
39
 
39
- // The answerable prompt synthesised when the agent left no usable question — the implement-stage
40
- // analogue of record-trial-merge's synthesised trial-merge question. It names the recoverable work (a
41
- // PR may exist on the slice's branch) and the two answers the cell's `ic_gw_answer` gateway routes on.
42
- const NO_RESULT_QUESTION =
43
- 'The implementation agent finished without a machine-readable result (no status was reported), so we cannot tell whether the slice succeeded. It may still have opened a PR (check for a branch targeting the epic base). Choose "Answer" and give guidance to re-run the slice — or choose "Abandon" to skip it and continue.';
44
-
45
40
  const handler: AppJobHandler<In, Out> = async (job, app) => {
46
41
  const subjectKey = job.variables.subjectKey;
47
42
  // Fail fast rather than keying the `feature_escalations` audit log (and the guarded `feature_runs`
@@ -61,11 +56,30 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
61
56
  // so the parked task is never a dead end. Route through the single canonical taxonomy so this net can
62
57
  // never drift from the tier logic every other raise site uses. A "task"-kind escalation is
63
58
  // `decision-required` only when the agent left an answerable question, so this already covers the
64
- // blank-question case; the extra `&& rawQuestion` is the type narrowing that lets us hand the string
65
- // through without an assertion.
59
+ // blank-question case.
66
60
  const agentEscalated = classifyEscalation({ kind: "task", status: job.variables.status, question: rawQuestion }) ===
67
61
  "decision-required";
68
- const question = agentEscalated && rawQuestion ? rawQuestion : NO_RESULT_QUESTION;
62
+ // Build the escalation question through the single canonical builder (issue #865), ALWAYS — including
63
+ // when the agent raised its own answerable question. Passing the agent's `question` lets the builder
64
+ // lead with it AND fold in the agent's `summary` + transcript as supporting context (#865 review —
65
+ // otherwise an `escalated` result's summary/transcript were dropped and only the bare question
66
+ // survived). With no answerable question it synthesises an accurate reason — distinguishing a true
67
+ // no-result (blank status) from a claimed-completion-without-delivery (an affirmative-completion alias
68
+ // such as `completed`) and from a reported non-completion status — never the false "no status was
69
+ // reported" diagnosis for a reported `escalated`/`failed` (#865 review). The reason can never drift
70
+ // from the reconcile step because both read the same canonical builder.
71
+ const question = implementEscalationQuestion({
72
+ status: job.variables.status,
73
+ question: rawQuestion,
74
+ summary: job.variables.summary,
75
+ transcriptUrl: job.variables.transcriptUrl,
76
+ // Delivery EVIDENCE (#865 review): the preserved PR (an `escalated`/`failed` with a blank question
77
+ // retains it — never claim "opened no pull request") and whether the reconcile step's GitHub lookup
78
+ // actually CONFIRMED the delivery state (only then may the reason say "none was found"). Both are
79
+ // process variables the upstream `reconcile-implement` step emits into scope.
80
+ pr: job.variables.pr,
81
+ deliveryVerified: job.variables.deliveryVerified,
82
+ });
69
83
 
70
84
  // Append to the canonical `feature_escalations` audit log (the surviving table `pollUserTasks` reads),
71
85
  // keyed by `subjectKey` — the feature run's `feature_key`, or the epic's `plan_key` for a wave slice.
@@ -79,7 +93,7 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
79
93
  await runs.update(subjectKey, { status: "escalated", updated_at: new Date().toISOString() });
80
94
  }
81
95
 
82
- app.log.info("record-escalation", { subjectKey, synthesised: question === NO_RESULT_QUESTION });
96
+ app.log.info("record-escalation", { subjectKey, synthesised: !(agentEscalated && rawQuestion) });
83
97
 
84
98
  // Re-emit the resolved question so the `feature-escalation` form (and the answer loop) see it.
85
99
  return { question };