tines 0.0.203 → 0.0.204

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 +5 -3
  2. package/dist/index.js +742 -295
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3407,8 +3407,8 @@ var STATE_CATEGORIES = [
3407
3407
  "awaiting_human",
3408
3408
  "done"
3409
3409
  ];
3410
- function runRefLabel(run) {
3411
- return run.issue_ref ? `run on ${run.issue_ref.project_name}/${run.issue_ref.number}` : `run ${run.run_id}`;
3410
+ function runRefLabel(run2) {
3411
+ return run2.issue_ref ? `run on ${run2.issue_ref.project_name}/${run2.issue_ref.number}` : `run ${run2.run_id}`;
3412
3412
  }
3413
3413
  function actorLabel(actor) {
3414
3414
  if (actor.run) {
@@ -3530,13 +3530,13 @@ function ageLabel(at, now = Date.now()) {
3530
3530
  function prUrlOf(v) {
3531
3531
  return v.pr_repo_url && v.pr_number !== null ? `${v.pr_repo_url}/pull/${v.pr_number}` : null;
3532
3532
  }
3533
- function runDurationLabel(run, now = Date.now()) {
3534
- if (!run.started_at) return "\u2014";
3535
- const seconds = Math.max(0, Math.round(((run.ended_at ?? now) - run.started_at) / 1e3));
3533
+ function runDurationLabel(run2, now = Date.now()) {
3534
+ if (!run2.started_at) return "\u2014";
3535
+ const seconds = Math.max(0, Math.round(((run2.ended_at ?? now) - run2.started_at) / 1e3));
3536
3536
  return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m`;
3537
3537
  }
3538
- function runCostLabel(run) {
3539
- const usage = run.usage;
3538
+ function runCostLabel(run2) {
3539
+ const usage = run2.usage;
3540
3540
  if (!usage) return null;
3541
3541
  if (usage.cost_usd !== void 0) {
3542
3542
  const dollars = usage.cost_usd === 0 ? "$0" : usage.cost_usd < 0.01 ? "<$0.01" : `$${usage.cost_usd.toFixed(2)}`;
@@ -3559,13 +3559,13 @@ function utilizationLabel(quota, activeRuns, stateName = (id2) => id2) {
3559
3559
  return `${activeRuns.length}/${quota.limit} global slot${quota.limit === 1 ? "" : "s"} in use`;
3560
3560
  }
3561
3561
  const counts = /* @__PURE__ */ new Map();
3562
- for (const run of activeRuns) {
3563
- const entry = counts.get(run.state_id_at_start) ?? {
3564
- name: run.state_at_start_name ?? stateName(run.state_id_at_start),
3562
+ for (const run2 of activeRuns) {
3563
+ const entry = counts.get(run2.state_id_at_start) ?? {
3564
+ name: run2.state_at_start_name ?? stateName(run2.state_id_at_start),
3565
3565
  n: 0
3566
3566
  };
3567
3567
  entry.n += 1;
3568
- counts.set(run.state_id_at_start, entry);
3568
+ counts.set(run2.state_id_at_start, entry);
3569
3569
  }
3570
3570
  for (const stateId of Object.keys(quota.overrides)) {
3571
3571
  if (!counts.has(stateId)) counts.set(stateId, { name: stateName(stateId), n: 0 });
@@ -3587,8 +3587,8 @@ function str(v) {
3587
3587
  function isoTime(v) {
3588
3588
  return typeof v === "number" && Number.isFinite(v) ? new Date(v).toISOString() : "an unknown time";
3589
3589
  }
3590
- function joinChanged(v, sep2) {
3591
- return Array.isArray(v) ? v.join(sep2) : "";
3590
+ function joinChanged(v, sep3) {
3591
+ return Array.isArray(v) ? v.join(sep3) : "";
3592
3592
  }
3593
3593
  function action(type) {
3594
3594
  return type.split(".")[1] ?? type;
@@ -5623,7 +5623,7 @@ function roundLines(round, now = Date.now()) {
5623
5623
  if (latest.artifacts.length > 0) {
5624
5624
  lines2.push(` ${latest.artifacts.map(artifactChangeLabel).join(" \xB7 ")}`);
5625
5625
  }
5626
- for (const run of earlier) lines2.push(` ${earlierAttemptLine(run, now)}`);
5626
+ for (const run2 of earlier) lines2.push(` ${earlierAttemptLine(run2, now)}`);
5627
5627
  if (latest.summary_comment) {
5628
5628
  lines2.push(` summary (${latest.summary_comment.id}):`);
5629
5629
  for (const line of latest.summary_comment.body.split("\n")) lines2.push(` ${line}`);
@@ -5637,20 +5637,20 @@ function roundLines(round, now = Date.now()) {
5637
5637
  }
5638
5638
  return lines2;
5639
5639
  }
5640
- function runHeadline(run, now) {
5641
- const cost = runCostLabel(run);
5640
+ function runHeadline(run2, now) {
5641
+ const cost = runCostLabel(run2);
5642
5642
  return [
5643
- `${run.run_id} on ${run.runner_name}`,
5644
- runDurationLabel(run, now),
5645
- run.outcome ?? run.status,
5643
+ `${run2.run_id} on ${run2.runner_name}`,
5644
+ runDurationLabel(run2, now),
5645
+ run2.outcome ?? run2.status,
5646
5646
  ...cost ? [cost] : [],
5647
- run.transition ? `"${run.transition.action ?? "moved directly"}" \u2192 ${run.transition.to_state.name}` : "no transition"
5647
+ run2.transition ? `"${run2.transition.action ?? "moved directly"}" \u2192 ${run2.transition.to_state.name}` : "no transition"
5648
5648
  ].join(" \xB7 ");
5649
5649
  }
5650
- function earlierAttemptLine(run, now) {
5651
- const back = run.returned_via;
5650
+ function earlierAttemptLine(run2, now) {
5651
+ const back = run2.returned_via;
5652
5652
  const how = back ? back.action ? `sent back by ${back.from_state.name} ("${back.action}")` : "moved directly back" : "never came back";
5653
- return `${run.run_id} \xB7 ${runDurationLabel(run, now)} \xB7 ${run.outcome ?? run.status} \xB7 ${how}`;
5653
+ return `${run2.run_id} \xB7 ${runDurationLabel(run2, now)} \xB7 ${run2.outcome ?? run2.status} \xB7 ${how}`;
5654
5654
  }
5655
5655
  function artifactChangeLabel(a) {
5656
5656
  const versions = a.from_version === null ? `v${a.to_version}` : `v${a.from_version} \u2192 v${a.to_version}`;
@@ -5669,16 +5669,16 @@ function roundSummaryLabel(summary) {
5669
5669
  ];
5670
5670
  return parts.length > 0 ? parts.join(" \xB7 ") : "\u2014";
5671
5671
  }
5672
- function runRow(run) {
5672
+ function runRow(run2) {
5673
5673
  return [
5674
- run.id,
5675
- run.issue_ref ? issueRef(run.issue_ref) : run.issue_id,
5676
- run.runner_name,
5677
- `${run.tier}${run.model ? ` (${run.model})` : ""}`,
5678
- `${run.status}${run.resumed_from_run_id ? ` \xB7 resumed run ${run.resumed_from_run_id}` : ""}`,
5679
- runDurationLabel(run),
5680
- runCostLabel(run) ?? "\u2014",
5681
- timestamp(run.created_at)
5674
+ run2.id,
5675
+ run2.issue_ref ? issueRef(run2.issue_ref) : run2.issue_id,
5676
+ run2.runner_name,
5677
+ `${run2.tier}${run2.model ? ` (${run2.model})` : ""}`,
5678
+ `${run2.status}${run2.resumed_from_run_id ? ` \xB7 resumed run ${run2.resumed_from_run_id}` : ""}`,
5679
+ runDurationLabel(run2),
5680
+ runCostLabel(run2) ?? "\u2014",
5681
+ timestamp(run2.created_at)
5682
5682
  ];
5683
5683
  }
5684
5684
  function byteSize(bytes2) {
@@ -5751,11 +5751,11 @@ function assertNewStatesHavePrompts(states, prompts) {
5751
5751
  }
5752
5752
  }
5753
5753
  function parseScheduleRef(ref) {
5754
- const sep2 = ref.indexOf("/");
5755
- if (sep2 < 1 || sep2 === ref.length - 1) {
5754
+ const sep3 = ref.indexOf("/");
5755
+ if (sep3 < 1 || sep3 === ref.length - 1) {
5756
5756
  throw new CliError(`schedule reference must look like <project>/<name>, got "${ref}"`);
5757
5757
  }
5758
- return { project: ref.slice(0, sep2), name: ref.slice(sep2 + 1) };
5758
+ return { project: ref.slice(0, sep3), name: ref.slice(sep3 + 1) };
5759
5759
  }
5760
5760
  function parseIssueRef(ref) {
5761
5761
  const match = ref.match(/^(.+)\/(\d+)$/);
@@ -5763,12 +5763,12 @@ function parseIssueRef(ref) {
5763
5763
  return { project: match[1], number: Number.parseInt(match[2], 10) };
5764
5764
  }
5765
5765
  function parseFileSpec(spec) {
5766
- const sep2 = spec.indexOf("=");
5767
- if (sep2 < 1 || sep2 === spec.length - 1) {
5766
+ const sep3 = spec.indexOf("=");
5767
+ if (sep3 < 1 || sep3 === spec.length - 1) {
5768
5768
  throw new CliError(`--file must look like <path>=@<local-file>, got "${spec}"`);
5769
5769
  }
5770
- const path2 = spec.slice(0, sep2);
5771
- const source = spec.slice(sep2 + 1);
5770
+ const path2 = spec.slice(0, sep3);
5771
+ const source = spec.slice(sep3 + 1);
5772
5772
  if (!source.startsWith("@")) {
5773
5773
  throw new CliError(
5774
5774
  `skill file content always comes from a local file: --file ${path2}=@<local-file>`
@@ -5782,10 +5782,10 @@ function parseFileSpec(spec) {
5782
5782
  }
5783
5783
  }
5784
5784
  function parseTargetSpec(spec) {
5785
- const sep2 = spec.lastIndexOf(":");
5786
- if (sep2 === -1) return { name: spec };
5787
- const name2 = spec.slice(0, sep2);
5788
- const tier = spec.slice(sep2 + 1);
5785
+ const sep3 = spec.lastIndexOf(":");
5786
+ if (sep3 === -1) return { name: spec };
5787
+ const name2 = spec.slice(0, sep3);
5788
+ const tier = spec.slice(sep3 + 1);
5789
5789
  if (!name2) throw new CliError(`target must look like <runner>[:tier], got "${spec}"`);
5790
5790
  if (!MODEL_TIERS.includes(tier)) {
5791
5791
  throw new CliError(`unknown tier "${tier}" in "${spec}" (tiers: ${MODEL_TIERS.join(", ")})`);
@@ -5997,12 +5997,12 @@ async function resolveLabelFlag(api, ref) {
5997
5997
  return match.id;
5998
5998
  }
5999
5999
  async function resolveStateFlag(api, ref) {
6000
- const sep2 = ref.indexOf("/");
6001
- if (sep2 < 1 || sep2 === ref.length - 1) {
6000
+ const sep3 = ref.indexOf("/");
6001
+ if (sep3 < 1 || sep3 === ref.length - 1) {
6002
6002
  die(`--state must look like <workflow>/<state>, got "${ref}"`);
6003
6003
  }
6004
- const workflow2 = await resolveWorkflow(api, ref.slice(0, sep2));
6005
- const stateRef2 = ref.slice(sep2 + 1);
6004
+ const workflow2 = await resolveWorkflow(api, ref.slice(0, sep3));
6005
+ const stateRef2 = ref.slice(sep3 + 1);
6006
6006
  const state = workflow2.states.find((s) => s.name === stateRef2) ?? workflow2.states.find((s) => s.id === stateRef2);
6007
6007
  if (!state) {
6008
6008
  die(
@@ -8489,9 +8489,6 @@ function register7(program3) {
8489
8489
  });
8490
8490
  }
8491
8491
 
8492
- // src/commands/runners.ts
8493
- import { hostname as hostname2 } from "node:os";
8494
-
8495
8492
  // src/daemon/daemon.ts
8496
8493
  import { spawn as spawn2 } from "node:child_process";
8497
8494
  import {
@@ -8505,7 +8502,6 @@ import {
8505
8502
  unlinkSync as unlinkSync2,
8506
8503
  writeFileSync as writeFileSync3
8507
8504
  } from "node:fs";
8508
- import { hostname, platform, arch } from "node:os";
8509
8505
  import { randomUUID } from "node:crypto";
8510
8506
  import { dirname as dirname4, join as join5 } from "node:path";
8511
8507
  import { StringDecoder } from "node:string_decoder";
@@ -9138,9 +9134,9 @@ function formatExitLine(exit) {
9138
9134
  return `# tines runner: exit ${parts.join(" ")} after ${formatDuration(exit.durationMs)}
9139
9135
  `;
9140
9136
  }
9141
- function exitLineForRun(run, exit) {
9142
- if (run.settled) return null;
9143
- return formatExitLine({ ...exit, timedOut: run.timedOut });
9137
+ function exitLineForRun(run2, exit) {
9138
+ if (run2.settled) return null;
9139
+ return formatExitLine({ ...exit, timedOut: run2.timedOut });
9144
9140
  }
9145
9141
  var RunTable = class {
9146
9142
  constructor(effects, opts = {}) {
@@ -9163,8 +9159,8 @@ var RunTable = class {
9163
9159
  has(runId) {
9164
9160
  return this.runs.has(runId);
9165
9161
  }
9166
- track(run) {
9167
- this.runs.set(run.runId, run);
9162
+ track(run2) {
9163
+ this.runs.set(run2.runId, run2);
9168
9164
  this.effects.persist();
9169
9165
  }
9170
9166
  /** Call after setting a run's pid so the state file reflects it. */
@@ -9180,10 +9176,10 @@ var RunTable = class {
9180
9176
  * already-settled run, a daemon shutdown. Guessing `failed` also errs the
9181
9177
  * safe way — it keeps a directory rather than destroying evidence.
9182
9178
  */
9183
- cleanup(run, outcome = "failed") {
9184
- this.runs.delete(run.runId);
9179
+ cleanup(run2, outcome = "failed") {
9180
+ this.runs.delete(run2.runId);
9185
9181
  this.effects.persist();
9186
- this.effects.release(run, { keep: this.keep(outcome, run), outcome });
9182
+ this.effects.release(run2, { keep: this.keep(outcome, run2), outcome });
9187
9183
  }
9188
9184
  /**
9189
9185
  * Ends a run: flush logs, finish-report, clean up. On a run someone
@@ -9199,26 +9195,26 @@ var RunTable = class {
9199
9195
  * `{ judgment: 'rate_limited' }` with its reset time, while transient 5xx
9200
9196
  * and transport outages use `interrupted` and the normal short backoff.
9201
9197
  */
9202
- async finishAndCleanup(run, status, error, judgment) {
9203
- if (run.settled) return this.cleanup(run);
9204
- run.settled = true;
9205
- run.endNote ??= error;
9206
- run.drain?.();
9207
- if (this.keep(status, run)) this.effects.noteKept?.(run);
9208
- await run.flush?.();
9209
- const keptBeforeFinish = this.keep(status, run);
9198
+ async finishAndCleanup(run2, status, error, judgment) {
9199
+ if (run2.settled) return this.cleanup(run2);
9200
+ run2.settled = true;
9201
+ run2.endNote ??= error;
9202
+ run2.drain?.();
9203
+ if (this.keep(status, run2)) this.effects.noteKept?.(run2);
9204
+ await run2.flush?.();
9205
+ const keptBeforeFinish = this.keep(status, run2);
9210
9206
  try {
9211
- await this.effects.finish(run, status, error, judgment);
9212
- this.effects.log(`run ${run.runId} finished: ${status}${error ? ` (${error})` : ""}`);
9213
- if (!keptBeforeFinish && this.keep(status, run)) {
9214
- this.effects.log(`run ${run.runId} workspace kept for resume at ${run.workspace}`);
9207
+ await this.effects.finish(run2, status, error, judgment);
9208
+ this.effects.log(`run ${run2.runId} finished: ${status}${error ? ` (${error})` : ""}`);
9209
+ if (!keptBeforeFinish && this.keep(status, run2)) {
9210
+ this.effects.log(`run ${run2.runId} workspace kept for resume at ${run2.workspace}`);
9215
9211
  }
9216
9212
  } catch (err) {
9217
9213
  this.effects.log(
9218
- `finish report for run ${run.runId} not accepted: ${err instanceof Error ? err.message : String(err)}`
9214
+ `finish report for run ${run2.runId} not accepted: ${err instanceof Error ? err.message : String(err)}`
9219
9215
  );
9220
9216
  }
9221
- this.cleanup(run, status);
9217
+ this.cleanup(run2, status);
9222
9218
  }
9223
9219
  /**
9224
9220
  * Marks a supervisor-settled run (`cancels`): kill, do NOT finish-report.
@@ -9227,11 +9223,11 @@ var RunTable = class {
9227
9223
  * is cleaned up by the launch path's next settled check.
9228
9224
  */
9229
9225
  markCanceled(runId) {
9230
- const run = this.runs.get(runId);
9231
- if (!run || run.settled) return null;
9232
- run.canceled = true;
9233
- run.settled = true;
9234
- return run;
9226
+ const run2 = this.runs.get(runId);
9227
+ if (!run2 || run2.settled) return null;
9228
+ run2.canceled = true;
9229
+ run2.settled = true;
9230
+ return run2;
9235
9231
  }
9236
9232
  };
9237
9233
  var LogBatcher = class {
@@ -9466,6 +9462,9 @@ function message(err) {
9466
9462
  return err instanceof Error ? err.message : String(err);
9467
9463
  }
9468
9464
 
9465
+ // src/daemon/register.ts
9466
+ import { arch, hostname, platform } from "node:os";
9467
+
9469
9468
  // src/daemon/store.ts
9470
9469
  import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
9471
9470
  import { join as join4 } from "node:path";
@@ -9596,15 +9595,48 @@ function directorySizeBytes(path2) {
9596
9595
  return total;
9597
9596
  }
9598
9597
 
9598
+ // src/daemon/register.ts
9599
+ function missingApiKeyMessage(name2, baseUrl) {
9600
+ return `no stored runner token for "${name2}" at ${baseUrl} \u2014 set TINES_API_KEY (a user API key) to register`;
9601
+ }
9602
+ async function ensureRunnerCredentials(opts) {
9603
+ const stored = loadRunnerCredentials(opts.configDir, opts.baseUrl, opts.name);
9604
+ if (stored) {
9605
+ opts.log(
9606
+ `reconnecting as runner "${opts.name}" (${stored.runner_id}) \u2014 token from ${opts.configDir}`
9607
+ );
9608
+ return { creds: stored, registered: false };
9609
+ }
9610
+ if (!opts.apiKey) throw new Error(missingApiKeyMessage(opts.name, opts.baseUrl));
9611
+ const userClient = createApiClient({ baseUrl: opts.baseUrl, apiKey: opts.apiKey });
9612
+ const registered = await userClient.registerRunner({
9613
+ name: opts.name,
9614
+ harness: opts.harness,
9615
+ ...opts.command !== void 0 ? { command: opts.command } : {},
9616
+ max_concurrent: opts.maxConcurrent,
9617
+ hostname: hostname(),
9618
+ platform: `${platform()} ${arch()}`
9619
+ });
9620
+ const creds = { runner_id: registered.runner.id, token: registered.runner_token };
9621
+ saveRunnerCredentials(opts.configDir, opts.baseUrl, opts.name, creds);
9622
+ opts.log(
9623
+ `registered runner "${opts.name}" (${creds.runner_id}); token stored in ${opts.configDir}`
9624
+ );
9625
+ return { creds, registered: true };
9626
+ }
9627
+ function nextStepsMessage(name2, baseUrl) {
9628
+ return `next: route work to "${name2}" \u2014 ${baseUrl}/agents (or: tines routing set ${name2}). Eligible work starts when this runner is available and routing matches. If automation is stopped, resume it in Agents or with tines supervisor enable.`;
9629
+ }
9630
+
9599
9631
  // src/daemon/daemon.ts
9600
9632
  var CLI_REFRESH_TTL_MS = 10 * 6e4;
9601
9633
  var DAEMON_VERSION = cliVersion();
9602
- async function uploadRawLog(run) {
9603
- const path2 = run.rawSpoolPath;
9604
- if (!path2 || !run.rawUpload) return;
9605
- run.rawSpoolPath = void 0;
9634
+ async function uploadRawLog(run2) {
9635
+ const path2 = run2.rawSpoolPath;
9636
+ if (!path2 || !run2.rawUpload) return;
9637
+ run2.rawSpoolPath = void 0;
9606
9638
  try {
9607
- await new Promise((resolve) => run.rawSpool?.end(resolve) ?? resolve());
9639
+ await new Promise((resolve) => run2.rawSpool?.end(resolve) ?? resolve());
9608
9640
  const size = statSync4(path2).size;
9609
9641
  if (size > 0) {
9610
9642
  let body = readFileSync9(path2);
@@ -9618,7 +9650,7 @@ async function uploadRawLog(run) {
9618
9650
  body.subarray(body.byteLength - RUN_LOG_RAW_MAX_BYTES + marker.byteLength)
9619
9651
  ]);
9620
9652
  }
9621
- await run.rawUpload(body);
9653
+ await run2.rawUpload(body);
9622
9654
  }
9623
9655
  } catch {
9624
9656
  } finally {
@@ -9680,31 +9712,21 @@ async function runDaemon(opts) {
9680
9712
  );
9681
9713
  }
9682
9714
  };
9683
- let creds = loadRunnerCredentials(opts.configDir, baseUrl, opts.name);
9684
- if (creds) {
9685
- log(
9686
- `reconnecting as runner "${opts.name}" (${creds.runner_id}) \u2014 token from ${opts.configDir}`
9687
- );
9688
- } else {
9689
- if (!opts.apiKey) {
9690
- throw new Error(
9691
- `no stored runner token for "${opts.name}" at ${baseUrl} \u2014 set TINES_API_KEY (a user API key) to register`
9692
- );
9693
- }
9694
- const userClient = createApiClient({ baseUrl, apiKey: opts.apiKey });
9695
- const registered = await userClient.registerRunner({
9696
- name: opts.name,
9697
- harness: opts.harness,
9698
- ...opts.command !== void 0 ? { command: opts.command } : {},
9699
- max_concurrent: opts.maxConcurrent,
9700
- hostname: hostname(),
9701
- platform: `${platform()} ${arch()}`
9702
- });
9703
- creds = { runner_id: registered.runner.id, token: registered.runner_token };
9704
- saveRunnerCredentials(opts.configDir, baseUrl, opts.name, creds);
9705
- log(`registered runner "${opts.name}" (${creds.runner_id}); token stored in ${opts.configDir}`);
9715
+ const ensured = await ensureRunnerCredentials({
9716
+ configDir: opts.configDir,
9717
+ baseUrl,
9718
+ name: opts.name,
9719
+ harness: opts.harness,
9720
+ ...opts.command !== void 0 ? { command: opts.command } : {},
9721
+ maxConcurrent: opts.maxConcurrent,
9722
+ ...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
9723
+ log
9724
+ });
9725
+ const creds = ensured.creds;
9726
+ if (ensured.registered) {
9727
+ log(nextStepsMessage(opts.name, baseUrl));
9706
9728
  log(
9707
- `next: route work to "${opts.name}" \u2014 ${baseUrl}/agents (or: tines routing set ${opts.name}). Eligible work starts when this runner is available and routing matches. If automation is stopped, resume it in Agents or with tines supervisor enable.`
9729
+ `keep it running: \`tines runner install --name ${opts.name} --harness ${opts.harness.replaceAll("_", "-")}\` installs this runner as a launchd/systemd service that keeps itself updated (docs/runner-daemon.md)`
9708
9730
  );
9709
9731
  }
9710
9732
  const client2 = createApiClient({ baseUrl, apiKey: creds.token });
@@ -9719,7 +9741,7 @@ async function runDaemon(opts) {
9719
9741
  const selfUpdate = opts.selfUpdate && opts.cliRefresh && managedInstall;
9720
9742
  if (opts.selfUpdate && opts.cliRefresh) {
9721
9743
  log(
9722
- managedInstall ? `self-update: on \u2014 this daemon (tines ${DAEMON_VERSION}) runs from ${prefix} and will restart once idle after a newer release is installed there` : `self-update: off \u2014 this daemon (tines ${DAEMON_VERSION}) runs from ${selfPath ?? process.argv[1] ?? "?"}, not ${prefix}; launch it from ${join5(prefix, "node_modules", ".bin", "tines")} to have refreshes apply on restart (docs/runner-daemon.md)`
9744
+ managedInstall ? `self-update: on \u2014 this daemon (tines ${DAEMON_VERSION}) runs from ${prefix} and will restart once idle after a newer release is installed there` : `self-update: off \u2014 this daemon (tines ${DAEMON_VERSION}) runs from ${selfPath ?? process.argv[1] ?? "?"}, not ${prefix}; launch it from ${join5(prefix, "node_modules", ".bin", "tines")} to have refreshes apply on restart \u2014 \`tines runner install --name ${opts.name} --harness ${opts.harness.replaceAll("_", "-")}\` sets that up as a service (docs/runner-daemon.md)`
9723
9745
  );
9724
9746
  }
9725
9747
  let pendingUpdate = null;
@@ -9739,9 +9761,9 @@ async function runDaemon(opts) {
9739
9761
  log(`agent CLI: ${cliLabel(await ensureCli())}`);
9740
9762
  const table2 = new RunTable(
9741
9763
  {
9742
- finish: async (run, status, error, judgment) => {
9743
- const summary = run.renderer?.summary();
9744
- const ended = await client2.finishRun(run.runId, {
9764
+ finish: async (run2, status, error, judgment) => {
9765
+ const summary = run2.renderer?.summary();
9766
+ const ended = await client2.finishRun(run2.runId, {
9745
9767
  status,
9746
9768
  ...error ? { error } : {},
9747
9769
  ...judgment,
@@ -9749,8 +9771,8 @@ async function runDaemon(opts) {
9749
9771
  ...summary?.pricingEvidence ? {
9750
9772
  pricing_evidence: {
9751
9773
  ...summary.pricingEvidence,
9752
- model: run.pricingModel ?? null,
9753
- session_mode: run.pricingSessionMode ?? "cold",
9774
+ model: run2.pricingModel ?? null,
9775
+ session_mode: run2.pricingSessionMode ?? "cold",
9754
9776
  daemon_version: DAEMON_VERSION
9755
9777
  }
9756
9778
  } : {},
@@ -9758,33 +9780,33 @@ async function runDaemon(opts) {
9758
9780
  ...summary?.numTurns !== void 0 ? { turn_count: summary.numTurns } : {},
9759
9781
  // The whole conversation's turns, which for a resumed run is
9760
9782
  // more than this run's own: the size guard reads this one.
9761
- ...summary?.numTurns !== void 0 ? { conversation_turn_count: run.priorTurns + summary.numTurns } : {},
9762
- workspace_path: run.workspace
9783
+ ...summary?.numTurns !== void 0 ? { conversation_turn_count: run2.priorTurns + summary.numTurns } : {},
9784
+ workspace_path: run2.workspace
9763
9785
  });
9764
- if (ended?.resume_expires_at) run.keepForResume = true;
9786
+ if (ended?.resume_expires_at) run2.keepForResume = true;
9765
9787
  },
9766
- release: (run, { keep, outcome }) => {
9767
- if (run.timeout) clearTimeout(run.timeout);
9768
- run.renderer?.finish();
9769
- settleWorkspace(run.workspace, keep, {
9770
- run_id: run.runId,
9771
- ...run.issueLabel ? { issue_ref: run.issueLabel } : {},
9788
+ release: (run2, { keep, outcome }) => {
9789
+ if (run2.timeout) clearTimeout(run2.timeout);
9790
+ run2.renderer?.finish();
9791
+ settleWorkspace(run2.workspace, keep, {
9792
+ run_id: run2.runId,
9793
+ ...run2.issueLabel ? { issue_ref: run2.issueLabel } : {},
9772
9794
  status: outcome,
9773
- ...run.endNote ? { error: run.endNote } : {}
9795
+ ...run2.endNote ? { error: run2.endNote } : {}
9774
9796
  });
9775
- void uploadRawLog(run);
9797
+ void uploadRawLog(run2);
9776
9798
  sweepKeptWorkspaces();
9777
9799
  },
9778
- noteKept: (run) => run.batcher.append(`workspace kept at ${run.workspace}
9800
+ noteKept: (run2) => run2.batcher.append(`workspace kept at ${run2.workspace}
9779
9801
  `),
9780
9802
  persist: () => {
9781
- const entries = table2.values().filter((run) => run.child?.pid !== void 0).map((run) => ({
9782
- run_id: run.runId,
9783
- pid: run.child.pid,
9784
- workspace: run.workspace,
9785
- key_fingerprint: run.keyFingerprint,
9786
- started_at: run.spawnedAt,
9787
- ...run.issueLabel ? { issue_ref: run.issueLabel } : {}
9803
+ const entries = table2.values().filter((run2) => run2.child?.pid !== void 0).map((run2) => ({
9804
+ run_id: run2.runId,
9805
+ pid: run2.child.pid,
9806
+ workspace: run2.workspace,
9807
+ key_fingerprint: run2.keyFingerprint,
9808
+ started_at: run2.spawnedAt,
9809
+ ...run2.issueLabel ? { issue_ref: run2.issueLabel } : {}
9788
9810
  }));
9789
9811
  saveDaemonState(statePath, entries);
9790
9812
  },
@@ -9793,7 +9815,7 @@ async function runDaemon(opts) {
9793
9815
  {
9794
9816
  // A workspace the server retained for a resume is kept whatever the
9795
9817
  // --keep-workspaces mode says; pruning still bounds it by age/count.
9796
- keep: (outcome, run) => run.keepForResume || keepWorkspace(opts.keepWorkspaces, outcome)
9818
+ keep: (outcome, run2) => run2.keepForResume || keepWorkspace(opts.keepWorkspaces, outcome)
9797
9819
  }
9798
9820
  );
9799
9821
  for (const orphan of loadDaemonState(statePath)) {
@@ -9827,12 +9849,12 @@ async function runDaemon(opts) {
9827
9849
  }
9828
9850
  saveDaemonState(statePath, []);
9829
9851
  const killWithoutFinish = (runId) => {
9830
- const run = table2.markCanceled(runId);
9831
- if (!run) return;
9832
- run.endNote ??= "canceled by supervisor";
9852
+ const run2 = table2.markCanceled(runId);
9853
+ if (!run2) return;
9854
+ run2.endNote ??= "canceled by supervisor";
9833
9855
  log(`supervisor canceled run ${runId}; killing without finish-reporting`);
9834
- if (run.child?.pid) {
9835
- const pid = run.child.pid;
9856
+ if (run2.child?.pid) {
9857
+ const pid = run2.child.pid;
9836
9858
  killTree(pid, "SIGTERM");
9837
9859
  setTimeout(() => killTree(pid, "SIGKILL"), 5e3).unref?.();
9838
9860
  }
@@ -9848,7 +9870,7 @@ async function runDaemon(opts) {
9848
9870
  }
9849
9871
  const workspace = resume ? resume.workspace_path : join5(workspacesDir(opts.configDir), runId);
9850
9872
  const issueLabel = assignment.run.issue_ref ? `${assignment.run.issue_ref.project_name}/${assignment.run.issue_ref.number}` : void 0;
9851
- const run = {
9873
+ const run2 = {
9852
9874
  runId,
9853
9875
  workspace,
9854
9876
  ...issueLabel ? { issueLabel } : {},
@@ -9864,8 +9886,8 @@ async function runDaemon(opts) {
9864
9886
  { onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`) }
9865
9887
  )
9866
9888
  };
9867
- run.flush = () => run.batcher.flush();
9868
- table2.track(run);
9889
+ run2.flush = () => run2.batcher.flush();
9890
+ table2.track(run2);
9869
9891
  log(
9870
9892
  resume ? `run ${runId} assigned (issue ${issueLabel ?? assignment.run.issue_id}); resuming run ${resume.previous_run_id} in ${workspace}` : `run ${runId} assigned (issue ${issueLabel ?? assignment.run.issue_id}); materializing workspace`
9871
9893
  );
@@ -9893,28 +9915,28 @@ async function runDaemon(opts) {
9893
9915
  `
9894
9916
  );
9895
9917
  for (const repo of resume ? [] : assignment.bundle.repos) {
9896
- if (run.settled) return table2.cleanup(run);
9918
+ if (run2.settled) return table2.cleanup(run2);
9897
9919
  const args = [
9898
9920
  "clone",
9899
9921
  ...repo.branch ? ["--branch", repo.branch] : [],
9900
9922
  repo.url,
9901
9923
  repo.dir
9902
9924
  ];
9903
- run.batcher.append(`$ git ${args.join(" ")}
9925
+ run2.batcher.append(`$ git ${args.join(" ")}
9904
9926
  `);
9905
- const result = await runGit(args, workspace, run.batcher);
9927
+ const result = await runGit(args, workspace, run2.batcher);
9906
9928
  if (result !== 0) {
9907
9929
  return table2.finishAndCleanup(
9908
- run,
9930
+ run2,
9909
9931
  "failed",
9910
9932
  `git clone failed for ${repo.url} (exit ${result})`
9911
9933
  );
9912
9934
  }
9913
9935
  }
9914
- if (run.settled) return table2.cleanup(run);
9936
+ if (run2.settled) return table2.cleanup(run2);
9915
9937
  const cli = await ensureCli();
9916
- if (run.settled) return table2.cleanup(run);
9917
- run.batcher.append(
9938
+ if (run2.settled) return table2.cleanup(run2);
9939
+ run2.batcher.append(
9918
9940
  cli.source === "fresh" ? `tines CLI: ${cli.version ?? "unknown"} (daemon-managed)
9919
9941
  ` : cli.source === "stale" ? `warning: agent CLI refresh failed; using last-good tines ${cli.version ?? "unknown"} from ${opts.configDir}/cli
9920
9942
  ` : `warning: no daemon-managed tines CLI; using whatever \`tines\` is on this machine's PATH
@@ -9932,10 +9954,10 @@ async function runDaemon(opts) {
9932
9954
  harnessInput
9933
9955
  );
9934
9956
  if (opts.harness === "codex") {
9935
- run.pricingModel = harnessInput.model;
9936
- run.pricingSessionMode = resume ? "resumed" : "cold";
9957
+ run2.pricingModel = harnessInput.model;
9958
+ run2.pricingSessionMode = resume ? "resumed" : "cold";
9937
9959
  }
9938
- run.batcher.append(
9960
+ run2.batcher.append(
9939
9961
  formatLaunchBanner(invocation, harnessInput, {
9940
9962
  harness: opts.harness,
9941
9963
  timeoutMinutes: assignment.timeout_minutes,
@@ -9953,86 +9975,86 @@ async function runDaemon(opts) {
9953
9975
  stdio: ["ignore", "pipe", "pipe"],
9954
9976
  detached: true
9955
9977
  });
9956
- run.child = child;
9957
- run.spawnedAt = Date.now();
9978
+ run2.child = child;
9979
+ run2.spawnedAt = Date.now();
9958
9980
  table2.persist();
9959
9981
  log(`run ${runId}: launched ${invocation.file} (pid ${child.pid})`);
9960
9982
  if (opts.harness === "claude_code") {
9961
9983
  const limiter = new RateLimitDetector();
9962
- run.limiter = limiter;
9984
+ run2.limiter = limiter;
9963
9985
  const renderer = new ClaudeStreamRenderer(
9964
- (line) => run.batcher.append(line),
9986
+ (line) => run2.batcher.append(line),
9965
9987
  (event) => limiter.noteStreamEvent(event)
9966
9988
  );
9967
- run.renderer = renderer;
9989
+ run2.renderer = renderer;
9968
9990
  const spoolPath = join5(opts.configDir, "rawlogs", `${runId}.ndjson`);
9969
9991
  mkdirSync4(dirname4(spoolPath), { recursive: true });
9970
- run.rawSpoolPath = spoolPath;
9971
- run.rawSpool = createWriteStream(spoolPath);
9972
- run.rawUpload = (body) => client2.putRunLogRaw(runId, body);
9992
+ run2.rawSpoolPath = spoolPath;
9993
+ run2.rawSpool = createWriteStream(spoolPath);
9994
+ run2.rawUpload = (body) => client2.putRunLogRaw(runId, body);
9973
9995
  const decoder = new StringDecoder("utf8");
9974
- run.drain = () => {
9996
+ run2.drain = () => {
9975
9997
  const trailing = decoder.end();
9976
9998
  if (trailing) renderer.write(trailing);
9977
9999
  renderer.finish();
9978
10000
  };
9979
10001
  child.stdout?.on("data", (data) => {
9980
- run.rawSpool?.write(data);
10002
+ run2.rawSpool?.write(data);
9981
10003
  renderer.write(decoder.write(data));
9982
10004
  });
9983
10005
  } else if (opts.harness === "codex") {
9984
- const renderer = new CodexStreamRenderer((line) => run.batcher.append(line));
10006
+ const renderer = new CodexStreamRenderer((line) => run2.batcher.append(line));
9985
10007
  const decoder = new StringDecoder("utf8");
9986
- run.renderer = renderer;
9987
- run.drain = () => {
10008
+ run2.renderer = renderer;
10009
+ run2.drain = () => {
9988
10010
  const trailing = decoder.end();
9989
10011
  if (trailing) renderer.write(trailing);
9990
10012
  renderer.finish();
9991
10013
  };
9992
10014
  child.stdout?.on("data", (data) => renderer.write(decoder.write(data)));
9993
10015
  } else {
9994
- child.stdout?.on("data", (data) => run.batcher.append(data.toString("utf8")));
10016
+ child.stdout?.on("data", (data) => run2.batcher.append(data.toString("utf8")));
9995
10017
  }
9996
10018
  child.stderr?.on("data", (data) => {
9997
10019
  const text4 = data.toString("utf8");
9998
- run.batcher.append(text4);
9999
- run.limiter?.noteStderr(text4);
10020
+ run2.batcher.append(text4);
10021
+ run2.limiter?.noteStderr(text4);
10000
10022
  });
10001
- run.timeout = setTimeout(() => {
10002
- if (run.settled) return;
10023
+ run2.timeout = setTimeout(() => {
10024
+ if (run2.settled) return;
10003
10025
  log(`run ${runId} hit its ${assignment.timeout_minutes}m timeout; killing`);
10004
- run.timedOut = true;
10026
+ run2.timedOut = true;
10005
10027
  if (child.pid) killTree(child.pid, "SIGTERM");
10006
10028
  if (child.pid) setTimeout(() => killTree(child.pid, "SIGKILL"), 5e3).unref?.();
10007
10029
  }, assignment.timeout_minutes * 6e4);
10008
10030
  child.on("error", (err) => {
10009
- void table2.finishAndCleanup(run, "failed", `failed to launch harness: ${message2(err)}`);
10031
+ void table2.finishAndCleanup(run2, "failed", `failed to launch harness: ${message2(err)}`);
10010
10032
  });
10011
10033
  child.on("close", (code, signal) => {
10012
- run.drain?.();
10013
- run.limiter?.finish();
10014
- const closing = exitLineForRun(run, {
10034
+ run2.drain?.();
10035
+ run2.limiter?.finish();
10036
+ const closing = exitLineForRun(run2, {
10015
10037
  code,
10016
10038
  signal,
10017
- durationMs: Date.now() - (run.spawnedAt ?? Date.now())
10039
+ durationMs: Date.now() - (run2.spawnedAt ?? Date.now())
10018
10040
  });
10019
- if (closing) run.batcher.append(closing);
10020
- if (run.timedOut) {
10041
+ if (closing) run2.batcher.append(closing);
10042
+ if (run2.timedOut) {
10021
10043
  void table2.finishAndCleanup(
10022
- run,
10044
+ run2,
10023
10045
  "failed",
10024
10046
  `run exceeded the ${assignment.timeout_minutes}m timeout; harness killed`
10025
10047
  );
10026
10048
  } else if (code === 0) {
10027
- void table2.finishAndCleanup(run, "completed");
10049
+ void table2.finishAndCleanup(run2, "completed");
10028
10050
  } else {
10029
- const limited = signal ? null : run.limiter?.signal() ?? null;
10030
- const providerError = signal ? null : run.limiter?.providerError() ?? null;
10051
+ const limited = signal ? null : run2.limiter?.signal() ?? null;
10052
+ const providerError = signal ? null : run2.limiter?.providerError() ?? null;
10031
10053
  if (limited) {
10032
10054
  log(
10033
10055
  `run ${runId}: harness rate limited (${limited.detail}); reporting without a strike`
10034
10056
  );
10035
- void table2.finishAndCleanup(run, "failed", `rate limited: ${limited.detail}`, {
10057
+ void table2.finishAndCleanup(run2, "failed", `rate limited: ${limited.detail}`, {
10036
10058
  judgment: "rate_limited",
10037
10059
  ...limited.resumeAt !== null ? { resume_at: limited.resumeAt } : {}
10038
10060
  });
@@ -10040,12 +10062,12 @@ async function runDaemon(opts) {
10040
10062
  log(
10041
10063
  `run ${runId}: transient provider error (${providerError.detail}); reporting without a strike`
10042
10064
  );
10043
- void table2.finishAndCleanup(run, "failed", `provider error: ${providerError.detail}`, {
10065
+ void table2.finishAndCleanup(run2, "failed", `provider error: ${providerError.detail}`, {
10044
10066
  judgment: "interrupted"
10045
10067
  });
10046
10068
  } else {
10047
10069
  void table2.finishAndCleanup(
10048
- run,
10070
+ run2,
10049
10071
  "failed",
10050
10072
  signal ? `harness killed by ${signal}` : `harness exited with code ${code}`
10051
10073
  );
@@ -10053,7 +10075,7 @@ async function runDaemon(opts) {
10053
10075
  }
10054
10076
  });
10055
10077
  } catch (err) {
10056
- void table2.finishAndCleanup(run, "failed", `workspace setup failed: ${message2(err)}`);
10078
+ void table2.finishAndCleanup(run2, "failed", `workspace setup failed: ${message2(err)}`);
10057
10079
  }
10058
10080
  };
10059
10081
  const shutdown = async () => {
@@ -10061,9 +10083,9 @@ async function runDaemon(opts) {
10061
10083
  shuttingDown = true;
10062
10084
  log("shutting down; failing in-flight runs");
10063
10085
  await Promise.all(
10064
- table2.values().map(async (run) => {
10065
- if (run.child?.pid) killTree(run.child.pid, "SIGTERM");
10066
- await table2.finishAndCleanup(run, "failed", "daemon shut down", {
10086
+ table2.values().map(async (run2) => {
10087
+ if (run2.child?.pid) killTree(run2.child.pid, "SIGTERM");
10088
+ await table2.finishAndCleanup(run2, "failed", "daemon shut down", {
10067
10089
  judgment: "interrupted"
10068
10090
  });
10069
10091
  })
@@ -10114,11 +10136,11 @@ async function runDaemon(opts) {
10114
10136
  return;
10115
10137
  }
10116
10138
  if (err instanceof ApiError && err.status === 401) {
10117
- for (const run of table2.values()) {
10118
- if (run.child?.pid) killTree(run.child.pid, "SIGKILL");
10119
- settleWorkspace(run.workspace, keepWorkspace(opts.keepWorkspaces, "failed"), {
10120
- run_id: run.runId,
10121
- ...run.issueLabel ? { issue_ref: run.issueLabel } : {},
10139
+ for (const run2 of table2.values()) {
10140
+ if (run2.child?.pid) killTree(run2.child.pid, "SIGKILL");
10141
+ settleWorkspace(run2.workspace, keepWorkspace(opts.keepWorkspaces, "failed"), {
10142
+ run_id: run2.runId,
10143
+ ...run2.issueLabel ? { issue_ref: run2.issueLabel } : {},
10122
10144
  status: "failed",
10123
10145
  error: "daemon token rejected"
10124
10146
  });
@@ -10160,6 +10182,480 @@ function runGit(args, cwd, batcher) {
10160
10182
  });
10161
10183
  }
10162
10184
 
10185
+ // src/commands/daemon-flags.ts
10186
+ import { hostname as hostname2 } from "node:os";
10187
+ function withDaemonFlags(cmd) {
10188
+ return cmd.option(
10189
+ "--name <name>",
10190
+ "runner name, unique per user; name it machine-plus-harness, e.g. macbook-claude (default: this hostname)"
10191
+ ).option("--harness <harness>", "claude-code | codex | custom", "claude-code").option(
10192
+ "--command <template>",
10193
+ "custom harness command template ({prompt_file}, {workspace}, {model})"
10194
+ ).option("--max-concurrent <n>", "maximum simultaneous runs", (v) => Number.parseInt(v, 10), 1).option("--poll-interval <seconds>", "seconds between polls", (v) => Number.parseInt(v, 10), 15).option(
10195
+ "--keep-workspaces <mode>",
10196
+ "keep settled runs' workspaces for debugging: never | failed | always",
10197
+ "never"
10198
+ ).option(
10199
+ "--keep-workspaces-for <hours>",
10200
+ "delete kept workspaces older than this",
10201
+ (v) => Number(v),
10202
+ 72
10203
+ ).option(
10204
+ "--keep-workspaces-max <n>",
10205
+ "keep at most this many workspaces (oldest removed first)",
10206
+ (v) => Number.parseInt(v, 10),
10207
+ 20
10208
+ );
10209
+ }
10210
+ function parseDaemonFlags(opts) {
10211
+ const harness = opts.harness.replaceAll("-", "_");
10212
+ if (!HARNESS_KINDS.includes(harness)) {
10213
+ die(`--harness must be claude-code, codex, or custom, got "${opts.harness}"`);
10214
+ }
10215
+ if (harness === "custom" && !opts.command) {
10216
+ die('the custom harness needs --command "<template>" ({prompt_file}, {workspace}, {model})');
10217
+ }
10218
+ if (harness !== "custom" && opts.command) die("--command only applies to --harness custom");
10219
+ if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent < 1 || opts.maxConcurrent > 100) {
10220
+ die("--max-concurrent must be an integer between 1 and 100");
10221
+ }
10222
+ if (!Number.isInteger(opts.pollInterval) || opts.pollInterval < 1) {
10223
+ die("--poll-interval must be a positive number of seconds");
10224
+ }
10225
+ const keepWorkspaces = opts.keepWorkspaces;
10226
+ if (!KEEP_WORKSPACES_MODES.includes(keepWorkspaces)) {
10227
+ die(
10228
+ `--keep-workspaces must be ${KEEP_WORKSPACES_MODES.join(", ")}, got "${opts.keepWorkspaces}"`
10229
+ );
10230
+ }
10231
+ if (!Number.isFinite(opts.keepWorkspacesFor) || opts.keepWorkspacesFor <= 0) {
10232
+ die("--keep-workspaces-for must be a positive number of hours");
10233
+ }
10234
+ if (!Number.isInteger(opts.keepWorkspacesMax) || opts.keepWorkspacesMax < 1) {
10235
+ die("--keep-workspaces-max must be a positive integer");
10236
+ }
10237
+ return {
10238
+ name: opts.name ?? hostname2(),
10239
+ harness,
10240
+ ...opts.command !== void 0 ? { command: opts.command } : {},
10241
+ maxConcurrent: opts.maxConcurrent,
10242
+ pollIntervalSeconds: opts.pollInterval,
10243
+ keepWorkspaces,
10244
+ keepWorkspacesForHours: opts.keepWorkspacesFor,
10245
+ keepWorkspacesMax: opts.keepWorkspacesMax
10246
+ };
10247
+ }
10248
+ function harnessFlag(harness) {
10249
+ return harness.replaceAll("_", "-");
10250
+ }
10251
+
10252
+ // src/commands/runner-service.ts
10253
+ import { execFile } from "node:child_process";
10254
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
10255
+ import { homedir as homedir2, hostname as hostname3, userInfo } from "node:os";
10256
+ import { dirname as dirname6 } from "node:path";
10257
+ import { promisify } from "node:util";
10258
+
10259
+ // src/daemon/service.ts
10260
+ import { existsSync as existsSync6, statSync as statSync5 } from "node:fs";
10261
+ import { delimiter as delimiter2, dirname as dirname5, join as join6, sep as sep2 } from "node:path";
10262
+ function defaultServiceManager(platform2 = process.platform) {
10263
+ if (platform2 === "darwin") return "launchd";
10264
+ if (platform2 === "linux") return "systemd";
10265
+ return null;
10266
+ }
10267
+ function serviceLabel(name2, kind) {
10268
+ return kind === "launchd" ? `dev.tines.runner.${name2}` : `tines-runner-${name2}`;
10269
+ }
10270
+ function unitPath(name2, kind, home) {
10271
+ const label = serviceLabel(name2, kind);
10272
+ return kind === "launchd" ? join6(home, "Library", "LaunchAgents", `${label}.plist`) : join6(home, ".config", "systemd", "user", `${label}.service`);
10273
+ }
10274
+ function serviceLogPath(configDir, name2) {
10275
+ return join6(configDir, "logs", `runner-${name2}.log`);
10276
+ }
10277
+ function managedBinary(prefix) {
10278
+ return join6(prefix, "node_modules", ".bin", "tines");
10279
+ }
10280
+ function daemonArgs(input2) {
10281
+ const args = [
10282
+ "runner",
10283
+ "daemon",
10284
+ "--url",
10285
+ input2.url,
10286
+ "--name",
10287
+ input2.name,
10288
+ "--harness",
10289
+ input2.harness
10290
+ ];
10291
+ if (input2.command !== void 0) args.push("--command", input2.command);
10292
+ if (input2.maxConcurrent !== 1) args.push("--max-concurrent", String(input2.maxConcurrent));
10293
+ if (input2.pollIntervalSeconds !== void 0 && input2.pollIntervalSeconds !== 15)
10294
+ args.push("--poll-interval", String(input2.pollIntervalSeconds));
10295
+ if (input2.keepWorkspaces !== void 0 && input2.keepWorkspaces !== "never")
10296
+ args.push("--keep-workspaces", input2.keepWorkspaces);
10297
+ if (input2.keepWorkspacesForHours !== void 0 && input2.keepWorkspacesForHours !== 72)
10298
+ args.push("--keep-workspaces-for", String(input2.keepWorkspacesForHours));
10299
+ if (input2.keepWorkspacesMax !== void 0 && input2.keepWorkspacesMax !== 20)
10300
+ args.push("--keep-workspaces-max", String(input2.keepWorkspacesMax));
10301
+ return args;
10302
+ }
10303
+ function findOnPath(name2, envPath) {
10304
+ for (const dir of (envPath ?? "").split(delimiter2)) {
10305
+ if (!dir) continue;
10306
+ const candidate = join6(dir, name2);
10307
+ try {
10308
+ if (statSync5(candidate).isFile()) return candidate;
10309
+ } catch {
10310
+ }
10311
+ }
10312
+ return null;
10313
+ }
10314
+ function servicePath(input2) {
10315
+ const dirs = [dirname5(input2.execPath)];
10316
+ const missing = [];
10317
+ for (const name2 of input2.required) {
10318
+ const found = findOnPath(name2, input2.envPath);
10319
+ if (found) dirs.push(dirname5(found));
10320
+ else missing.push(name2);
10321
+ }
10322
+ dirs.push(
10323
+ ...input2.platform === "darwin" ? ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"] : ["/usr/local/bin", "/usr/bin", "/bin"]
10324
+ );
10325
+ return { path: [...new Set(dirs)].join(delimiter2), missing };
10326
+ }
10327
+ function xml(value) {
10328
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
10329
+ }
10330
+ function renderLaunchdPlist(spec) {
10331
+ const label = serviceLabel(spec.name, "launchd");
10332
+ const env = { PATH: spec.path, ...spec.env };
10333
+ const lines2 = [
10334
+ '<?xml version="1.0" encoding="UTF-8"?>',
10335
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
10336
+ '<plist version="1.0">',
10337
+ "<dict>",
10338
+ ` <key>Label</key><string>${xml(label)}</string>`,
10339
+ " <key>ProgramArguments</key>",
10340
+ " <array>",
10341
+ ...[spec.binary, ...spec.args].map((arg) => ` <string>${xml(arg)}</string>`),
10342
+ " </array>",
10343
+ " <key>EnvironmentVariables</key>",
10344
+ " <dict>",
10345
+ ...Object.entries(env).map(
10346
+ ([key, value]) => ` <key>${xml(key)}</key><string>${xml(value)}</string>`
10347
+ ),
10348
+ " </dict>",
10349
+ " <key>RunAtLoad</key><true/>",
10350
+ " <!-- Relaunches after any exit: a crash, and the daemon's own exit for a self-update. -->",
10351
+ " <key>KeepAlive</key><true/>",
10352
+ ` <key>StandardOutPath</key><string>${xml(spec.logPath)}</string>`,
10353
+ ` <key>StandardErrorPath</key><string>${xml(spec.logPath)}</string>`,
10354
+ "</dict>",
10355
+ "</plist>",
10356
+ ""
10357
+ ];
10358
+ return lines2.join("\n");
10359
+ }
10360
+ function unitQuote(value) {
10361
+ return /^[A-Za-z0-9_./:=@%+-]+$/.test(value) ? value : `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
10362
+ }
10363
+ function renderSystemdUnit(spec) {
10364
+ const env = { PATH: spec.path, ...spec.env };
10365
+ const lines2 = [
10366
+ "[Unit]",
10367
+ `Description=Tines local runner daemon (${spec.name})`,
10368
+ "After=network-online.target",
10369
+ "",
10370
+ "[Service]",
10371
+ `ExecStart=${[spec.binary, ...spec.args].map(unitQuote).join(" ")}`,
10372
+ ...Object.entries(env).map(([key, value]) => `Environment=${unitQuote(`${key}=${value}`)}`),
10373
+ `StandardOutput=append:${spec.logPath}`,
10374
+ `StandardError=append:${spec.logPath}`,
10375
+ "# always, not on-failure: the daemon exits 0 on purpose to pick up a self-update.",
10376
+ "Restart=always",
10377
+ "RestartSec=10",
10378
+ "",
10379
+ "[Install]",
10380
+ "WantedBy=default.target",
10381
+ ""
10382
+ ];
10383
+ return lines2.join("\n");
10384
+ }
10385
+ function renderUnit(spec) {
10386
+ return spec.kind === "launchd" ? renderLaunchdPlist(spec) : renderSystemdUnit(spec);
10387
+ }
10388
+ function serviceCommands(target) {
10389
+ const label = serviceLabel(target.name, target.kind);
10390
+ if (target.kind === "launchd") {
10391
+ const domain = `gui/${target.uid}`;
10392
+ return {
10393
+ // bootout first so a re-install replaces the loaded definition
10394
+ // instead of failing with "service already loaded".
10395
+ load: [
10396
+ { argv: ["launchctl", "bootout", `${domain}/${label}`], optional: true },
10397
+ { argv: ["launchctl", "bootstrap", domain, target.unitPath] }
10398
+ ],
10399
+ restart: [{ argv: ["launchctl", "kickstart", "-k", `${domain}/${label}`] }],
10400
+ unload: [{ argv: ["launchctl", "bootout", `${domain}/${label}`], optional: true }]
10401
+ };
10402
+ }
10403
+ return {
10404
+ load: [
10405
+ { argv: ["systemctl", "--user", "daemon-reload"] },
10406
+ { argv: ["systemctl", "--user", "enable", "--now", label] },
10407
+ // So the runner keeps working while nobody is logged in.
10408
+ { argv: ["loginctl", "enable-linger"], optional: true }
10409
+ ],
10410
+ restart: [{ argv: ["systemctl", "--user", "restart", label] }],
10411
+ unload: [
10412
+ { argv: ["systemctl", "--user", "disable", "--now", label], optional: true },
10413
+ { argv: ["systemctl", "--user", "daemon-reload"], optional: true }
10414
+ ]
10415
+ };
10416
+ }
10417
+ function matchDaemonProcesses(psOutput, query2) {
10418
+ const matches = [];
10419
+ for (const raw of psOutput.split("\n")) {
10420
+ const line = raw.trim();
10421
+ const m = /^(\d+)\s+(.*)$/.exec(line);
10422
+ if (!m) continue;
10423
+ const pid = Number(m[1]);
10424
+ const args = m[2];
10425
+ if (pid === query2.selfPid) continue;
10426
+ const words = args.split(/\s+/);
10427
+ const daemonAt = words.findIndex((w, i) => w === "runner" && words[i + 1] === "daemon");
10428
+ if (daemonAt < 0) continue;
10429
+ const binary = words[daemonAt - 1] ?? "";
10430
+ if (!/(^|\/)tines(\.js|\.cjs|\.mjs)?$/.test(binary) && !/(^|\/)tines$/.test(binary)) continue;
10431
+ let name2 = null;
10432
+ for (let i = daemonAt + 2; i < words.length; i++) {
10433
+ const w = words[i];
10434
+ if (w === "--name") name2 = words[i + 1] ?? null;
10435
+ else if (w.startsWith("--name=")) name2 = w.slice("--name=".length);
10436
+ }
10437
+ if ((name2 ?? query2.hostname) !== query2.name) continue;
10438
+ const root = query2.prefix.endsWith(sep2) ? query2.prefix : query2.prefix + sep2;
10439
+ matches.push({ pid, args, managed: args.includes(root) });
10440
+ }
10441
+ return matches;
10442
+ }
10443
+ function daemonStartedLine(name2) {
10444
+ const escaped = name2.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
10445
+ return new RegExp(`(reconnecting as runner|registered runner) "${escaped}"`);
10446
+ }
10447
+ function managedBinaryPresent(prefix) {
10448
+ return existsSync6(managedBinary(prefix));
10449
+ }
10450
+
10451
+ // src/commands/runner-service.ts
10452
+ var run = promisify(execFile);
10453
+ var STARTUP_TIMEOUT_MS = 3e4;
10454
+ var log2 = (message3) => console.log(message3);
10455
+ function resolveServiceManager(flag) {
10456
+ if (flag !== void 0) {
10457
+ if (flag !== "launchd" && flag !== "systemd")
10458
+ die(`--service-manager must be launchd or systemd, got "${flag}"`);
10459
+ return flag;
10460
+ }
10461
+ const kind = defaultServiceManager();
10462
+ if (!kind) {
10463
+ die(
10464
+ `no service manager is known for ${process.platform}; run \`tines runner daemon\` under your own supervisor instead`
10465
+ );
10466
+ }
10467
+ return kind;
10468
+ }
10469
+ async function runServiceCommands(steps) {
10470
+ for (const step of steps) {
10471
+ try {
10472
+ await run(step.argv[0], step.argv.slice(1), { timeout: 3e4 });
10473
+ } catch (err) {
10474
+ if (step.optional) continue;
10475
+ const detail = err && typeof err === "object" && "stderr" in err && String(err.stderr).trim() ? `: ${String(err.stderr).trim()}` : err instanceof Error ? `: ${err.message}` : "";
10476
+ die(`\`${step.argv.join(" ")}\` failed${detail}`);
10477
+ }
10478
+ }
10479
+ }
10480
+ async function listProcesses() {
10481
+ try {
10482
+ const { stdout } = await run("ps", ["-axo", "pid=,args="], { maxBuffer: 16 * 1024 * 1024 });
10483
+ return stdout;
10484
+ } catch {
10485
+ return null;
10486
+ }
10487
+ }
10488
+ function fileSize(path2) {
10489
+ try {
10490
+ return statSync6(path2).size;
10491
+ } catch {
10492
+ return 0;
10493
+ }
10494
+ }
10495
+ async function waitForStart(logPath, offset, name2) {
10496
+ const pattern = daemonStartedLine(name2);
10497
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
10498
+ while (Date.now() < deadline) {
10499
+ if (existsSync7(logPath)) {
10500
+ const content = readFileSync10(logPath, "utf8").slice(offset);
10501
+ if (pattern.test(content)) return true;
10502
+ }
10503
+ await new Promise((resolve) => setTimeout(resolve, 500));
10504
+ }
10505
+ return false;
10506
+ }
10507
+ function serviceTarget(name2, kind) {
10508
+ return { kind, name: name2, unitPath: unitPath(name2, kind, homedir2()), uid: userInfo().uid };
10509
+ }
10510
+ function registerServiceCommands(runnerCmd) {
10511
+ withCommon(
10512
+ withDaemonFlags(
10513
+ runnerCmd.command("install").description(
10514
+ "Register this machine as a runner and install the daemon as a launchd/systemd service that keeps itself updated"
10515
+ )
10516
+ ).option("--service-manager <kind>", "launchd | systemd (default: the one this OS ships)")
10517
+ ).action(async (opts) => {
10518
+ const settings = parseDaemonFlags(opts);
10519
+ const kind = resolveServiceManager(opts.serviceManager);
10520
+ const baseUrl = resolveUrl(opts).replace(/\/+$/, "");
10521
+ const configDir = defaultConfigDir();
10522
+ mkdirSync5(configDir, { recursive: true });
10523
+ const prefix = agentCliPrefix(configDir);
10524
+ const ps = await listProcesses();
10525
+ const running = ps ? matchDaemonProcesses(ps, {
10526
+ name: settings.name,
10527
+ hostname: hostname3(),
10528
+ prefix,
10529
+ selfPid: process.pid
10530
+ }) : [];
10531
+ if (running.length > 0) {
10532
+ const label = serviceLabel(settings.name, kind);
10533
+ const lines2 = running.map((p) => ` pid ${p.pid}: ${p.args}`);
10534
+ die(
10535
+ running.every((p) => p.managed) ? `"${settings.name}" is already running under its service (${label}):
10536
+ ${lines2.join("\n")}
10537
+ to change its flags: \`tines runners pause ${settings.name}\`, wait for 0 active runs, \`tines runner uninstall ${settings.name}\`, then install again \u2014 or \`tines runner restart ${settings.name}\` to relaunch it as is` : `a daemon for "${settings.name}" is already running outside the service:
10538
+ ${lines2.join("\n")}
10539
+ stop it first \u2014 \`tines runners pause ${settings.name}\`, wait for 0 active runs, kill that process \u2014 then run install again; it reconnects with the stored token, so no key is needed`
10540
+ );
10541
+ }
10542
+ if (!managedBinaryPresent(prefix)) {
10543
+ log2(`installing the daemon-managed tines into ${prefix} \u2026`);
10544
+ await installAgentCli({ configDir, log: log2 });
10545
+ if (!managedBinaryPresent(prefix)) {
10546
+ die(
10547
+ `could not install tines into ${prefix}; the service needs that copy (npm and network access required)`
10548
+ );
10549
+ }
10550
+ }
10551
+ let ensured;
10552
+ try {
10553
+ ensured = await ensureRunnerCredentials({
10554
+ configDir,
10555
+ baseUrl,
10556
+ name: settings.name,
10557
+ harness: settings.harness,
10558
+ ...settings.command !== void 0 ? { command: settings.command } : {},
10559
+ maxConcurrent: settings.maxConcurrent,
10560
+ ...resolveApiKey(opts) !== void 0 ? { apiKey: resolveApiKey(opts) } : {},
10561
+ log: log2
10562
+ });
10563
+ } catch (err) {
10564
+ die(err instanceof Error ? err.message : String(err));
10565
+ }
10566
+ const harnessBinary = settings.harness === "claude_code" ? "claude" : settings.harness === "codex" ? "codex" : null;
10567
+ const { path: path2, missing } = servicePath({
10568
+ execPath: process.execPath,
10569
+ required: [...harnessBinary ? [harnessBinary] : [], "git"],
10570
+ envPath: process.env.PATH,
10571
+ platform: process.platform
10572
+ });
10573
+ for (const name2 of missing) {
10574
+ log2(
10575
+ `warning: \`${name2}\` is not on this shell's PATH, so it is not on the service's either \u2014 install it, then \`tines runner install\` again`
10576
+ );
10577
+ }
10578
+ const target = serviceTarget(settings.name, kind);
10579
+ const logPath = serviceLogPath(configDir, settings.name);
10580
+ const spec = {
10581
+ name: settings.name,
10582
+ kind,
10583
+ binary: managedBinary(prefix),
10584
+ args: daemonArgs({
10585
+ url: baseUrl,
10586
+ name: settings.name,
10587
+ harness: harnessFlag(settings.harness),
10588
+ ...settings.command !== void 0 ? { command: settings.command } : {},
10589
+ maxConcurrent: settings.maxConcurrent,
10590
+ pollIntervalSeconds: settings.pollIntervalSeconds,
10591
+ keepWorkspaces: settings.keepWorkspaces,
10592
+ keepWorkspacesForHours: settings.keepWorkspacesForHours,
10593
+ keepWorkspacesMax: settings.keepWorkspacesMax
10594
+ }),
10595
+ path: path2,
10596
+ logPath,
10597
+ env: process.env.TINES_CONFIG_DIR ? { TINES_CONFIG_DIR: process.env.TINES_CONFIG_DIR } : {}
10598
+ };
10599
+ mkdirSync5(dirname6(target.unitPath), { recursive: true });
10600
+ mkdirSync5(dirname6(logPath), { recursive: true });
10601
+ writeFileSync4(target.unitPath, renderUnit(spec), { mode: 420 });
10602
+ log2(`wrote ${target.unitPath}`);
10603
+ const offset = fileSize(logPath);
10604
+ await runServiceCommands(serviceCommands(target).load);
10605
+ log2(`loaded ${serviceLabel(settings.name, kind)}; waiting for the daemon to report in \u2026`);
10606
+ const started = await waitForStart(logPath, offset, settings.name);
10607
+ const summary = {
10608
+ name: settings.name,
10609
+ runner_id: ensured.creds.runner_id,
10610
+ registered: ensured.registered,
10611
+ service_manager: kind,
10612
+ unit_path: target.unitPath,
10613
+ log_path: logPath,
10614
+ started
10615
+ };
10616
+ if (!started) {
10617
+ if (opts.json) printJson(summary);
10618
+ die(
10619
+ `the service is loaded but the daemon has not reported in after ${STARTUP_TIMEOUT_MS / 1e3}s \u2014 read ${logPath} (a missing \`node\` on the service PATH, or a rejected token, shows there)`
10620
+ );
10621
+ }
10622
+ if (opts.json) return printJson(summary);
10623
+ log2(
10624
+ `runner "${settings.name}" is up under ${kind} and updates itself: log at ${logPath}; \`tines runner restart ${settings.name}\` relaunches it, \`tines runner uninstall ${settings.name}\` removes the service`
10625
+ );
10626
+ if (ensured.registered) log2(nextStepsMessage(settings.name, baseUrl));
10627
+ });
10628
+ runnerCmd.command("restart [name]").description(
10629
+ "Relaunch an installed runner service (interrupts runs in flight: pause the runner and wait for 0 active runs first if that matters)"
10630
+ ).option("--service-manager <kind>", "launchd | systemd (default: the one this OS ships)").option("--json", "print JSON instead of a message").action(async (name2, opts) => {
10631
+ const kind = resolveServiceManager(opts.serviceManager);
10632
+ const target = serviceTarget(name2 ?? hostname3(), kind);
10633
+ if (!existsSync7(target.unitPath)) {
10634
+ die(
10635
+ `"${target.name}" is not installed as a service (no ${target.unitPath}); run \`tines runner install --name ${target.name} \u2026\` first`
10636
+ );
10637
+ }
10638
+ await runServiceCommands(serviceCommands(target).restart);
10639
+ if (opts.json)
10640
+ return printJson({ name: target.name, service_manager: kind, restarted: true });
10641
+ log2(`restarted ${serviceLabel(target.name, kind)}`);
10642
+ });
10643
+ runnerCmd.command("uninstall [name]").description(
10644
+ "Stop and remove a runner's service unit (the stored runner token is kept, so a later install reconnects)"
10645
+ ).option("--service-manager <kind>", "launchd | systemd (default: the one this OS ships)").option("--json", "print JSON instead of a message").action(async (name2, opts) => {
10646
+ const kind = resolveServiceManager(opts.serviceManager);
10647
+ const target = serviceTarget(name2 ?? hostname3(), kind);
10648
+ const existed = existsSync7(target.unitPath);
10649
+ await runServiceCommands(serviceCommands(target).unload);
10650
+ rmSync4(target.unitPath, { force: true });
10651
+ if (opts.json)
10652
+ return printJson({ name: target.name, service_manager: kind, removed: existed });
10653
+ log2(
10654
+ existed ? `removed ${serviceLabel(target.name, kind)} (${target.unitPath}); the runner token is still stored, so \`tines runner install\` reconnects without a key` : `no service unit for "${target.name}" at ${target.unitPath}; nothing to remove`
10655
+ );
10656
+ });
10657
+ }
10658
+
10163
10659
  // src/commands/runners.ts
10164
10660
  function parseBoolean(value) {
10165
10661
  if (value === "true") return true;
@@ -10455,87 +10951,38 @@ function register8(program3) {
10455
10951
  });
10456
10952
  const runnerCmd = program3.command("runner").description("The local runner daemon");
10457
10953
  withCommon(
10458
- runnerCmd.command("daemon").description(
10459
- "Run the local runner daemon: register/reconnect, poll for assigned runs, execute them"
10460
- ).option(
10461
- "--name <name>",
10462
- "runner name, unique per user; name it machine-plus-harness, e.g. macbook-claude (default: this hostname)"
10463
- ).option("--harness <harness>", "claude-code | codex | custom", "claude-code").option(
10464
- "--command <template>",
10465
- "custom harness command template ({prompt_file}, {workspace}, {model})"
10466
- ).option("--max-concurrent <n>", "maximum simultaneous runs", (v) => Number.parseInt(v, 10), 1).option(
10467
- "--poll-interval <seconds>",
10468
- "seconds between polls",
10469
- (v) => Number.parseInt(v, 10),
10470
- 15
10954
+ withDaemonFlags(
10955
+ runnerCmd.command("daemon").description(
10956
+ "Run the local runner daemon in the foreground: register/reconnect, poll for assigned runs, execute them (`tines runner install` runs it as a service instead)"
10957
+ )
10471
10958
  ).option(
10472
10959
  "--no-cli-refresh",
10473
10960
  "do not install/refresh the agent-facing tines CLI from npm (harnesses use the ambient PATH)"
10474
10961
  ).option(
10475
10962
  "--no-self-update",
10476
10963
  "do not exit for the service manager to relaunch a newer daemon (only applies when launched from the daemon-managed prefix)"
10477
- ).option(
10478
- "--keep-workspaces <mode>",
10479
- "keep settled runs' workspaces for debugging: never | failed | always",
10480
- "never"
10481
- ).option(
10482
- "--keep-workspaces-for <hours>",
10483
- "delete kept workspaces older than this",
10484
- (v) => Number(v),
10485
- 72
10486
- ).option(
10487
- "--keep-workspaces-max <n>",
10488
- "keep at most this many workspaces (oldest removed first)",
10489
- (v) => Number.parseInt(v, 10),
10490
- 20
10491
10964
  )
10492
10965
  ).action(
10493
10966
  async (opts) => {
10494
- const harness = opts.harness.replaceAll("-", "_");
10495
- if (!HARNESS_KINDS.includes(harness)) {
10496
- die(`--harness must be claude-code, codex, or custom, got "${opts.harness}"`);
10497
- }
10498
- if (harness === "custom" && !opts.command) {
10499
- die(
10500
- 'the custom harness needs --command "<template>" ({prompt_file}, {workspace}, {model})'
10501
- );
10502
- }
10503
- if (harness !== "custom" && opts.command) die("--command only applies to --harness custom");
10504
- if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent < 1 || opts.maxConcurrent > 100) {
10505
- die("--max-concurrent must be an integer between 1 and 100");
10506
- }
10507
- if (!Number.isInteger(opts.pollInterval) || opts.pollInterval < 1) {
10508
- die("--poll-interval must be a positive number of seconds");
10509
- }
10510
- const keepWorkspaces = opts.keepWorkspaces;
10511
- if (!KEEP_WORKSPACES_MODES.includes(keepWorkspaces)) {
10512
- die(
10513
- `--keep-workspaces must be ${KEEP_WORKSPACES_MODES.join(", ")}, got "${opts.keepWorkspaces}"`
10514
- );
10515
- }
10516
- if (!Number.isFinite(opts.keepWorkspacesFor) || opts.keepWorkspacesFor <= 0) {
10517
- die("--keep-workspaces-for must be a positive number of hours");
10518
- }
10519
- if (!Number.isInteger(opts.keepWorkspacesMax) || opts.keepWorkspacesMax < 1) {
10520
- die("--keep-workspaces-max must be a positive integer");
10521
- }
10967
+ const settings = parseDaemonFlags(opts);
10522
10968
  await runDaemon({
10523
10969
  url: resolveUrl(opts).replace(/\/+$/, ""),
10524
10970
  apiKey: resolveApiKey(opts),
10525
- name: opts.name ?? hostname2(),
10526
- harness,
10527
- command: opts.command,
10528
- maxConcurrent: opts.maxConcurrent,
10529
- pollIntervalMs: opts.pollInterval * 1e3,
10971
+ name: settings.name,
10972
+ harness: settings.harness,
10973
+ command: settings.command,
10974
+ maxConcurrent: settings.maxConcurrent,
10975
+ pollIntervalMs: settings.pollIntervalSeconds * 1e3,
10530
10976
  configDir: defaultConfigDir(),
10531
10977
  cliRefresh: opts.cliRefresh,
10532
10978
  selfUpdate: opts.selfUpdate,
10533
- keepWorkspaces,
10534
- keepWorkspacesForHours: opts.keepWorkspacesFor,
10535
- keepWorkspacesMax: opts.keepWorkspacesMax
10979
+ keepWorkspaces: settings.keepWorkspaces,
10980
+ keepWorkspacesForHours: settings.keepWorkspacesForHours,
10981
+ keepWorkspacesMax: settings.keepWorkspacesMax
10536
10982
  });
10537
10983
  }
10538
10984
  );
10985
+ registerServiceCommands(runnerCmd);
10539
10986
  const workspacesCmd = runnerCmd.command("workspaces").description("List run workspaces the daemon kept for debugging (--keep-workspaces)").option("--json", "print JSON instead of a table").action((opts) => {
10540
10987
  const configDir = defaultConfigDir();
10541
10988
  const kept = listKeptWorkspaces(configDir);
@@ -10622,14 +11069,14 @@ function register8(program3) {
10622
11069
  if (evidence.population === "pending") {
10623
11070
  table([
10624
11071
  ["ID", "ISSUE", "RUNNER", "TIER", "STATUS", "COST", "CREATED"],
10625
- ...items.map((run) => [
10626
- run.id,
10627
- run.issue_ref ? `${run.issue_ref.project_name}/${run.issue_ref.number}` : run.issue_id,
10628
- run.runner_name,
10629
- run.tier,
11072
+ ...items.map((run2) => [
11073
+ run2.id,
11074
+ run2.issue_ref ? `${run2.issue_ref.project_name}/${run2.issue_ref.number}` : run2.issue_id,
11075
+ run2.runner_name,
11076
+ run2.tier,
10630
11077
  "Pending at cutoff",
10631
11078
  "\u2014",
10632
- timestamp(run.created_at)
11079
+ timestamp(run2.created_at)
10633
11080
  ])
10634
11081
  ]);
10635
11082
  return;
@@ -10645,29 +11092,29 @@ function register8(program3) {
10645
11092
  ).action(
10646
11093
  async (id2, opts) => {
10647
11094
  const api = client(opts);
10648
- const run = await api.getRun(id2);
10649
- if (opts.json) return printJson(run);
11095
+ const run2 = await api.getRun(id2);
11096
+ if (opts.json) return printJson(run2);
10650
11097
  console.log(
10651
- `${run.id} ${run.status} on ${run.runner_name}${run.resumed_from_run_id ? ` \xB7 resumed run ${run.resumed_from_run_id}` : ""}`
11098
+ `${run2.id} ${run2.status} on ${run2.runner_name}${run2.resumed_from_run_id ? ` \xB7 resumed run ${run2.resumed_from_run_id}` : ""}`
10652
11099
  );
10653
- if (run.issue_ref) console.log(`issue: ${issueRef(run.issue_ref)} \u2014 ${run.issue_ref.title}`);
10654
- console.log(`tier: ${run.tier} model: ${run.model ?? "(n/a)"}`);
11100
+ if (run2.issue_ref) console.log(`issue: ${issueRef(run2.issue_ref)} \u2014 ${run2.issue_ref.title}`);
11101
+ console.log(`tier: ${run2.tier} model: ${run2.model ?? "(n/a)"}`);
10655
11102
  console.log(
10656
- `states: ${run.state_at_start_name ?? run.state_id_at_start} \u2192 ${run.state_at_end_name ?? run.state_id_at_end ?? "\u2026"}`
11103
+ `states: ${run2.state_at_start_name ?? run2.state_id_at_start} \u2192 ${run2.state_at_end_name ?? run2.state_id_at_end ?? "\u2026"}`
10657
11104
  );
10658
11105
  console.log(
10659
- `created: ${timestamp(run.created_at)} started: ${run.started_at ? timestamp(run.started_at) : "\u2014"} ended: ${run.ended_at ? timestamp(run.ended_at) : "\u2014"} duration: ${runDurationLabel(run)}`
11106
+ `created: ${timestamp(run2.created_at)} started: ${run2.started_at ? timestamp(run2.started_at) : "\u2014"} ended: ${run2.ended_at ? timestamp(run2.ended_at) : "\u2014"} duration: ${runDurationLabel(run2)}`
10660
11107
  );
10661
- if (run.usage) {
10662
- const u = run.usage;
10663
- const costLabel = runCostLabel(run);
11108
+ if (run2.usage) {
11109
+ const u = run2.usage;
11110
+ const costLabel = runCostLabel(run2);
10664
11111
  const metric = (value) => value === void 0 ? "unknown" : value.toLocaleString();
10665
11112
  console.log(
10666
11113
  `usage: input ${metric(u.input_tokens)} cache-read ${metric(u.cache_read_tokens)} cache-write ${metric(u.cache_write_tokens)} output ${metric(u.output_tokens)}`
10667
11114
  );
10668
11115
  if (u.cost_usd !== void 0)
10669
11116
  console.log(
10670
- `cost: ${runCostLabel(run)}${u.cost_source === void 0 ? " \xB7 source unavailable" : ""}`
11117
+ `cost: ${runCostLabel(run2)}${u.cost_source === void 0 ? " \xB7 source unavailable" : ""}`
10671
11118
  );
10672
11119
  const pricing = u.pricing;
10673
11120
  if (pricing?.status === "provider_authoritative")
@@ -10692,9 +11139,9 @@ function register8(program3) {
10692
11139
  console.log(" Standard API list-price estimate; not an invoice or subscription usage.");
10693
11140
  }
10694
11141
  }
10695
- if (run.provider_session_id) console.log(`provider session: ${run.provider_session_id}`);
10696
- if (run.provider_url) console.log(`provider console: ${run.provider_url}`);
10697
- if (run.error) console.log(`error: ${run.error}`);
11142
+ if (run2.provider_session_id) console.log(`provider session: ${run2.provider_session_id}`);
11143
+ if (run2.provider_url) console.log(`provider console: ${run2.provider_url}`);
11144
+ if (run2.error) console.log(`error: ${run2.error}`);
10698
11145
  if (opts.logs) {
10699
11146
  console.log("");
10700
11147
  if (opts.full || opts.raw) {
@@ -10711,12 +11158,12 @@ function register8(program3) {
10711
11158
  process.stdout.write(decoder.decode());
10712
11159
  return;
10713
11160
  }
10714
- if (run.log_bytes_dropped > 0) {
11161
+ if (run2.log_bytes_dropped > 0) {
10715
11162
  console.log(
10716
- `[${Math.round(run.log_bytes_dropped / 1024)} KB truncated from the head \u2014 ` + (run.log_expired ? "past its retention window; only this tail remains]" : `run \`tines runs show ${run.id} --logs --full\` for the complete ${Math.round(run.log_full_bytes / 1024)} KB log]`)
11163
+ `[${Math.round(run2.log_bytes_dropped / 1024)} KB truncated from the head \u2014 ` + (run2.log_expired ? "past its retention window; only this tail remains]" : `run \`tines runs show ${run2.id} --logs --full\` for the complete ${Math.round(run2.log_full_bytes / 1024)} KB log]`)
10717
11164
  );
10718
11165
  }
10719
- console.log(run.log || "(no log output captured)");
11166
+ console.log(run2.log || "(no log output captured)");
10720
11167
  }
10721
11168
  }
10722
11169
  );
@@ -10724,10 +11171,10 @@ function register8(program3) {
10724
11171
  runsCmd.command("cancel <id>").description("Cancel a run (judged like any other end: usually a strike)")
10725
11172
  ).action(async (id2, opts) => {
10726
11173
  const api = client(opts);
10727
- const run = await api.cancelRun(id2);
10728
- if (opts.json) return printJson(run);
11174
+ const run2 = await api.cancelRun(id2);
11175
+ if (opts.json) return printJson(run2);
10729
11176
  console.log(
10730
- `canceled ${run.id}${run.issue_ref ? ` on ${issueRef(run.issue_ref)}` : ""} (was on ${run.runner_name})`
11177
+ `canceled ${run2.id}${run2.issue_ref ? ` on ${issueRef(run2.issue_ref)}` : ""} (was on ${run2.runner_name})`
10731
11178
  );
10732
11179
  });
10733
11180
  }
@@ -11104,12 +11551,12 @@ function register10(program3) {
11104
11551
  const api = client(opts);
11105
11552
  const overrides = {};
11106
11553
  for (const spec of opts.state) {
11107
- const sep2 = spec.lastIndexOf("=");
11108
- if (sep2 < 1 || sep2 === spec.length - 1) {
11554
+ const sep3 = spec.lastIndexOf("=");
11555
+ if (sep3 < 1 || sep3 === spec.length - 1) {
11109
11556
  die(`--state must look like <workflow>/<state>=<n>, got "${spec}"`);
11110
11557
  }
11111
- const limit = Number.parseInt(spec.slice(sep2 + 1), 10);
11112
- const { state } = await resolveStateFlag(api, spec.slice(0, sep2));
11558
+ const limit = Number.parseInt(spec.slice(sep3 + 1), 10);
11559
+ const { state } = await resolveStateFlag(api, spec.slice(0, sep3));
11113
11560
  overrides[state.id] = limit;
11114
11561
  }
11115
11562
  const settings = await api.updateSupervisorSettings({
@@ -11126,17 +11573,17 @@ function register10(program3) {
11126
11573
  }
11127
11574
 
11128
11575
  // src/commands/workflows.ts
11129
- import { readFileSync as readFileSync11 } from "node:fs";
11576
+ import { readFileSync as readFileSync12 } from "node:fs";
11130
11577
 
11131
11578
  // src/workflow-packages.ts
11132
11579
  import { createInterface as createInterface4 } from "node:readline/promises";
11133
11580
  import { createHash } from "node:crypto";
11134
- import { readFileSync as readFileSync10 } from "node:fs";
11581
+ import { readFileSync as readFileSync11 } from "node:fs";
11135
11582
  var PACKAGE_STDIN = "-";
11136
11583
  var MAX_LOCAL_JSON_BYTES = 32 * 1024 * 1024;
11137
11584
  function readPackageSource(path2) {
11138
11585
  try {
11139
- const bytes2 = readFileSync10(path2 === PACKAGE_STDIN ? 0 : path2);
11586
+ const bytes2 = readFileSync11(path2 === PACKAGE_STDIN ? 0 : path2);
11140
11587
  if (bytes2.byteLength > MAX_LOCAL_JSON_BYTES)
11141
11588
  throw new Error(`input exceeds ${MAX_LOCAL_JSON_BYTES} bytes`);
11142
11589
  return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2);
@@ -11480,14 +11927,14 @@ function readJsonBody(inline, file) {
11480
11927
  if (file !== void 0 && file !== "-") {
11481
11928
  let raw;
11482
11929
  try {
11483
- raw = readFileSync11(file, "utf8");
11930
+ raw = readFileSync12(file, "utf8");
11484
11931
  } catch (err) {
11485
11932
  die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
11486
11933
  }
11487
11934
  return parseJsonObject(raw, file);
11488
11935
  }
11489
11936
  if (file === "-" || !process.stdin.isTTY) {
11490
- const raw = readFileSync11(0, "utf8");
11937
+ const raw = readFileSync12(0, "utf8");
11491
11938
  if (raw.trim() === "") {
11492
11939
  if (file === "-") die("no JSON on stdin");
11493
11940
  return void 0;
@@ -11596,9 +12043,9 @@ async function resolveStateBases(states, library) {
11596
12043
  }
11597
12044
  }
11598
12045
  function resolveBasePair(lib, pair, stateName) {
11599
- const sep2 = pair.indexOf("/");
11600
- const workflowRef = pair.slice(0, sep2).trim();
11601
- const stateRef2 = pair.slice(sep2 + 1).trim();
12046
+ const sep3 = pair.indexOf("/");
12047
+ const workflowRef = pair.slice(0, sep3).trim();
12048
+ const stateRef2 = pair.slice(sep3 + 1).trim();
11602
12049
  const where = `state "${stateName}" inherits from "${pair}"`;
11603
12050
  if (!workflowRef || !stateRef2) {
11604
12051
  die(`${where}, which is not a <workflow>/<state> pair`);