@nanobpm/nano-workforce 0.200.6 → 0.200.8

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/app/contracts.ts CHANGED
@@ -296,9 +296,9 @@ export const ENV_CONTRACTS = {
296
296
  NANO_PR_AGENT_SLA_TIMEOUT: {
297
297
  category: "env",
298
298
  name: "NANO_PR_AGENT_SLA_TIMEOUT",
299
- owner: "app/service.ts",
299
+ owner: "app/agentSla.ts",
300
300
  semantics:
301
- "SLA timeout for an agent (service) task before its boundary timer fires and the PR escalates for human attention (ISO-8601 duration). A malformed value falls back to the default.",
301
+ "SLA timeout for an external agent (service) task before its interrupting boundary timer fires and the process escalates for human attention (ISO-8601 duration). Seeded as the `agentSlaTimeout` process variable at every process start that hosts an external agent task (issue #849): convergence-loop (service.ts `submitPr`), merge-loop (service.ts `startMerge`), feature (feature.ts), plan-fanout (plan.ts), retro (retro.ts), and delivery-graph runs (deliveryRunner.ts). A malformed value falls back to the default.",
302
302
  default: "PT2H",
303
303
  },
304
304
  NANO_CAPS_WAIT_TIMEOUT: {
@@ -18,6 +18,7 @@ import { createHash, randomUUID } from "node:crypto";
18
18
  import type { EngineClient } from "@nanobpm/urban";
19
19
  import type { DeliveryFact, DeliveryGraph, DeliveryNode } from "../nano-generated/api-io.d.ts";
20
20
  import { TRANSCRIPT_URL_BASE_VAR, transcriptUrlBaseFor } from "./agentic/transcript-url.ts";
21
+ import { AGENT_SLA_TIMEOUT } from "./agentSla.ts";
21
22
  import { isPlausibleBranchName } from "./baseBranch.ts";
22
23
  import { AGENT_REPO_SPEC_HEADER, AGENT_TERMINAL_SUCCESS_STATUSES, assertNever, compileDeliveryGraph, DELIVERY_GRAPH_PROCESS_ID, redactFreeText } from "./deliveryGraphCompiler.ts";
23
24
  import { DEFAULT_EVERY_MS, msToIsoDuration, parseProbe, readinessPollEvery, readinessTimeout } from "./readiness.ts";
@@ -239,6 +240,10 @@ export async function runDeliveryGraph(
239
240
  // node ioMapping in deliveryGraphCompiler). Seeded once at the run root — the same value for
240
241
  // every node — and read down into each agent job via `=transcriptUrlBase`.
241
242
  [TRANSCRIPT_URL_BASE_VAR]: transcriptUrlBaseFor(),
243
+ // Agent-task liveness SLA (issue #849): the compiled graph's per-node `=nodeTimeout` boundary
244
+ // bounds a node's whole retry budget; this seeds the shared `agentSlaTimeout` knob so any
245
+ // agent cell modelled with the fleet-wide SLA boundary resolves it too.
246
+ agentSlaTimeout: AGENT_SLA_TIMEOUT,
242
247
  },
243
248
  });
244
249
  // The engine can yield a numeric key; `DeliveryRunHandle.processInstanceKey` is typed `string` and
package/app/feature.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  // SQL — matching app/plan.ts and app/service.ts.
17
17
  import type { DataLayer, EngineClient } from "@nanobpm/urban";
18
18
  import { TRANSCRIPT_URL_BASE_VAR, transcriptUrlBaseFor } from "./agentic/transcript-url.ts";
19
+ import { AGENT_SLA_TIMEOUT } from "./agentSla.ts";
19
20
  import { coalesceTitle, fetchIssueTitle } from "./github.ts";
20
21
  import { derivedTrackingTable } from "./instanceTracking.ts";
21
22
  import { ESCALATION_SLA_TIMEOUT, normalizeBaseBranch, type ParsedIssue, renderBaseBranchBrief } from "./plan.ts";
@@ -531,6 +532,10 @@ export async function startFeature(
531
532
  // `feature-escalation` user task's interrupting timer boundary and assignment definition.
532
533
  escalationSlaTimeout: ESCALATION_SLA_TIMEOUT,
533
534
  escalationAssignee: null,
535
+ // Agent-task liveness SLA (issue #849): bounds the implement-cell's external agent task
536
+ // (`implement-task`) — its interrupting timer boundary evaluates `=agentSlaTimeout` at timer
537
+ // creation, so a hung or looping implementation agent escalates instead of parking forever.
538
+ agentSlaTimeout: AGENT_SLA_TIMEOUT,
534
539
  // Base branch (ADR 0003): the branch the agent branches off and opens its PR against. The
535
540
  // brief rides `appendPrompt` in the implement task, exactly like the epic implementer.
536
541
  baseBranch: base,
package/app/plan.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  // the process. Data access goes through the record gateway (`data.table`), never
11
11
  // hand-written SQL — matching app/service.ts.
12
12
  import type { DataLayer, EngineClient } from "@nanobpm/urban";
13
+ import { AGENT_SLA_TIMEOUT } from "./agentSla.ts";
13
14
  import {
14
15
  InvalidBaseBranchError,
15
16
  isPlausibleBranchName,
@@ -1021,6 +1022,17 @@ export async function startPlan(
1021
1022
  // `operators` candidate group); an operator/agent can claim/reassign via the task inbox.
1022
1023
  escalationSlaTimeout: ESCALATION_SLA_TIMEOUT,
1023
1024
  escalationAssignee: null,
1025
+ // Agent-task liveness SLA (issue #849): PRE-SEED the canonical bound for the fan-out's
1026
+ // external agent tasks. Each slice's implementation agent (via the implement-cell
1027
+ // callActivity's explicit `agentSlaTimeout` input mapping) is bounded today; the wave's own
1028
+ // inline `trial-merge` agent (`plan-fanout.bpmn:524-542`) is NOT — it is a separate external
1029
+ // task from the standalone `merge-cell` process (which has no callActivity caller), sits on a
1030
+ // rerun back-edge, and its SLA boundary is deferred to #868 alongside the planner (`plan`) and
1031
+ // plan reviewer (`review-plan`), whose back-edge loop the layouter cannot yet route (layouter
1032
+ // bug #867). For those three this seed is preparation, not yet an armed bound. Each bounded
1033
+ // task's interrupting timer boundary evaluates `=agentSlaTimeout` at timer creation, so a hung
1034
+ // or looping agent escalates to a human instead of parking forever.
1035
+ agentSlaTimeout: AGENT_SLA_TIMEOUT,
1024
1036
  // Capability-barrier bound (#289): the validated ISO-8601 duration read by the
1025
1037
  // `wait-caps-timeout` timer arm of the `wait-caps-resolved` event-based gateway. A task whose
1026
1038
  // declared cross-repo capabilities never resolve (most acutely an unresolvable capabilityRef the
package/app/retro.test.ts CHANGED
@@ -4,6 +4,7 @@ import { assert, assertEquals, assertStringIncludes } from "#test-assert";
4
4
  import type { DataLayer, EngineClient } from "@nanobpm/urban";
5
5
  import { memBlackboardSource } from "../test/blackboardDb.ts";
6
6
  import { withTrackingViews } from "../test/trackingViews.ts";
7
+ import { AGENT_SLA_TIMEOUT } from "./agentSla.ts";
7
8
  import { appendEntry } from "./blackboard.ts";
8
9
  import { recordTaskDelta } from "./taskDelta.ts";
9
10
  import {
@@ -308,6 +309,10 @@ test("maybeStartRetro: starts the retro exactly once when the last PR lands with
308
309
  assertEquals(started.length, 1);
309
310
  assertEquals(started[0].processDefinitionId, "retro");
310
311
  assertEquals(started[0].variables.planKey, PLAN);
312
+ // The retro seed must carry the canonical agent-SLA bound through its PRODUCTION start path
313
+ // (not just the e2e, which supplies agentSlaTimeout manually) — else the two external agent
314
+ // tasks (`conformance` / `synthesize`) would run unbounded (Previously-missed advisory, PR #864).
315
+ assertEquals(started[0].variables.agentSlaTimeout, AGENT_SLA_TIMEOUT);
311
316
  assert(stores["plans"][0].retro_started_at, "retro_started_at must be stamped");
312
317
  assertEquals(stores["plan_retro_starts"].length, 1);
313
318
 
package/app/retro.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  // Data access goes through the record gateway (`data.table`), never hand-written SQL — matching
15
15
  // app/plan.ts, app/blackboard.ts, and app/taskDelta.ts.
16
16
  import type { DataLayer, EngineClient, Logger } from "@nanobpm/urban";
17
+ import { AGENT_SLA_TIMEOUT } from "./agentSla.ts";
17
18
  import { type BlackboardEntry, isUniqueViolation, readBlackboard } from "./blackboard.ts";
18
19
  import { hasDeliveredImplementationForPlan } from "./conformance.ts";
19
20
  import { TERMINAL_STATUSES } from "./delivery.ts";
@@ -355,6 +356,9 @@ export async function maybeStartRetro(
355
356
  planKey,
356
357
  repo: digest.repo,
357
358
  issueUrl: digest.issueUrl,
359
+ // Agent-task liveness SLA (issue #849): bounds the retro's two external agent tasks
360
+ // (`conformance`, `synthesize`) — a hung agent escalates to a human instead of parking.
361
+ agentSlaTimeout: AGENT_SLA_TIMEOUT,
358
362
  },
359
363
  }));
360
364
  } catch (err) {
package/app/service.ts CHANGED
@@ -11,7 +11,7 @@ import type { DataLayer, EngineClient } from "@nanobpm/urban";
11
11
  import { ABANDONED_STATUS, abandonUrl, mintAbandonToken, renderAbandonBrief } from "./abandon.ts";
12
12
  import { matchAdjudication, prAdjudications, resetAdjudications } from "./adjudications.ts";
13
13
  import { completeEscalationAutoApplied, escalationFormId } from "./agentCompletion.ts";
14
- import { agentSlaTimeout } from "./agentSla.ts";
14
+ import { AGENT_SLA_TIMEOUT } from "./agentSla.ts";
15
15
  import {
16
16
  CAPS_RESOLVED_MESSAGE,
17
17
  type CapabilityNeed,
@@ -214,13 +214,25 @@ export const MAX_ADVERSARIAL_PASSES = clampCiFixBudget(
214
214
  * (allows 0 = escalate immediately, ceiling-capped). */
215
215
  export const MAX_MERGE_STALL_ROUNDS = clampCiFixBudget(process.env.NANO_PR_MAX_MERGE_STALL_ROUNDS, 3);
216
216
 
217
- /** How long a merge-loop AGENT service task (rebase / fix-ci) may sit without completing before its
218
- * interrupting timer boundary fires and the PR escalates for human attention. Seeded as the
219
- * `agentSlaTimeout` process variable at merge start and evaluated by those tasks' boundary timers.
220
- * Unlike a human-decision escalation (PT24H), an agent task has no human in the loop — if its
221
- * capability is unstaffed or the agent hangs/crashes without failing the job, the token would
222
- * otherwise park forever. Override with `NANO_PR_AGENT_SLA_TIMEOUT` (ISO-8601 duration). */
223
- export const AGENT_SLA_TIMEOUT = agentSlaTimeout(process.env.NANO_PR_AGENT_SLA_TIMEOUT);
217
+ /** How long an external AGENT service task may sit without completing before its interrupting
218
+ * timer boundary fires and the process escalates for human attention. Seeded as the
219
+ * `agentSlaTimeout` process variable at every process start that hosts an external agent task
220
+ * (issue #849), and evaluated by the BOUNDED subset's boundary timers
221
+ * (`<bpmn:timeDuration>=agentSlaTimeout`, FEEL at timer creation): the implement-cell, the
222
+ * merge-cell's trial-merge, the merge-loop's rebase / fix-ci, and retro's conformance / synthesize.
223
+ * The convergence-loop (review-round / adversarial-review / classify-scope) and plan-fanout (plan /
224
+ * review-plan) tasks are only PRE-SEEDED here — their boundaries are intentionally deferred to #868
225
+ * (a bpmn-auto-layout back-edge routing limitation, #867). Plan-fanout's inline wave `trial-merge`
226
+ * (`plan-fanout.bpmn:524-542`) is likewise deferred: it is a separate external agent task from the
227
+ * standalone `merge-cell` (which has no callActivity caller) on a rerun back-edge with no boundary
228
+ * yet. For all of these this seed is preparation, not yet an armed bound. Unlike a human-decision
229
+ * escalation (PT24H), an agent task has no human in
230
+ * the loop — if its capability is unstaffed or the agent hangs/crashes without failing the job, the
231
+ * token would otherwise park forever. Override with `NANO_PR_AGENT_SLA_TIMEOUT` (ISO-8601 duration).
232
+ * The canonical validated constant lives in the leaf module `app/agentSla.ts` (re-exported here) so
233
+ * `app/feature.ts`, `app/plan.ts`, `app/retro.ts`, and `app/deliveryRunner.ts` seed it without an
234
+ * import cycle. */
235
+ export { AGENT_SLA_TIMEOUT } from "./agentSla.ts";
224
236
 
225
237
  /** How long the convergence loop waits for a fresh review before escalating to a human. Seeded as
226
238
  * the `reviewWaitTimeout` process variable at submit and evaluated by the process's
@@ -761,6 +773,13 @@ async function submitPrCritical(
761
773
  round: 1,
762
774
  maxRounds: clampRounds(maxRounds, MAX_ROUNDS),
763
775
  reviewWaitTimeout: REVIEW_WAIT_TIMEOUT,
776
+ // Agent-task liveness SLA (issue #849): PRE-SEED the bound for the loop's external agent
777
+ // tasks (review-round / adversarial-review / classify-scope). Those tasks sit on a
778
+ // back-edge loop whose boundary the layouter cannot yet route, so their SLA boundaries are
779
+ // intentionally deferred to #868 (layouter bug #867) — this seed is preparation for that
780
+ // follow-up, not yet an armed bound on this loop. (The merge-loop's rebase / fix-ci, seeded
781
+ // from the same constant, ARE bounded today.)
782
+ agentSlaTimeout: AGENT_SLA_TIMEOUT,
764
783
  // Bounded agent auto-ack (issue #796): the convergence loop re-dispatches the review-round
765
784
  // agent up to `ackRetryMax` times to ack suppressed advisories when the converge-gate blocks
766
785
  // solely on unacked ones, before escalating to a human. `ackRetryRound` counts those passes.
@@ -116,6 +116,15 @@ blank/absent base is rejected with a 400. Starting a
116
116
  plan is idempotent on the plan key; an already-running plan short-circuits. The
117
117
  response (202) echoes the `planKey` and engine `processKey`.
118
118
 
119
+ **Single issue, one PR (feature run, `startFeature`)** — no planning or fan-out. Same body and
120
+ base-branch admission, plus `converge` (enrol the PR in §1) and `autoMerge`:
121
+
122
+ ```bash
123
+ curl -sS -X POST __BASE__/actions/start/feature \
124
+ -H 'content-type: application/json' \
125
+ -d '{ "issue": "owner/repo#123", "baseBranch": "main", "confirmDefaultBase": true, "converge": true, "autoMerge": true }'
126
+ ```
127
+
119
128
  ### Base-branch admission (ADR 0003)
120
129
 
121
130
  `startPlanFanout` admits the base through one fail-fast gate before any task fans out.