@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 +12 -0
- package/app/contracts.ts +10 -2
- package/app/repoEnvelope.ts +21 -0
- package/app/service.test.ts +34 -0
- package/install.sh +45 -5
- package/package.json +1 -1
- package/test/install-smoke.sh +7 -0
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",
|
package/app/repoEnvelope.ts
CHANGED
|
@@ -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", {});
|
package/app/service.test.ts
CHANGED
|
@@ -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
|
-
|
|
1271
|
+
_env_body=$(build_config_body) || {
|
|
1251
1272
|
record_failure "app: invalid ProjectConfig.env value (control characters)"
|
|
1252
1273
|
return 1
|
|
1253
1274
|
}
|
|
1254
|
-
|
|
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
|
-
|
|
1259
|
-
|
|
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.
|
|
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",
|
package/test/install-smoke.sh
CHANGED
|
@@ -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, {});
|