@nanobpm/nano-workforce 0.171.11 → 0.172.1

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,15 @@
1
+ ## [0.172.1](https://github.com/nanobpm/nano-workforce/compare/v0.172.0...v0.172.1) (2026-09-01)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **onboarding:** send full ProjectConfig on env write (install.sh Phase 2 HTTP 422) ([#693](https://github.com/nanobpm/nano-workforce/issues/693)) ([cea5a13](https://github.com/nanobpm/nano-workforce/commit/cea5a133fe2e53299432e8f61b81c8eadbf9f1ed)), closes [nanobpm/nano-workforce#692](https://github.com/nanobpm/nano-workforce/issues/692) [#692](https://github.com/nanobpm/nano-workforce/issues/692)
6
+
7
+ ## [0.172.0](https://github.com/nanobpm/nano-workforce/compare/v0.171.11...v0.172.0) (2026-09-01)
8
+
9
+ ### Features
10
+
11
+ * **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)
12
+
1
13
  ## [0.171.11](https://github.com/nanobpm/nano-workforce/compare/v0.171.10...v0.171.11) (2026-09-01)
2
14
 
3
15
  ### 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/install.sh CHANGED
@@ -1124,6 +1124,25 @@ build_config_body() { # $1 = redact? ("redact" to mask the token)
1124
1124
  "$_gh" "$(json_str "$CONSOLE_ORIGIN")" "$(json_str "$APPVIEW_BASE")" "$(json_str "${PR_REVIEW_PORT:-3000}")"
1125
1125
  }
1126
1126
 
1127
+ # Merge our env object into the project's FULL ProjectConfig and print the
1128
+ # merged object. `PUT /console/api/projects/<name>/config` is a whole-object
1129
+ # replace: it deserializes the body into the complete ProjectConfig (which
1130
+ # requires `name`, `main`, `platforms`, `lang`, …), so a partial {"env":{…}}
1131
+ # body fails with HTTP 422 "missing field name" (nanobpm/nano-workforce#692).
1132
+ # We therefore fetch the live config and set its `.env` instead of hand-building
1133
+ # a minimal body. Values are passed via the environment (not argv) so quoting is
1134
+ # never an issue; node is a hard preflight requirement so it is always present.
1135
+ # Prints the merged JSON on success; returns non-zero (empty stdout) if either
1136
+ # input is not parseable.
1137
+ merge_config_env() { # $1 = current ProjectConfig JSON, $2 = {"env":{…}} JSON
1138
+ NWF_CFG_JSON=$1 NWF_ENV_JSON=$2 node -e '
1139
+ const cfg = JSON.parse(process.env.NWF_CFG_JSON);
1140
+ const env = JSON.parse(process.env.NWF_ENV_JSON).env || {};
1141
+ cfg.env = env;
1142
+ process.stdout.write(JSON.stringify(cfg));
1143
+ ' 2>/dev/null
1144
+ }
1145
+
1127
1146
  # Step 9 — the console must be reachable before we mutate anything.
1128
1147
  app_preflight() {
1129
1148
  info "Preflighting the console at ${CONSOLE_ORIGIN}/console"
@@ -1245,18 +1264,39 @@ app_provision() {
1245
1264
  esac
1246
1265
  fi
1247
1266
 
1248
- # 11.5 — write ProjectConfig.env (token redacted in dry-run output).
1267
+ # 11.5 — write ProjectConfig.env (token redacted in dry-run output). The
1268
+ # config endpoint is a whole-object replace, so we GET the current config and
1269
+ # merge our env into it; a partial {"env":{…}} body 422s (#692).
1249
1270
  info "Configuring project '$PROJECT' (ProjectConfig.env)"
1250
- _cfg_body=$(build_config_body) || {
1271
+ _env_body=$(build_config_body) || {
1251
1272
  record_failure "app: invalid ProjectConfig.env value (control characters)"
1252
1273
  return 1
1253
1274
  }
1254
- _cfg_show=$(build_config_body redact) || {
1275
+ _env_show=$(build_config_body redact) || {
1255
1276
  record_failure "app: invalid ProjectConfig.env value (control characters)"
1256
1277
  return 1
1257
1278
  }
1258
- api PUT "/console/api/projects/${PROJECT}/config" "$_cfg_body" "$_cfg_show"
1259
- if [ "$DRY_RUN" -eq 0 ]; then
1279
+ if [ "$DRY_RUN" -eq 1 ]; then
1280
+ # No live config to fetch under --dry-run: show the env we would merge in.
1281
+ api PUT "/console/api/projects/${PROJECT}/config" "$_env_body" "$_env_show"
1282
+ else
1283
+ api GET "/console/api/projects/${PROJECT}/config"
1284
+ case "$API_STATUS" in
1285
+ 2*) : ;;
1286
+ *) err "GET project config -> $(api_outcome)"
1287
+ [ -n "$API_BODY" ] && note "$API_BODY"
1288
+ record_failure "app: GET config $(api_outcome)"
1289
+ return 1 ;;
1290
+ esac
1291
+ _cur_cfg=$API_BODY
1292
+ _cfg_body=$(merge_config_env "$_cur_cfg" "$_env_body") || {
1293
+ err "could not merge env into the current ProjectConfig (unparseable config?)."
1294
+ [ -n "$_cur_cfg" ] && note "$_cur_cfg"
1295
+ record_failure "app: merge ProjectConfig.env"
1296
+ return 1
1297
+ }
1298
+ _cfg_show=$(merge_config_env "$_cur_cfg" "$_env_show") || _cfg_show=$_cfg_body
1299
+ api PUT "/console/api/projects/${PROJECT}/config" "$_cfg_body" "$_cfg_show"
1260
1300
  case "$API_STATUS" in
1261
1301
  2*) ok "config written (NANO_WORKFORCE_BASE_URL=${APPVIEW_BASE})" ;;
1262
1302
  *) err "PUT project config -> $(api_outcome)"
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.1",
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",
@@ -302,6 +302,13 @@ const srv = http.createServer((req, res) => {
302
302
  if (m === 'POST' && url === '/console/api/extensions/install') return json(201, { id: 'nano-workforce' });
303
303
  if (m === 'POST' && url === '/console/api/projects') return mode === 'exists' ? json(409, { error: 'exists' }) : json(201, { name: 'Workforce' });
304
304
  if (m === 'PUT' && url === '/console/api/projects/Workforce/config') return json(200, { ok: true });
305
+ if (m === 'GET' && url === '/console/api/projects/Workforce/config') {
306
+ // The whole-object ProjectConfig install.sh fetches before merging `.env`
307
+ // and PUTting it back (nanobpm/nano-workforce#692). Must carry the required
308
+ // top-level fields (name, main, platforms, lang) so merge_config_env has a
309
+ // full object to set `.env` on.
310
+ return json(200, { name: 'Workforce', main: 'nano-workforce', platforms: ['console'], lang: 'ts', env: {} });
311
+ }
305
312
  if (m === 'POST' && url === '/console/api/projects/Workforce/run') { ran = true; return json(200, { state: 'running' }); }
306
313
  if (m === 'GET' && url === '/console/app-view/Workforce/app/api/version') {
307
314
  if (mode === 'timeout') return json(404, {});