@nanobpm/nano-workforce 0.195.0 → 0.195.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,9 @@
1
+ ## [0.195.1](https://github.com/nanobpm/nano-workforce/compare/v0.195.0...v0.195.1) (2026-09-30)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **service:** commit review/fix-ci/rebase rounds to the PR branch, not a nano/agent-work fallback ([#817](https://github.com/nanobpm/nano-workforce/issues/817)) ([212e0cc](https://github.com/nanobpm/nano-workforce/commit/212e0cc9755d2cbba34ed5f170952a5e6a142729)), closes [jwulf/c8ctl-plugin-nano#231](https://github.com/jwulf/c8ctl-plugin-nano/issues/231) [#776](https://github.com/nanobpm/nano-workforce/issues/776)
6
+
1
7
  ## [0.195.0](https://github.com/nanobpm/nano-workforce/compare/v0.194.0...v0.195.0) (2026-09-30)
2
8
 
3
9
  ### Features
package/app/contracts.ts CHANGED
@@ -424,7 +424,7 @@ export const WIRE_CONTRACTS = {
424
424
  name: "io.nanobpm.agentTask.repository",
425
425
  owner: "app/repoEnvelope.ts",
426
426
  semantics:
427
- "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 for a single-task feature run AND — per issue #776 — the deterministic `feat/<node.id>` for each single-instance delivery-graph agent cell, injected per-cell by `agentNodeRepoEnvelope`/`app/deliveryRunner.ts` so a forgetful agent can never be left committing on the base branch and stranding its run on a non-ff push; the epic plan-fanout seed still omits it because its MI children each cut a per-child `feat/<task.id>` the app can't name at compile time, so those agents branch themselves). 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). Alongside the `repository` slice the envelope carries a sibling `task.allowPr: true` (issue #770): c8ctl-plugin-nano (≥1.60.2) only resolves the git credential (GITHUB_TOKEN, or the `gh` default) for repo provisioning behind that flag, so every repo-backed envelope sets it or the clone dies with `unable to get password from user`; the repoless path emits no envelope and so no `task`. Gated on c8ctl provisioner support (jwulf/c8ctl-plugin-nano#91, branch-cut guard jwulf/c8ctl-plugin-nano#231).",
427
+ "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`. On the PRE-PR path `branch.create` is the deterministic `feat/<task.id>` (emitted for a single-task feature run AND — per issue #776 — the deterministic `feat/<node.id>` for each single-instance delivery-graph agent cell, injected per-cell by `agentNodeRepoEnvelope`/`app/deliveryRunner.ts` so a forgetful agent can never be left committing on the base branch and stranding its run on a non-ff push; the epic plan-fanout seed still omits it because its MI children each cut a per-child `feat/<task.id>` the app can't name at compile time, so those agents branch themselves). On the PR-based paths `branch.create` is set to the PR HEAD branch itself (equal to `ref`, jwulf/c8ctl-plugin-nano#231): without it the harness's base-branch guard treats the checkout as base-like and cuts a throwaway `nano/agent-work/<base>-<run>` fallback, stranding the round's commits OFF the PR; with `ref == create == head` the harness does a no-op `checkout -B <head>` and commits/pushes to the PR head branch. So `branch.create` is present on both the pre-PR and PR-based paths (only a blank/omitted value drops the key). 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). Alongside the `repository` slice the envelope carries a sibling `task.allowPr: true` (issue #770): c8ctl-plugin-nano (≥1.60.2) only resolves the git credential (GITHUB_TOKEN, or the `gh` default) for repo provisioning behind that flag, so every repo-backed envelope sets it or the clone dies with `unable to get password from user`; the repoless path emits no envelope and so no `task`. Gated on c8ctl provisioner support (jwulf/c8ctl-plugin-nano#91, branch-cut guard jwulf/c8ctl-plugin-nano#231).",
428
428
  shape:
429
429
  'io.nanobpm.agentTask: { repository: { provider: "github", url: string, ref?: string, singleBranch: true, filter: "blob:none", cloneTimeoutMs: number, baseRef?: string, sha?: string, branch?: { create: string } }, task: { allowPr: true } }',
430
430
  },
@@ -133,8 +133,14 @@ export class RepoEnvelopeConflictError extends Error {
133
133
  * `implement-cell`) dispatches its agent BEFORE any PR exists, so it passes `ref = base` and a
134
134
  * `branchCreate` naming the deterministic `feat/<task.id>` feature branch the harness cuts off that
135
135
  * base itself (the agent-guide's `feat/*` convention) — instead of the agent branching by hand — so
136
- * the isolated clone lands on the right branch deterministically across a resume. `branchCreate` is
137
- * omitted on the PR-based paths, which check out an existing head. */
136
+ * the isolated clone lands on the right branch deterministically across a resume. The PR-based paths
137
+ * (review-round / fix-ci / rebase) ALSO pass `branchCreate = ref` (the PR HEAD branch, equal to
138
+ * `ref`): without an explicit `branch.create` the harness's base-branch guard
139
+ * (jwulf/c8ctl-plugin-nano#231) treats the checkout as base-like and cuts a throwaway
140
+ * `nano/agent-work/<base>-<run>` fallback, stranding the round's commits OFF the PR; with
141
+ * `ref == create == head` the harness does a no-op `checkout -B <head>` and commits/pushes to the PR
142
+ * head branch itself. `branchCreate` is therefore emitted on both the pre-PR and PR-based paths (only
143
+ * a blank/omitted value drops the key). */
138
144
  export function repoEnvelopeVars(
139
145
  repo: string,
140
146
  ref: string | null,
@@ -186,7 +192,10 @@ export function repoEnvelopeVars(
186
192
  // head. `branchCreate` asks the harness to cut the deterministic `feat/<task.id>` feature branch
187
193
  // off that base itself (the agent-guide's `feat/*` convention) instead of the agent branching by
188
194
  // hand — making the isolated clone land on the right branch deterministically across a resume.
189
- // Omitted (no key) on the PR-based paths (review/fix-ci/rebase), which check out an existing head.
195
+ // The PR-based paths (review/fix-ci/rebase) ALSO pass `branchCreate = ref` (the PR head): without
196
+ // it the harness's base-branch guard (jwulf/c8ctl-plugin-nano#231) cuts a throwaway
197
+ // `nano/agent-work/*` fallback OFF the PR; with `ref == create == head` it commits/pushes to the
198
+ // head branch. Only a blank/omitted value drops the key.
190
199
  ...(typeof branchCreate === "string" && branchCreate.trim() !== ""
191
200
  ? { branch: { create: branchCreate.trim() } }
192
201
  : {}),
@@ -1226,8 +1226,10 @@ test("repoEnvelopeVars emits the push-checkpoint under the harness-read `sha` ke
1226
1226
 
1227
1227
  // Pre-PR provisioning (issue #684): the implementation path has no head branch yet, so it passes
1228
1228
  // `ref = base` + a `branchCreate` so the harness clones the base and cuts the deterministic
1229
- // `feat/<task.id>` feature branch off it. `branch.create` is emitted only for a non-blank branch and
1230
- // is absent on the PR-based paths (which check out an existing head).
1229
+ // `feat/<task.id>` feature branch off it. `branch.create` is emitted only for a non-blank branch.
1230
+ // The PR-based paths (review-round / fix-ci / rebase) now pass `branchCreate = headRef` too (equal
1231
+ // to `ref`) so the harness commits/pushes to the PR head branch instead of cutting a throwaway
1232
+ // `nano/agent-work/*` fallback (jwulf/c8ctl-plugin-nano#231); see the PR-path test below.
1231
1233
  test("repoEnvelopeVars emits branch.create only for a non-blank pre-PR branch (#684)", () => {
1232
1234
  const repo = (repoEnvelopeVars("owner/repo", "main", null, null, "feat/issue-7") as any)["io.nanobpm.agentTask"]
1233
1235
  .repository;
@@ -1252,7 +1254,95 @@ test("repoEnvelopeVars emits branch.create only for a non-blank pre-PR branch (#
1252
1254
  assertEquals("branch" in (repoEnvelopeVars("owner/repo", "feat/x", "main") as any)["io.nanobpm.agentTask"].repository, false);
1253
1255
  });
1254
1256
 
1255
- // Durable-resume enrolment gate (issue #325, ADR 0062 Slice 5/5): `worldRestoreSha` — the seam
1257
+ // PR-path provisioning (jwulf/c8ctl-plugin-nano#231): the review-round / fix-ci / rebase call sites
1258
+ // pass `branchCreate = headRef` (equal to `ref`, the PR head). Without it the harness's base-branch
1259
+ // guard treats the checkout as base-like and cuts a throwaway `nano/agent-work/<base>-<run>` fallback,
1260
+ // stranding the round's commits OFF the PR (the head never advances → no-progress escalation). With
1261
+ // `ref == create == head` the harness does a no-op `checkout -B <head>` and commits/pushes to the PR
1262
+ // head branch itself.
1263
+ test("repoEnvelopeVars emits branch.create = head on the PR path (ref == create == head) (#231)", () => {
1264
+ const repo = (repoEnvelopeVars("owner/repo", "feat/issue-12", "main", null, "feat/issue-12") as any)[
1265
+ "io.nanobpm.agentTask"
1266
+ ].repository;
1267
+ assertEquals(repo.ref, "feat/issue-12", "the PR-path envelope checks out the head branch");
1268
+ assertEquals(repo.branch.create, "feat/issue-12", "and asks the harness to work on that same head branch, not a fallback");
1269
+ // Still branch-scoped and blobless, and the base tip stays reachable for the 3-dot diff.
1270
+ assertEquals(repo.singleBranch, true);
1271
+ assertEquals(repo.filter, "blob:none");
1272
+ assertEquals(repo.baseRef, "main");
1273
+ });
1274
+
1275
+ // Regression guard for the ACTUAL call sites (Copilot PR #817 review): the helper test above would
1276
+ // still pass if `submitPr`/`startMerge` dropped the 5th `branchCreate` argument, because
1277
+ // `repoEnvelopeVars` accepted it before this change. Drive both PUBLIC entry points with a mocked
1278
+ // PR-head response and assert the seeded `createInstance` envelope carries `branch.create === headRef`
1279
+ // — i.e. the call sites really thread the PR head into the branch-create slot (jwulf/c8ctl-plugin-nano#231).
1280
+ // Both `fetchPrMeta` (submit) and `fetchPrHead` (merge) read the same token-transport pulls endpoint,
1281
+ // so one fetch stub returning `head.ref` covers both.
1282
+ function withGithubHead(headRef: string, baseRef: string, run: () => Promise<void>): Promise<void> {
1283
+ const prevMode = process.env["NANO_PR_GITHUB_TRANSPORT"];
1284
+ const prevTok = process.env["GITHUB_TOKEN"];
1285
+ const prevFetch = globalThis.fetch;
1286
+ process.env["NANO_PR_GITHUB_TRANSPORT"] = "token";
1287
+ process.env["GITHUB_TOKEN"] = "t0ken";
1288
+ globalThis.fetch = ((url: string | URL | Request) => {
1289
+ void url;
1290
+ return Promise.resolve(
1291
+ new Response(
1292
+ JSON.stringify({ title: "PR title", body: "", head: { ref: headRef, sha: "HEADSHA" }, base: { ref: baseRef } }),
1293
+ { status: 200, headers: { "content-type": "application/json" } },
1294
+ ),
1295
+ );
1296
+ }) as typeof fetch;
1297
+ return run().finally(() => {
1298
+ globalThis.fetch = prevFetch;
1299
+ if (prevMode !== undefined) process.env["NANO_PR_GITHUB_TRANSPORT"] = prevMode;
1300
+ else delete process.env["NANO_PR_GITHUB_TRANSPORT"];
1301
+ if (prevTok !== undefined) process.env["GITHUB_TOKEN"] = prevTok;
1302
+ else delete process.env["GITHUB_TOKEN"];
1303
+ });
1304
+ }
1305
+
1306
+ test("submitPr threads the resolved PR head into the envelope branch.create (#231)", async () => {
1307
+ await withGithubHead("feat/issue-12", "main", async () => {
1308
+ const { data, engine, get } = captureVars();
1309
+ await submitPr(data, engine, {
1310
+ repo: "owner/repo",
1311
+ number: 12,
1312
+ url: "https://github.com/owner/repo/pull/12",
1313
+ prKey: "owner/repo#12",
1314
+ });
1315
+ const repo = (get()?.["io.nanobpm.agentTask"] as any)?.repository;
1316
+ assertEquals(repo?.ref, "feat/issue-12", "submitPr checks out the PR head branch");
1317
+ assertEquals(repo?.branch?.create, "feat/issue-12", "submitPr passes headRef as branch.create, not a fallback");
1318
+ });
1319
+ });
1320
+
1321
+ test("startMerge threads the resolved PR head into the envelope branch.create (#231)", async () => {
1322
+ await withGithubHead("feat/issue-13", "main", async () => {
1323
+ const { data, engine, get } = captureVars();
1324
+ await data.table("pull_requests", "pr_key").insert({
1325
+ pr_key: "owner/repo#13",
1326
+ repo: "owner/repo",
1327
+ number: 13,
1328
+ url: "https://github.com/owner/repo/pull/13",
1329
+ status: "converged",
1330
+ abandon_token: "tok-13",
1331
+ root_request_key: "owner/repo#13",
1332
+ });
1333
+ await startMerge(data, engine, {
1334
+ repo: "owner/repo",
1335
+ number: 13,
1336
+ url: "https://github.com/owner/repo/pull/13",
1337
+ prKey: "owner/repo#13",
1338
+ round: 2,
1339
+ });
1340
+ const repo = (get()?.["io.nanobpm.agentTask"] as any)?.repository;
1341
+ assertEquals(repo?.ref, "feat/issue-13", "startMerge checks out the PR head branch");
1342
+ assertEquals(repo?.branch?.create, "feat/issue-13", "startMerge passes headRef as branch.create, not a fallback");
1343
+ });
1344
+ });
1345
+
1256
1346
  // `submitPr`/`startMerge` thread into `repoEnvelopeVars` — hands the harness the last push-checkpoint
1257
1347
  // ONLY when the enrolled fleet advertises `durable-resume`. With no participant it degrades to null,
1258
1348
  // so the round redrives from scratch (exactly as today). Proven against a REAL in-memory SQLite db
package/app/service.ts CHANGED
@@ -758,7 +758,14 @@ async function submitPrCritical(
758
758
  // Host-git provisioning (c8ctl): deliver the repository envelope so the `senior:pr-review`
759
759
  // harness clones an isolated workspace checked out on the PR head branch. Spread last so an
760
760
  // unresolved head (`{}`) leaves the other vars untouched.
761
- ...repoEnvelopeVars(parsed.repo, headRef, baseRef, worldSha),
761
+ //
762
+ // Pass `headRef` as `branch.create` too (5th arg): on a PR path `ref` is the head branch, but
763
+ // WITHOUT an explicit `branch.create` the harness's base-branch guard (jwulf/c8ctl-plugin-nano#231)
764
+ // treats the checkout as base-like and cuts a throwaway `nano/agent-work/<base>-<run>` fallback,
765
+ // stranding the round's commits OFF the PR (the head never advances → no-progress escalation).
766
+ // With `ref == create == head` the harness does a no-op `checkout -B <head>` and commits/pushes
767
+ // to the PR head branch itself. Mirrors the delivery-graph node path (`agentNodeRepoEnvelope`, #776).
768
+ ...repoEnvelopeVars(parsed.repo, headRef, baseRef, worldSha, headRef),
762
769
  },
763
770
  });
764
771
  const processKey = processInstanceKey == null ? null : String(processInstanceKey);
@@ -919,8 +926,10 @@ export async function startMerge(
919
926
  abandonUrl: abUrl,
920
927
  abandonBrief: renderAbandonBrief(abUrl),
921
928
  // Host-git provisioning (c8ctl): same repository envelope as the convergence loop, so the
922
- // fix-ci/rebase agents operate on an isolated checkout of the PR head branch.
923
- ...repoEnvelopeVars(pr.repo, headRef, baseRef, worldSha),
929
+ // fix-ci/rebase agents operate on an isolated checkout of the PR head branch. `headRef` is
930
+ // passed as `branch.create` (5th arg) so the harness commits/pushes to the PR head branch
931
+ // instead of cutting a `nano/agent-work/*` fallback (jwulf/c8ctl-plugin-nano#231); see submitPr.
932
+ ...repoEnvelopeVars(pr.repo, headRef, baseRef, worldSha, headRef),
924
933
  },
925
934
  });
926
935
  if (processInstanceKey != null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.195.0",
3
+ "version": "0.195.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",