@nanobpm/nano-workforce 0.171.11 → 0.172.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 CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.172.0](https://github.com/nanobpm/nano-workforce/compare/v0.171.11...v0.172.0) (2026-09-01)
2
+
3
+ ### Features
4
+
5
+ * **service:** emit repository.cloneTimeoutMs in the agent-task envelope ([#697](https://github.com/nanobpm/nano-workforce/issues/697)) ([0d8a358](https://github.com/nanobpm/nano-workforce/commit/0d8a3582769f6561d647563168a8a50d0d54f3df)), closes [#694](https://github.com/nanobpm/nano-workforce/issues/694)
6
+
1
7
  ## [0.171.11](https://github.com/nanobpm/nano-workforce/compare/v0.171.10...v0.171.11) (2026-09-01)
2
8
 
3
9
  ### Bug Fixes
package/app/contracts.ts CHANGED
@@ -96,6 +96,14 @@ export const ENV_CONTRACTS = {
96
96
  semantics: "Poller cadence in milliseconds for the self-scheduling reconciliation loop.",
97
97
  default: "60000",
98
98
  },
99
+ NANO_PR_CLONE_TIMEOUT_MS: {
100
+ category: "env",
101
+ name: "NANO_PR_CLONE_TIMEOUT_MS",
102
+ owner: "app/repoEnvelope.ts",
103
+ semantics:
104
+ "Clone timeout in milliseconds emitted as `repository.cloneTimeoutMs` in the agent-task envelope so the c8ctl harness raises its 120s default for large-repo provisioning (branch-scoped blobless clones of big monorepos still approach/exceed 120s; issue #694). Inherited by both the review-round and merge paths via the one `repoEnvelopeVars` builder.",
105
+ default: "600000",
106
+ },
99
107
  NANO_PR_MAX_ROUNDS: {
100
108
  category: "env",
101
109
  name: "NANO_PR_MAX_ROUNDS",
@@ -384,9 +392,9 @@ export const WIRE_CONTRACTS = {
384
392
  name: "io.nanobpm.agentTask.repository",
385
393
  owner: "app/repoEnvelope.ts",
386
394
  semantics:
387
- "Repo-provisioning envelope the app emits as a `createInstance` process variable (`repoEnvelopeVars`, app/repoEnvelope.ts) and the c8ctl worker harness consumes to provision an isolated clone — instead of the agent inheriting the worker's launch dir (issue #684). `ref` is the branch checked out: the PR HEAD branch on the PR-based paths (review-round / fix-ci / rebase), or — on the PRE-PR implementation path (feature.bpmn / plan-fanout's `implement-cell`, issue #684; the delivery-graph runner's agent cells, issue #686) — the BASE branch, off which the harness cuts a new feature branch named by the optional `branch.create` (the deterministic `feat/<task.id>`, emitted only for a single-task feature run; the epic seed AND the delivery-graph run-root seed omit it so each fan-out slice's agent branches per node/MI child). Beyond `{provider,url,ref}`, it carries clone-shaping fields for large monorepos (issue #287): `singleBranch:true` + `filter:\"blob:none\"` (a branch-scoped, blobless partial clone — trees fetched up-front, blobs lazily, no `--depth 1` so the merge-base/3-dot diff stays valid) and an optional `baseRef` (the PR base branch, emitted only when resolvable, so the harness fetches its tip and keeps `origin/<base>` reachable). World-restore (issue #324, ADR 0062 Slice 4/5): an optional `sha` — the last durable push-checkpoint — is emitted so a REPLACEMENT activation on a fresh worktree reconstructs the tree to the EXACT pushed SHA (inverting the round's `git push` into `git fetch && git checkout <sha>`), omitted when the PR has no checkpoint yet. The field is named `sha` because that is the field the c8ctl harness's `provisionRepo` reads to drive the checkout — an earlier `commitSha` key was a silent no-op (issue #695). Gated on c8ctl provisioner support (jwulf/c8ctl-plugin-nano#91).",
395
+ "Repo-provisioning envelope the app emits as a `createInstance` process variable (`repoEnvelopeVars`, app/repoEnvelope.ts) and the c8ctl worker harness consumes to provision an isolated clone — instead of the agent inheriting the worker's launch dir (issue #684). `ref` is the branch checked out: the PR HEAD branch on the PR-based paths (review-round / fix-ci / rebase), or — on the PRE-PR implementation path (feature.bpmn / plan-fanout's `implement-cell`, issue #684; the delivery-graph runner's agent cells, issue #686) — the BASE branch, off which the harness cuts a new feature branch named by the optional `branch.create` (the deterministic `feat/<task.id>`, emitted only for a single-task feature run; the epic seed AND the delivery-graph run-root seed omit it so each fan-out slice's agent branches per node/MI child). Beyond `{provider,url,ref}`, it carries clone-shaping fields for large monorepos (issue #287): `singleBranch:true` + `filter:\"blob:none\"` (a branch-scoped, blobless partial clone — trees fetched up-front, blobs lazily, no `--depth 1` so the merge-base/3-dot diff stays valid) and an optional `baseRef` (the PR base branch, emitted only when resolvable, so the harness fetches its tip and keeps `origin/<base>` reachable) and a `cloneTimeoutMs` (from `NANO_PR_CLONE_TIMEOUT_MS`, default 600000 = 10 min) that raises the harness's 120s default so a large monorepo's blobless single-branch clone provisions instead of dying at 120s (issue #694). World-restore (issue #324, ADR 0062 Slice 4/5): an optional `sha` — the last durable push-checkpoint — is emitted so a REPLACEMENT activation on a fresh worktree reconstructs the tree to the EXACT pushed SHA (inverting the round's `git push` into `git fetch && git checkout <sha>`), omitted when the PR has no checkpoint yet. The field is named `sha` because that is the field the c8ctl harness's `provisionRepo` reads to drive the checkout — an earlier `commitSha` key was a silent no-op (issue #695). Gated on c8ctl provisioner support (jwulf/c8ctl-plugin-nano#91).",
388
396
  shape:
389
- '{ provider: "github", url: string, ref: string, singleBranch: true, filter: "blob:none", baseRef?: string, sha?: string, branch?: { create: string } }',
397
+ '{ provider: "github", url: string, ref: string, singleBranch: true, filter: "blob:none", cloneTimeoutMs: number, baseRef?: string, sha?: string, branch?: { create: string } }',
390
398
  },
391
399
  "epicSet.submit": {
392
400
  category: "wire",
@@ -8,6 +8,7 @@
8
8
  // rebase) and the PRE-PR implementation dispatch (`feature.ts`: `startFeature`; `plan.ts`:
9
9
  // `startPlan` → the epic's `implement-cell`) derive from this single implementation without a
10
10
  // `plan.ts ↔ service.ts` import cycle (AGENTS.md "derivation over duplication — no drift surfaces").
11
+ import { readEnvOr } from "./contracts.ts";
11
12
  import { isCommitSha } from "./world/index.ts";
12
13
 
13
14
  /** The reserved namespace key the c8ctl nano worker harness reads the agent-task envelope from
@@ -78,6 +79,13 @@ export function repoEnvelopeVars(
78
79
  // has a valid merge-base. Gated on c8ctl provisioner support (jwulf/c8ctl-plugin-nano#91).
79
80
  singleBranch: true,
80
81
  filter: "blob:none",
82
+ // Raise the harness's 120s default clone timeout for large-repo provisioning (issue #694).
83
+ // A branch-scoped blobless clone of a 24.6k-file monorepo (`camunda/camunda`) still
84
+ // approaches/exceeds 120s, so we emit `cloneTimeoutMs` (default 600000 = 10 min, via the
85
+ // `NANO_PR_CLONE_TIMEOUT_MS` knob) rather than let the harness fall back to its 120000ms
86
+ // default. Emitted on ALL paths from this one builder, so BOTH the review-round path
87
+ // (`service.ts` `submitPr`) and the merge path (`service.ts` `startMerge`) inherit it.
88
+ cloneTimeoutMs: cloneTimeoutMs(),
81
89
  // The base branch this PR targets — emitted so the harness fetches its tip alongside the
82
90
  // single-branch head, keeping `origin/<base>` reachable for the diff. Omitted when unknown.
83
91
  ...(baseRef ? { baseRef } : {}),
@@ -105,3 +113,16 @@ export function repoEnvelopeVars(
105
113
  },
106
114
  };
107
115
  }
116
+
117
+ /** Resolve the clone timeout (ms) the harness applies to provisioning, from the one typed knob
118
+ * `NANO_PR_CLONE_TIMEOUT_MS` (default 600000 = 10 min; issue #694). A branch-scoped blobless clone
119
+ * of a large monorepo still approaches/exceeds the harness's 120s default, so we raise it here. A
120
+ * non-numeric or non-positive override degrades to the registered default rather than emitting a
121
+ * bogus (0 / NaN) timeout the harness would then treat as "use the 120s default". */
122
+ function cloneTimeoutMs(): number {
123
+ const raw = Number(readEnvOr("NANO_PR_CLONE_TIMEOUT_MS"));
124
+ return Number.isFinite(raw) && raw > 0 ? raw : Number(envDefaultCloneTimeoutMs);
125
+ }
126
+
127
+ /** The registered default, resolved once from the schema so the fallback stays single-sourced. */
128
+ const envDefaultCloneTimeoutMs = readEnvOr("NANO_PR_CLONE_TIMEOUT_MS", "600000", {});
@@ -483,6 +483,38 @@ test("repoEnvelopeVars emits the repository envelope keyed on the PR head branch
483
483
  assertEquals(env.repository.filter, "blob:none");
484
484
  // The base branch is emitted so the harness fetches its tip, keeping `origin/<base>` reachable.
485
485
  assertEquals(env.repository.baseRef, "main");
486
+ // The clone timeout is raised above the harness's 120s default for large-repo provisioning (#694).
487
+ assertEquals(env.repository.cloneTimeoutMs, 600000);
488
+ });
489
+
490
+ test("repoEnvelopeVars emits cloneTimeoutMs from NANO_PR_CLONE_TIMEOUT_MS, default 600000 (#694)", () => {
491
+ // Default (knob unset): 600000ms = 10 min, raising the harness's 120000ms default so a
492
+ // branch-scoped blobless clone of a large monorepo provisions instead of dying at 120s.
493
+ const prev = process.env.NANO_PR_CLONE_TIMEOUT_MS;
494
+ delete process.env.NANO_PR_CLONE_TIMEOUT_MS;
495
+ try {
496
+ const def = (repoEnvelopeVars("owner/repo", "feat/x", "main") as any)["io.nanobpm.agentTask"];
497
+ assertEquals(def.repository.cloneTimeoutMs, 600000);
498
+ // Override wins.
499
+ process.env.NANO_PR_CLONE_TIMEOUT_MS = "900000";
500
+ const over = (repoEnvelopeVars("owner/repo", "feat/x", "main") as any)["io.nanobpm.agentTask"];
501
+ assertEquals(over.repository.cloneTimeoutMs, 900000);
502
+ // A non-positive / non-numeric override degrades to the registered default rather than emitting
503
+ // a bogus 0/NaN the harness would treat as "use the 120s default".
504
+ process.env.NANO_PR_CLONE_TIMEOUT_MS = "0";
505
+ assertEquals(
506
+ ((repoEnvelopeVars("owner/repo", "feat/x", "main") as any)["io.nanobpm.agentTask"]).repository.cloneTimeoutMs,
507
+ 600000,
508
+ );
509
+ process.env.NANO_PR_CLONE_TIMEOUT_MS = "notanumber";
510
+ assertEquals(
511
+ ((repoEnvelopeVars("owner/repo", "feat/x", "main") as any)["io.nanobpm.agentTask"]).repository.cloneTimeoutMs,
512
+ 600000,
513
+ );
514
+ } finally {
515
+ if (prev === undefined) delete process.env.NANO_PR_CLONE_TIMEOUT_MS;
516
+ else process.env.NANO_PR_CLONE_TIMEOUT_MS = prev;
517
+ }
486
518
  });
487
519
 
488
520
  test("repoEnvelopeVars omits baseRef when the base branch is unresolved", () => {
@@ -492,6 +524,8 @@ test("repoEnvelopeVars omits baseRef when the base branch is unresolved", () =>
492
524
  assertEquals(env.repository.filter, "blob:none");
493
525
  // …but `baseRef` is omitted entirely rather than emitted as null (no key at all).
494
526
  assertEquals("baseRef" in env.repository, false);
527
+ // The clone timeout is still emitted (it's base-ref-independent).
528
+ assertEquals(env.repository.cloneTimeoutMs, 600000);
495
529
  });
496
530
 
497
531
  test("repoEnvelopeVars emits nothing when the head branch is unresolved", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.171.11",
3
+ "version": "0.172.0",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",