c8ctl-plugin-nano 1.35.6 → 1.36.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.
Files changed (3) hide show
  1. package/README.md +30 -5
  2. package/c8ctl-plugin.js +211 -30
  3. package/package.json +8 -8
package/README.md CHANGED
@@ -485,7 +485,7 @@ stdin payload as `task`:
485
485
  Element templates emit flat dotpath header keys (strings); the plugin expands
486
486
  them into a nested object and coerces `"true"/"false"` → bool and numeric
487
487
  strings → int. The normalized shape is
488
- `{ schemaVersion, repository{provider,url,ref,depth,submodules,authRef}, branch{base,create,push}, setup{commands,env,secretRefs}, task{prompt,promptFile,maxIterations,timeoutMs,allowPr,prBase} }`.
488
+ `{ schemaVersion, repository{provider,url,ref,sha,depth,singleBranch,filter,baseRef,baseSha,cloneTimeoutMs,submodules,authRef}, branch{base,create,push}, setup{commands,env,secretRefs}, task{prompt,promptFile,maxIterations,timeoutMs,allowPr,prBase} }`.
489
489
 
490
490
  **Prompt = base + optional verbatim append.** The agent's prompt resolves to
491
491
  `task.prompt` (typically a model header filled at deploy time), falling back to a
@@ -531,8 +531,33 @@ the harness:
531
531
  1. resolve the optional repo credential (`repository.authRef`, or `GITHUB_TOKEN`
532
532
  for GitHub) — absent ⇒ anonymous clone;
533
533
  2. `git clone` (honouring `depth`/`submodules`, and `repository.ref`/`branch.base`
534
- as the checkout target) into a throwaway workspace under
535
- `<state>/agent-runs/run-*`;
534
+ as the checkout target). **`ref` is always a branch/tag name** (there is no hex
535
+ heuristic, so a legitimately hex-named branch like `deadbeef` is cloned via
536
+ `--branch`, never misread as a commit); to pin a **raw commit** use the
537
+ dedicated **`repository.sha`** field, which clones `ref`/`branch.base` (if any)
538
+ then fetches + checks the commit out as a detached HEAD. The clone lands in a
539
+ throwaway workspace under `<state>/agent-runs/run-*`. For a **huge monorepo**
540
+ the clone envelope can be
541
+ scoped so it finishes inside the clone timeout: **`singleBranch`** adds
542
+ `--single-branch` (fetch only `ref`, not every branch — a plain `clone --branch`
543
+ still pulls all branches/history); **`filter`** (e.g. `"blob:none"`) adds
544
+ `--filter=<spec>` for a partial/treeless clone (full commit graph, lazy blobs —
545
+ so `merge-base`/`git diff base...head` still work); **`baseRef`**/**`baseSha`**
546
+ additionally `git fetch` the base (respecting `depth`/`filter`) so a
547
+ single-branch/shallow clone can still diff `base...head` (exported as
548
+ `AGENT_REPO_BASE`). **`baseRef`** is always treated as a branch/tag name (even
549
+ one that looks hex-like) and is mapped into `refs/remotes/origin/<baseRef>`;
550
+ **`baseSha`** is the field for a raw commit SHA (fetched by id, exposed as the
551
+ SHA itself). `baseRef` and `baseSha` are **mutually exclusive** — setting both
552
+ is ambiguous, so the base fetch is skipped and a non-fatal `baseFetchError`
553
+ records the misconfiguration. A `--depth 1 --single-branch` of only the head
554
+ otherwise has NO
555
+ base and NO merge-base, so a naive `git diff main` fails. A failed base fetch
556
+ is **non-fatal** — the head clone still succeeds and
557
+ the failure is logged. **`cloneTimeoutMs`** overrides the clone/fetch timeout
558
+ per envelope (default 120s, or the `--clone-timeout` worker flag) as a backstop
559
+ for repos big enough to approach the cap even when shallow; a timeout is now
560
+ reported *as a timeout* rather than an opaque `exit 128`;
536
561
  3. create `branch.create` (if set) off that target;
537
562
  4. set a **committer identity** on the workspace, preferring the operator's own
538
563
  (`GIT_AUTHOR_*` env → global `git config user.name/email` → the
@@ -540,8 +565,8 @@ the harness:
540
565
  none resolve — so autonomous commits are authored by the human running the
541
566
  fleet (who has signed any CLA/DCO), not an anonymous bot;
542
567
  5. run the harness **in the workspace** (`cwd`), with `AGENT_WORKSPACE`,
543
- `AGENT_REPO_URL`, `AGENT_REPO_BRANCH`, `AGENT_REPO_REF` exported and the job
544
- envelope on stdin;
568
+ `AGENT_REPO_URL`, `AGENT_REPO_BRANCH`, `AGENT_REPO_REF`, `AGENT_REPO_BASE`
569
+ exported and the job envelope on stdin;
545
570
  6. on success, enumerate new commits, `git push` the branch when `branch.push`
546
571
  (default true), and — when `task.allowPr` — **reconcile the PR the agent
547
572
  opened** for the branch (`gh pr list --head <branch>`; `openedBy` reports the
package/c8ctl-plugin.js CHANGED
@@ -2294,7 +2294,25 @@ function normalizeTaskEnvelope(customHeaders, variables, opts = {}) {
2294
2294
  provider: (str(repo.provider) || 'github').toLowerCase(),
2295
2295
  url: str(repo.url),
2296
2296
  ref: str(repo.ref),
2297
+ // Dedicated field for a raw commit SHA to check out (detached), mirroring
2298
+ // baseSha/baseRef. `ref` is ALWAYS a branch/tag name — there is no hex
2299
+ // heuristic — so a legitimately hex-named branch (e.g. `deadbeef`) is
2300
+ // never misread as a commit; pin a commit via `sha` instead.
2301
+ sha: str(repo.sha),
2297
2302
  depth: coerceInt(repo.depth, undefined),
2303
+ // Scope the fetch to just `ref` (independent of depth) for callers that want
2304
+ // full history of one branch but not every branch of a huge monorepo.
2305
+ singleBranch: coerceBool(repo.singleBranch, false),
2306
+ // Partial/treeless clone spec (e.g. "blob:none"): full commit graph with
2307
+ // lazy blob fetch, so `merge-base` / `git diff base...head` still work.
2308
+ filter: str(repo.filter),
2309
+ // When a base branch/sha is supplied we additionally fetch it so a
2310
+ // single-branch/shallow clone can still diff `base...head`.
2311
+ baseRef: str(repo.baseRef),
2312
+ baseSha: str(repo.baseSha),
2313
+ // Per-envelope override of the clone/fetch timeout (ms) — a backstop for
2314
+ // repos big enough to approach the default 120s cap even when shallow.
2315
+ cloneTimeoutMs: coerceInt(repo.cloneTimeoutMs, undefined),
2298
2316
  submodules: coerceBool(repo.submodules, false),
2299
2317
  authRef: str(repo.authRef),
2300
2318
  };
@@ -2390,11 +2408,16 @@ function sameOrigin(a, b) {
2390
2408
  }
2391
2409
  }
2392
2410
 
2393
- function resolveBrokerRestConfig(env = process.env) {
2411
+ function resolveBrokerRestConfig(env = process.env, opts = {}) {
2394
2412
  // readConfig() swallows parse/IO errors and never throws (returns {}), so no
2395
2413
  // local try/catch is needed here.
2396
2414
  const cfg = readConfig() || {};
2415
+ // `opts.baseUrl` lets a caller pin the effective base (e.g. the active c8ctl
2416
+ // profile's REST address — see resolveAutoRestConfig) while still running it
2417
+ // through the SAME token same-origin gate below, so the token logic stays
2418
+ // single-sourced (never duplicated per call site).
2397
2419
  const baseUrl =
2420
+ opts.baseUrl ||
2398
2421
  env.NANO_REST_URL ||
2399
2422
  env.NANO_BASE_URL ||
2400
2423
  cfg.nanoUrl ||
@@ -2422,6 +2445,33 @@ function resolveBrokerRestConfig(env = process.env) {
2422
2445
  return { baseUrl, token };
2423
2446
  }
2424
2447
 
2448
+ // Resolve the C8 REST config the `--auto` engine-read reader is built from.
2449
+ // This is the job-type-read analogue of resolveLinkedPromptSource: an explicit
2450
+ // NANO_REST_URL / NANO_BASE_URL / cfg.nanoUrl override still wins (operator
2451
+ // escape hatch), but with NONE of those set the base is derived from the SAME
2452
+ // c8ctl client that activates jobs (its getConfig().restAddress) rather than the
2453
+ // localhost default — so a worker that can activate jobs against a profile
2454
+ // engine can also read the deployed job types from it. Without this, an `--auto`
2455
+ // worker on an active remote profile reads from http://localhost:8080, finds no
2456
+ // engine, discovers 0 job types, and crash-loops (jwulf/c8ctl-plugin-nano#93).
2457
+ // Falls back to resolveBrokerRestConfig's localhost default only when the client
2458
+ // exposes no usable restAddress. The token same-origin gate lives in
2459
+ // resolveBrokerRestConfig (re-run against the profile base), never duplicated.
2460
+ function resolveAutoRestConfig(camunda, env = process.env) {
2461
+ const cfg = readConfig() || {};
2462
+ const hasExplicitBase = Boolean(env.NANO_REST_URL || env.NANO_BASE_URL || cfg.nanoUrl);
2463
+ if (!hasExplicitBase && camunda && typeof camunda.getConfig === 'function') {
2464
+ let profileBase = '';
2465
+ try {
2466
+ profileBase = normalizeRestBase(camunda.getConfig()?.restAddress);
2467
+ } catch {
2468
+ // ignore — degrade to the resolveBrokerRestConfig (localhost) default below
2469
+ }
2470
+ if (profileBase) return resolveBrokerRestConfig(env, { baseUrl: profileBase });
2471
+ }
2472
+ return resolveBrokerRestConfig(env);
2473
+ }
2474
+
2425
2475
  // ---------------------------------------------------------------------------
2426
2476
  // `nano work --auto`: zero-config engine-read enrolment (issue #66).
2427
2477
  //
@@ -2790,12 +2840,51 @@ function redactToken(text, token) {
2790
2840
  return s.replace(/(https?:\/\/)[^@/\s]+@/gi, '$1');
2791
2841
  }
2792
2842
 
2843
+ // Build an operator-actionable diagnostic from one or more runGit results.
2844
+ // git splits its output unpredictably across stdout/stderr, so preferring one
2845
+ // stream (`stderr || stdout`) can drop the only useful line — the root of the
2846
+ // "stub reason"/"opaque exit 128" incidents. Combine BOTH streams of every
2847
+ // command, redact the token, and always append status/signal context (from the
2848
+ // last, i.e. failing, command) so an empty-output failure still says something.
2849
+ function gitErrorDetail(results, token, limit = 500) {
2850
+ const list = Array.isArray(results) ? results : [results];
2851
+ const body = redactToken(
2852
+ list
2853
+ .flatMap((r) => [r?.stderr, r?.stdout])
2854
+ .map((s) => String(s ?? '').trim())
2855
+ .filter(Boolean)
2856
+ .join('\n'),
2857
+ token,
2858
+ ).trim().slice(0, limit);
2859
+ const last = list[list.length - 1] || {};
2860
+ const ctx = [];
2861
+ if (last.status != null) ctx.push(`exit ${last.status}`);
2862
+ if (last.signal) ctx.push(`signal ${last.signal}`);
2863
+ const ctxStr = ctx.length ? `(${ctx.join(', ')})` : '';
2864
+ return [body, ctxStr].filter(Boolean).join(' ') || 'unknown error';
2865
+ }
2866
+
2793
2867
  function runGit(args, { cwd, env, timeoutMs = 120_000 } = {}) {
2794
2868
  try {
2795
2869
  const r = spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: timeoutMs });
2796
- return { status: r.status ?? (r.signal ? 128 : null), stdout: r.stdout || '', stderr: r.stderr || '', signal: r.signal || null };
2870
+ // spawnSync does not throw on timeout it returns with `error.code` set to
2871
+ // 'ETIMEDOUT' and the child SIGTERM-killed (signal set, status null). Surface
2872
+ // that as `timedOut` so callers can report a timeout instead of an
2873
+ // uninformative "exit 128" (ties to #89).
2874
+ const timedOut = !!(r.error && r.error.code === 'ETIMEDOUT');
2875
+ // A non-timeout spawn failure (e.g. ENOENT when `git` is missing, EACCES)
2876
+ // comes back via `r.error` with empty stdout/stderr; discarding
2877
+ // `r.error.message` leaves callers reporting an empty/"unknown error"
2878
+ // detail. Fold the spawn error (code + message) into stderr so
2879
+ // `gitErrorDetail` still surfaces something actionable.
2880
+ let stderr = r.stderr || '';
2881
+ if (r.error && !timedOut) {
2882
+ const spawnMsg = [r.error.code, r.error.message].filter(Boolean).join(': ');
2883
+ stderr = [stderr.trim(), spawnMsg].filter(Boolean).join('\n');
2884
+ }
2885
+ return { status: r.status ?? (r.signal ? 128 : null), stdout: r.stdout || '', stderr, signal: r.signal || null, timedOut, timeoutMs };
2797
2886
  } catch (err) {
2798
- return { status: null, stdout: '', stderr: err.message, signal: null };
2887
+ return { status: null, stdout: '', stderr: err.message, signal: null, timedOut: false, timeoutMs };
2799
2888
  }
2800
2889
  }
2801
2890
 
@@ -3115,38 +3204,108 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
3115
3204
  gitEnv.GIT_CONFIG_GLOBAL = devNull;
3116
3205
  }
3117
3206
 
3118
- const target = repo.ref || envelope.branch?.base || '';
3119
- // `git clone --branch` accepts a branch or tag name but NOT a raw commit SHA.
3120
- // For a SHA we clone the default branch, then fetch + check it out below.
3121
- const isSha = !!target && /^[0-9a-f]{7,40}$/i.test(target);
3207
+ const branchName = repo.ref || envelope.branch?.base || '';
3208
+ // A raw commit is requested ONLY via the dedicated `repository.sha` field
3209
+ // the sole unambiguous way to pin a commit. `ref` (→ `branchName`) is ALWAYS a
3210
+ // branch/tag name and is passed to `git clone --branch`; there is no hex
3211
+ // heuristic, so a legitimately hex-named branch like `deadbeef` is cloned as a
3212
+ // branch, not misread as a SHA. When a `sha` is given we clone `branchName`
3213
+ // (if any — the branch that should contain it) then fetch + detach onto it.
3214
+ const commitSha = repo.sha || '';
3215
+ // `sha` pins a raw commit and is passed to `git fetch origin <sha>` /
3216
+ // `git checkout --detach <sha>`. Validate it is a hex commit id (7–40 chars)
3217
+ // before use: this fails a misconfigured envelope fast and, because a hex id
3218
+ // can never start with `-`, forecloses a value being (mis)parsed as a git
3219
+ // option.
3220
+ if (commitSha && !/^[0-9a-f]{7,40}$/i.test(commitSha)) {
3221
+ throw new ProvisionError(`invalid repository.sha ${JSON.stringify(commitSha)} — expected a hex commit id (7–40 chars)`);
3222
+ }
3223
+ const isSha = !!commitSha;
3224
+ // Per-envelope timeout override (backstop for giant monorepos that approach the
3225
+ // default cap even when shallow); falls back to the caller-supplied timeout.
3226
+ const effectiveTimeoutMs = (repo.cloneTimeoutMs && repo.cloneTimeoutMs > 0) ? repo.cloneTimeoutMs : timeoutMs;
3122
3227
  const cloneArgs = [...credArgs(), 'clone', '--no-tags'];
3123
3228
  if (repo.depth && repo.depth > 0) cloneArgs.push('--depth', String(repo.depth));
3229
+ // `--single-branch` restricts the fetch to just `ref` — a plain `clone --branch`
3230
+ // still downloads every branch and all history. `--depth` implies this, but
3231
+ // honor it independently for a full-history single-branch clone.
3232
+ if (repo.singleBranch) cloneArgs.push('--single-branch');
3233
+ // Partial (blob-filtered) clone: full commit graph, lazy blobs — best fit for
3234
+ // reviewing a PR on a monorepo where a full checkout blows the timeout.
3235
+ if (repo.filter) cloneArgs.push(`--filter=${repo.filter}`);
3124
3236
  if (repo.submodules) cloneArgs.push('--recurse-submodules');
3125
- if (target && !isSha) cloneArgs.push('--branch', target);
3237
+ if (branchName) cloneArgs.push('--branch', branchName);
3126
3238
  const remote = authUrl(repo.url, repo.provider || 'github', !!token);
3127
3239
  cloneArgs.push(remote, workspaceDir);
3128
3240
 
3129
- const clone = runGit(cloneArgs, { env: gitEnv, timeoutMs });
3241
+ const clone = runGit(cloneArgs, { env: gitEnv, timeoutMs: effectiveTimeoutMs });
3130
3242
  if (clone.status !== 0) {
3131
- throw new ProvisionError(describeGitFailure('git clone', clone, { token, timeoutMs }));
3243
+ if (clone.timedOut) {
3244
+ // Preserve whatever git managed to print before SIGTERM (plus exit/signal
3245
+ // context) so a timeout is still diagnosable, not an opaque wall-clock hit.
3246
+ const detail = gitErrorDetail(clone, token);
3247
+ const detailNote = detail && detail !== 'unknown error' ? ` — last git output: ${detail}` : '';
3248
+ throw new ProvisionError(`git clone timed out after ${effectiveTimeoutMs}ms — the repo may be too large, or the network stalled; raise the timeout (repository.cloneTimeoutMs in the envelope, or the worker's --clone-timeout flag) or scope the clone with filter/singleBranch/depth${detailNote}`);
3249
+ }
3250
+ throw new ProvisionError(`git clone failed: ${gitErrorDetail(clone, token)}`);
3132
3251
  }
3133
3252
 
3134
3253
  if (isSha) {
3135
- // The SHA may not be present under a shallow clone of the default branch —
3136
- // fetch it explicitly (best effort), then check it out (detached HEAD).
3137
- const fetch = runGit([...credArgs(), 'fetch', '--no-tags', 'origin', target], { cwd: workspaceDir, env: gitEnv, timeoutMs });
3138
- const co = runGit(['checkout', '--detach', target], { cwd: workspaceDir, env: gitEnv, timeoutMs });
3254
+ // The SHA may not be present under a shallow clone of the branch — fetch it
3255
+ // explicitly (best effort), then check it out (detached HEAD).
3256
+ const fetch = runGit([...credArgs(), 'fetch', '--no-tags', 'origin', commitSha], { cwd: workspaceDir, env: gitEnv, timeoutMs: effectiveTimeoutMs });
3257
+ const co = runGit(['checkout', '--detach', commitSha], { cwd: workspaceDir, env: gitEnv });
3139
3258
  if (co.status !== 0) {
3140
- // Prefer the failing checkout's own output, but fall back to the fetch's
3141
- // (a timeout/fatal there is the real cause the checkout can't recover from)
3142
- // ONLY when fetch actually failed a succeeded fetch (status 0) is not the
3143
- // cause, and reporting it would yield a misleading "exit 0" message.
3144
- const useCheckout = !!(co.stderr || co.stdout || fetch.status === 0);
3145
- const source = useCheckout ? co : fetch;
3146
- // Label the failure with the action whose output we actually report, so a
3147
- // fetch timeout/fatal isn't misreported as a checkout failure.
3148
- const action = useCheckout ? `git checkout ${target}` : `git fetch origin ${target}`;
3149
- throw new ProvisionError(describeGitFailure(action, source, { token, timeoutMs }));
3259
+ // Combine the fetch + checkout output (the real reason often lives in the
3260
+ // fetch), and annotate a fetch timeout explicitly so a slow `git fetch
3261
+ // origin <sha>` is not misread as an opaque checkout failure.
3262
+ const fetchNote = fetch.timedOut ? ` (preceding git fetch origin ${commitSha} timed out after ${effectiveTimeoutMs}ms)` : '';
3263
+ throw new ProvisionError(`git checkout ${commitSha} failed: ${gitErrorDetail([fetch, co], token, 300)}${fetchNote}`);
3264
+ }
3265
+ }
3266
+
3267
+ // Optional base fetch: with a single-branch/shallow clone the head has no base
3268
+ // and no merge-base, so a naive `git diff <base>` fails. When a base branch or
3269
+ // sha is supplied, fetch it (respecting depth/filter) into a remote-tracking
3270
+ // ref so the harness can compute `git diff origin/<base>...HEAD`. Best-effort:
3271
+ // a failed base fetch is recorded, not fatal (the head clone still succeeded).
3272
+ let base = '';
3273
+ let baseFetchError;
3274
+ // `baseRef` (branch/tag) and `baseSha` (raw commit) are mutually exclusive — a
3275
+ // caller picks one. If BOTH are set the envelope is ambiguous, so rather than
3276
+ // silently preferring one (a surprising `base...head` diff), skip the base
3277
+ // fetch and record a non-fatal diagnostic so the misconfiguration is visible.
3278
+ if (repo.baseSha && repo.baseRef) {
3279
+ baseFetchError = `ambiguous base: both baseRef (${repo.baseRef}) and baseSha (${repo.baseSha}) set — provide only one`;
3280
+ } else {
3281
+ const baseTarget = repo.baseSha || repo.baseRef;
3282
+ if (baseTarget) {
3283
+ const isBaseSha = !!repo.baseSha;
3284
+ const fetchArgs = [...credArgs(), 'fetch', '--no-tags'];
3285
+ if (repo.depth && repo.depth > 0) fetchArgs.push('--depth', String(repo.depth));
3286
+ if (repo.filter) fetchArgs.push(`--filter=${repo.filter}`);
3287
+ if (isBaseSha) {
3288
+ // A raw sha can't be mapped to a stable name — fetch it (updates FETCH_HEAD)
3289
+ // and expose the sha itself as the diff base.
3290
+ fetchArgs.push('origin', baseTarget);
3291
+ base = baseTarget;
3292
+ } else {
3293
+ // Map the branch onto refs/remotes/origin/<baseRef> so `origin/<base>`
3294
+ // resolves for the reviewer even on a single-branch clone.
3295
+ fetchArgs.push('origin', `+${baseTarget}:refs/remotes/origin/${baseTarget}`);
3296
+ base = `origin/${baseTarget}`;
3297
+ }
3298
+ const bf = runGit(fetchArgs, { cwd: workspaceDir, env: gitEnv, timeoutMs: effectiveTimeoutMs });
3299
+ if (bf.status !== 0) {
3300
+ // Name the base target (branch vs sha) so the warning is actionable when
3301
+ // multiple refs are in play, and preserve git's output/context in both the
3302
+ // timeout and non-timeout paths.
3303
+ const baseLabel = `${isBaseSha ? 'sha' : 'branch'} ${baseTarget}`;
3304
+ base = '';
3305
+ baseFetchError = bf.timedOut
3306
+ ? `base fetch (${baseLabel}) timed out after ${effectiveTimeoutMs}ms: ${gitErrorDetail(bf, token, 300)}`
3307
+ : `base fetch (${baseLabel}) failed: ${gitErrorDetail(bf, token, 300)}`;
3308
+ }
3150
3309
  }
3151
3310
  }
3152
3311
 
@@ -3193,7 +3352,7 @@ function provisionRepo({ envelope, token, runDir, timeoutMs = 120_000 }) {
3193
3352
  // `git rev-parse HEAD` on an unborn branch (freshly cloned empty repo) exits
3194
3353
  // non-zero and echoes the literal "HEAD" on stdout — treat that as "no base
3195
3354
  // commit" (empty startSha) rather than a bogus revision.
3196
- return { workspaceDir, gitEnv, committer, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref: target || '', remote: redactToken(repo.url, token) };
3355
+ return { workspaceDir, gitEnv, committer, startSha: sha.status === 0 ? (sha.stdout || '').trim() : '', workingBranch, detached: !workingBranch, ref: commitSha || branchName || '', base, baseFetchError, remote: redactToken(repo.url, token) };
3197
3356
  }
3198
3357
 
3199
3358
  // Look up a PR for this branch (2a does NOT open it — the harness does, driven
@@ -4431,10 +4590,14 @@ async function workAgent(req, flags) {
4431
4590
  }
4432
4591
  const camunda = globalThis.c8ctl.createClient();
4433
4592
 
4434
- // Broker REST endpoint for live linked-resource prompts (issue #63) the same
4435
- // nano endpoint this worker already talks to. Resolved once at startup, and
4436
- // reused as the C8 REST source for `--auto`'s engine-read enrolment.
4437
- const restConfig = resolveBrokerRestConfig();
4593
+ // Broker REST endpoint for live linked-resource prompts (issue #63) and the
4594
+ // C8 REST source for `--auto`'s engine-read enrolment. Derived from the SAME
4595
+ // client that activates jobs (its profile REST address) when no explicit
4596
+ // NANO_REST_URL/NANO_BASE_URL/cfg.nanoUrl override is set — so a worker that
4597
+ // can activate jobs against the active profile engine also reads job types
4598
+ // from it, instead of a localhost default that crash-loops when nothing is
4599
+ // listening on :8080 (jwulf/c8ctl-plugin-nano#93). Resolved once at startup.
4600
+ const restConfig = resolveAutoRestConfig(camunda);
4438
4601
 
4439
4602
  // The desired job-type set. In `--auto` it is engine-read (∪ any --job-type
4440
4603
  // extras); otherwise it is the rank×capability matrix (∪ extras). The initial
@@ -4725,12 +4888,19 @@ async function workAgent(req, flags) {
4725
4888
  runDir = mkdtempSync(join(agentRunsRoot(), 'run-'));
4726
4889
  liveRunDirs.add(runDir);
4727
4890
  provisioned = provisionRepo({ envelope, token: repoToken, runDir, timeoutMs: cloneTimeoutMs });
4891
+ if (provisioned.baseFetchError) {
4892
+ logger.warn(`[${jobType}] job ${job.jobKey} base fetch failed — ${provisioned.baseFetchError}; base...head diffs may be unavailable`);
4893
+ }
4728
4894
  cwd = provisioned.workspaceDir;
4729
4895
  extraEnv = {
4730
4896
  AGENT_WORKSPACE: provisioned.workspaceDir,
4731
4897
  AGENT_REPO_URL: provisioned.remote,
4732
4898
  AGENT_REPO_BRANCH: provisioned.workingBranch || '',
4733
4899
  AGENT_REPO_REF: provisioned.ref || '',
4900
+ // The fetched base ref (e.g. `origin/main` or a base sha) for
4901
+ // computing `git diff <base>...HEAD`; empty when none was requested
4902
+ // or the base fetch failed (see provisioned.baseFetchError).
4903
+ AGENT_REPO_BASE: provisioned.base || '',
4734
4904
  // Pin the harness's commit identity to the resolved (placeholder-
4735
4905
  // sanitized) committer so the agent's own `git commit` can't be
4736
4906
  // hijacked by a placeholder GIT_AUTHOR_* inherited from process.env
@@ -5049,7 +5219,16 @@ async function workAgent(req, flags) {
5049
5219
  if (inFlightReconcile) return;
5050
5220
  reconcile().catch((err) => logger.warn(`--auto reconcile failed: ${err?.message || err}`));
5051
5221
  }, AUTO_POLL_INTERVAL_MS);
5052
- if (typeof autoPollTimer.unref === 'function') autoPollTimer.unref();
5222
+ // Deliberately REF'd (unlike the reaper/run-dir hygiene timers, which are
5223
+ // unref'd): in `--auto` this poll IS the retry loop, and it must keep the
5224
+ // process alive even with zero pollers. When the INITIAL engine read fails
5225
+ // (transient miss, or the engine isn't up yet) the worker registers 0
5226
+ // pollers; nothing else holds the event loop open (the SDK client with no
5227
+ // job workers doesn't, and the hygiene timers are unref'd), so an unref'd
5228
+ // poll timer would let the process exit 0 — the observed crash-loop under a
5229
+ // supervisor (jwulf/c8ctl-plugin-nano#93). Keeping it ref'd makes the worker
5230
+ // stay up and re-read on the next poll, exactly as the initial-read warning
5231
+ // promises. Shutdown clears it (clearInterval), so Ctrl-C/SIGTERM still exit.
5053
5232
  } else {
5054
5233
  // `watchFile` (polling stat) is deliberate over `fs.watch`: it survives the
5055
5234
  // atomic temp+rename that `writeConfig` does (fs.watch would rebind to the old
@@ -8413,6 +8592,7 @@ export {
8413
8592
  parseLinkedResources,
8414
8593
  pickLinkedResource,
8415
8594
  resolveBrokerRestConfig,
8595
+ resolveAutoRestConfig,
8416
8596
  resourceContentUrl,
8417
8597
  fetchLinkedResourceContent,
8418
8598
  resolveLinkedPrompt,
@@ -8468,6 +8648,7 @@ export {
8468
8648
  scanAgentTaskLeaves,
8469
8649
  readDeployedAgentJobTypes,
8470
8650
  resolveAutoJobTypes,
8651
+ workAgent,
8471
8652
  derivePollTimeoutMs,
8472
8653
  AGENT_TASK_NS,
8473
8654
  AGENT_RESULT_KEY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.35.6",
3
+ "version": "1.36.1",
4
4
  "type": "module",
5
5
  "description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
6
6
  "main": "c8ctl-plugin.js",
@@ -57,12 +57,12 @@
57
57
  },
58
58
  "optionalDependencies": {
59
59
  "node-pty": "^1.0.0",
60
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.35.6",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.35.6",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.35.6",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.35.6",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.35.6",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.35.6",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.35.6"
60
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.36.1",
61
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.36.1",
62
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.36.1",
63
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.36.1",
64
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.36.1",
65
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.36.1",
66
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.36.1"
67
67
  }
68
68
  }