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.
- package/README.md +5 -3
- package/dist/index.js +742 -295
- 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(
|
|
3411
|
-
return
|
|
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(
|
|
3534
|
-
if (!
|
|
3535
|
-
const seconds = Math.max(0, Math.round(((
|
|
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(
|
|
3539
|
-
const 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
|
|
3563
|
-
const entry = counts.get(
|
|
3564
|
-
name:
|
|
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(
|
|
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,
|
|
3591
|
-
return Array.isArray(v) ? v.join(
|
|
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
|
|
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(
|
|
5641
|
-
const cost = runCostLabel(
|
|
5640
|
+
function runHeadline(run2, now) {
|
|
5641
|
+
const cost = runCostLabel(run2);
|
|
5642
5642
|
return [
|
|
5643
|
-
`${
|
|
5644
|
-
runDurationLabel(
|
|
5645
|
-
|
|
5643
|
+
`${run2.run_id} on ${run2.runner_name}`,
|
|
5644
|
+
runDurationLabel(run2, now),
|
|
5645
|
+
run2.outcome ?? run2.status,
|
|
5646
5646
|
...cost ? [cost] : [],
|
|
5647
|
-
|
|
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(
|
|
5651
|
-
const back =
|
|
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 `${
|
|
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(
|
|
5672
|
+
function runRow(run2) {
|
|
5673
5673
|
return [
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
`${
|
|
5678
|
-
`${
|
|
5679
|
-
runDurationLabel(
|
|
5680
|
-
runCostLabel(
|
|
5681
|
-
timestamp(
|
|
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
|
|
5755
|
-
if (
|
|
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,
|
|
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
|
|
5767
|
-
if (
|
|
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,
|
|
5771
|
-
const source = spec.slice(
|
|
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
|
|
5786
|
-
if (
|
|
5787
|
-
const name2 = spec.slice(0,
|
|
5788
|
-
const tier = spec.slice(
|
|
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
|
|
6001
|
-
if (
|
|
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,
|
|
6005
|
-
const stateRef2 = ref.slice(
|
|
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(
|
|
9142
|
-
if (
|
|
9143
|
-
return formatExitLine({ ...exit, 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(
|
|
9167
|
-
this.runs.set(
|
|
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(
|
|
9184
|
-
this.runs.delete(
|
|
9179
|
+
cleanup(run2, outcome = "failed") {
|
|
9180
|
+
this.runs.delete(run2.runId);
|
|
9185
9181
|
this.effects.persist();
|
|
9186
|
-
this.effects.release(
|
|
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(
|
|
9203
|
-
if (
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
if (this.keep(status,
|
|
9208
|
-
await
|
|
9209
|
-
const keptBeforeFinish = this.keep(status,
|
|
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(
|
|
9212
|
-
this.effects.log(`run ${
|
|
9213
|
-
if (!keptBeforeFinish && this.keep(status,
|
|
9214
|
-
this.effects.log(`run ${
|
|
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 ${
|
|
9214
|
+
`finish report for run ${run2.runId} not accepted: ${err instanceof Error ? err.message : String(err)}`
|
|
9219
9215
|
);
|
|
9220
9216
|
}
|
|
9221
|
-
this.cleanup(
|
|
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
|
|
9231
|
-
if (!
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
return
|
|
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(
|
|
9603
|
-
const path2 =
|
|
9604
|
-
if (!path2 || !
|
|
9605
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
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
|
-
`
|
|
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 (
|
|
9743
|
-
const summary =
|
|
9744
|
-
const ended = await client2.finishRun(
|
|
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:
|
|
9753
|
-
session_mode:
|
|
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:
|
|
9762
|
-
workspace_path:
|
|
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)
|
|
9786
|
+
if (ended?.resume_expires_at) run2.keepForResume = true;
|
|
9765
9787
|
},
|
|
9766
|
-
release: (
|
|
9767
|
-
if (
|
|
9768
|
-
|
|
9769
|
-
settleWorkspace(
|
|
9770
|
-
run_id:
|
|
9771
|
-
...
|
|
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
|
-
...
|
|
9795
|
+
...run2.endNote ? { error: run2.endNote } : {}
|
|
9774
9796
|
});
|
|
9775
|
-
void uploadRawLog(
|
|
9797
|
+
void uploadRawLog(run2);
|
|
9776
9798
|
sweepKeptWorkspaces();
|
|
9777
9799
|
},
|
|
9778
|
-
noteKept: (
|
|
9800
|
+
noteKept: (run2) => run2.batcher.append(`workspace kept at ${run2.workspace}
|
|
9779
9801
|
`),
|
|
9780
9802
|
persist: () => {
|
|
9781
|
-
const entries = table2.values().filter((
|
|
9782
|
-
run_id:
|
|
9783
|
-
pid:
|
|
9784
|
-
workspace:
|
|
9785
|
-
key_fingerprint:
|
|
9786
|
-
started_at:
|
|
9787
|
-
...
|
|
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,
|
|
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
|
|
9831
|
-
if (!
|
|
9832
|
-
|
|
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 (
|
|
9835
|
-
const 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
|
|
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
|
-
|
|
9868
|
-
table2.track(
|
|
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 (
|
|
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
|
-
|
|
9925
|
+
run2.batcher.append(`$ git ${args.join(" ")}
|
|
9904
9926
|
`);
|
|
9905
|
-
const result = await runGit(args, workspace,
|
|
9927
|
+
const result = await runGit(args, workspace, run2.batcher);
|
|
9906
9928
|
if (result !== 0) {
|
|
9907
9929
|
return table2.finishAndCleanup(
|
|
9908
|
-
|
|
9930
|
+
run2,
|
|
9909
9931
|
"failed",
|
|
9910
9932
|
`git clone failed for ${repo.url} (exit ${result})`
|
|
9911
9933
|
);
|
|
9912
9934
|
}
|
|
9913
9935
|
}
|
|
9914
|
-
if (
|
|
9936
|
+
if (run2.settled) return table2.cleanup(run2);
|
|
9915
9937
|
const cli = await ensureCli();
|
|
9916
|
-
if (
|
|
9917
|
-
|
|
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
|
-
|
|
9936
|
-
|
|
9957
|
+
run2.pricingModel = harnessInput.model;
|
|
9958
|
+
run2.pricingSessionMode = resume ? "resumed" : "cold";
|
|
9937
9959
|
}
|
|
9938
|
-
|
|
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
|
-
|
|
9957
|
-
|
|
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
|
-
|
|
9984
|
+
run2.limiter = limiter;
|
|
9963
9985
|
const renderer = new ClaudeStreamRenderer(
|
|
9964
|
-
(line) =>
|
|
9986
|
+
(line) => run2.batcher.append(line),
|
|
9965
9987
|
(event) => limiter.noteStreamEvent(event)
|
|
9966
9988
|
);
|
|
9967
|
-
|
|
9989
|
+
run2.renderer = renderer;
|
|
9968
9990
|
const spoolPath = join5(opts.configDir, "rawlogs", `${runId}.ndjson`);
|
|
9969
9991
|
mkdirSync4(dirname4(spoolPath), { recursive: true });
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
10006
|
+
const renderer = new CodexStreamRenderer((line) => run2.batcher.append(line));
|
|
9985
10007
|
const decoder = new StringDecoder("utf8");
|
|
9986
|
-
|
|
9987
|
-
|
|
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) =>
|
|
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
|
-
|
|
9999
|
-
|
|
10020
|
+
run2.batcher.append(text4);
|
|
10021
|
+
run2.limiter?.noteStderr(text4);
|
|
10000
10022
|
});
|
|
10001
|
-
|
|
10002
|
-
if (
|
|
10023
|
+
run2.timeout = setTimeout(() => {
|
|
10024
|
+
if (run2.settled) return;
|
|
10003
10025
|
log(`run ${runId} hit its ${assignment.timeout_minutes}m timeout; killing`);
|
|
10004
|
-
|
|
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(
|
|
10031
|
+
void table2.finishAndCleanup(run2, "failed", `failed to launch harness: ${message2(err)}`);
|
|
10010
10032
|
});
|
|
10011
10033
|
child.on("close", (code, signal) => {
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
const closing = exitLineForRun(
|
|
10034
|
+
run2.drain?.();
|
|
10035
|
+
run2.limiter?.finish();
|
|
10036
|
+
const closing = exitLineForRun(run2, {
|
|
10015
10037
|
code,
|
|
10016
10038
|
signal,
|
|
10017
|
-
durationMs: Date.now() - (
|
|
10039
|
+
durationMs: Date.now() - (run2.spawnedAt ?? Date.now())
|
|
10018
10040
|
});
|
|
10019
|
-
if (closing)
|
|
10020
|
-
if (
|
|
10041
|
+
if (closing) run2.batcher.append(closing);
|
|
10042
|
+
if (run2.timedOut) {
|
|
10021
10043
|
void table2.finishAndCleanup(
|
|
10022
|
-
|
|
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(
|
|
10049
|
+
void table2.finishAndCleanup(run2, "completed");
|
|
10028
10050
|
} else {
|
|
10029
|
-
const limited = signal ? null :
|
|
10030
|
-
const providerError = signal ? 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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
10065
|
-
if (
|
|
10066
|
-
await table2.finishAndCleanup(
|
|
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
|
|
10118
|
-
if (
|
|
10119
|
-
settleWorkspace(
|
|
10120
|
-
run_id:
|
|
10121
|
-
...
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
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
|
-
|
|
10459
|
-
"
|
|
10460
|
-
|
|
10461
|
-
|
|
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
|
|
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:
|
|
10526
|
-
harness,
|
|
10527
|
-
command:
|
|
10528
|
-
maxConcurrent:
|
|
10529
|
-
pollIntervalMs:
|
|
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:
|
|
10535
|
-
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((
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
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(
|
|
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
|
|
10649
|
-
if (opts.json) return printJson(
|
|
11095
|
+
const run2 = await api.getRun(id2);
|
|
11096
|
+
if (opts.json) return printJson(run2);
|
|
10650
11097
|
console.log(
|
|
10651
|
-
`${
|
|
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 (
|
|
10654
|
-
console.log(`tier: ${
|
|
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: ${
|
|
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(
|
|
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 (
|
|
10662
|
-
const u =
|
|
10663
|
-
const costLabel = runCostLabel(
|
|
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(
|
|
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 (
|
|
10696
|
-
if (
|
|
10697
|
-
if (
|
|
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 (
|
|
11161
|
+
if (run2.log_bytes_dropped > 0) {
|
|
10715
11162
|
console.log(
|
|
10716
|
-
`[${Math.round(
|
|
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(
|
|
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
|
|
10728
|
-
if (opts.json) return printJson(
|
|
11174
|
+
const run2 = await api.cancelRun(id2);
|
|
11175
|
+
if (opts.json) return printJson(run2);
|
|
10729
11176
|
console.log(
|
|
10730
|
-
`canceled ${
|
|
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
|
|
11108
|
-
if (
|
|
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(
|
|
11112
|
-
const { state } = await resolveStateFlag(api, spec.slice(0,
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
11600
|
-
const workflowRef = pair.slice(0,
|
|
11601
|
-
const stateRef2 = pair.slice(
|
|
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`);
|