tines 0.0.89 → 0.0.91
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/dist/index.js +342 -46
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/program.ts
|
|
4
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5
|
-
|
|
6
3
|
// ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
|
|
7
4
|
var CommanderError = class extends Error {
|
|
8
5
|
/**
|
|
@@ -4078,6 +4075,36 @@ function runRow(run) {
|
|
|
4078
4075
|
timestamp(run.created_at)
|
|
4079
4076
|
];
|
|
4080
4077
|
}
|
|
4078
|
+
function byteSize(bytes) {
|
|
4079
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
4080
|
+
let value = Math.max(0, bytes);
|
|
4081
|
+
let unit = 0;
|
|
4082
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
4083
|
+
value /= 1024;
|
|
4084
|
+
unit += 1;
|
|
4085
|
+
}
|
|
4086
|
+
const rounded = unit <= 1 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
4087
|
+
return `${unit <= 1 ? rounded : rounded.toFixed(1)} ${units[unit]}`;
|
|
4088
|
+
}
|
|
4089
|
+
function ageLabel(isoTimestamp, now = Date.now()) {
|
|
4090
|
+
const then = Date.parse(isoTimestamp);
|
|
4091
|
+
if (!Number.isFinite(then)) return "\u2014";
|
|
4092
|
+
const seconds = Math.max(0, Math.round((now - then) / 1e3));
|
|
4093
|
+
if (seconds < 60) return `${seconds}s`;
|
|
4094
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
4095
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
|
4096
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
4097
|
+
}
|
|
4098
|
+
function keptWorkspaceRow(kept, sizeBytes, now = Date.now()) {
|
|
4099
|
+
return [
|
|
4100
|
+
kept.run_id,
|
|
4101
|
+
kept.issue_ref ?? "\u2014",
|
|
4102
|
+
kept.status,
|
|
4103
|
+
ageLabel(kept.kept_at, now),
|
|
4104
|
+
byteSize(sizeBytes),
|
|
4105
|
+
kept.path
|
|
4106
|
+
];
|
|
4107
|
+
}
|
|
4081
4108
|
function formatTable(rows) {
|
|
4082
4109
|
if (rows.length === 0) return "";
|
|
4083
4110
|
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
|
|
@@ -5501,16 +5528,28 @@ import { hostname as hostname2 } from "node:os";
|
|
|
5501
5528
|
import { spawn as spawn2 } from "node:child_process";
|
|
5502
5529
|
import {
|
|
5503
5530
|
createWriteStream,
|
|
5531
|
+
existsSync as existsSync4,
|
|
5504
5532
|
mkdirSync as mkdirSync4,
|
|
5505
|
-
readFileSync as
|
|
5506
|
-
rmSync,
|
|
5507
|
-
statSync as
|
|
5533
|
+
readFileSync as readFileSync7,
|
|
5534
|
+
rmSync as rmSync2,
|
|
5535
|
+
statSync as statSync3,
|
|
5508
5536
|
unlinkSync,
|
|
5509
5537
|
writeFileSync as writeFileSync3
|
|
5510
5538
|
} from "node:fs";
|
|
5511
5539
|
import { hostname, platform, arch } from "node:os";
|
|
5512
5540
|
import { dirname as dirname4, join as join4 } from "node:path";
|
|
5513
5541
|
|
|
5542
|
+
// src/version.ts
|
|
5543
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
5544
|
+
function cliVersion() {
|
|
5545
|
+
try {
|
|
5546
|
+
const manifest = new URL("../package.json", import.meta.url);
|
|
5547
|
+
return JSON.parse(readFileSync4(manifest, "utf8")).version ?? "0.0.0-unknown";
|
|
5548
|
+
} catch {
|
|
5549
|
+
return "0.0.0-unknown";
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5514
5553
|
// src/daemon/claude-stream.ts
|
|
5515
5554
|
function clip(value, max) {
|
|
5516
5555
|
return value.length > max ? `${value.slice(0, max)}\u2026` : value;
|
|
@@ -5608,12 +5647,16 @@ var ClaudeStreamRenderer = class {
|
|
|
5608
5647
|
|
|
5609
5648
|
// src/daemon/cli-refresh.ts
|
|
5610
5649
|
import { spawn } from "node:child_process";
|
|
5611
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as
|
|
5650
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
5612
5651
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
5613
5652
|
|
|
5614
5653
|
// src/daemon/support.ts
|
|
5615
5654
|
import { delimiter } from "node:path";
|
|
5616
5655
|
var HARNESS_KINDS = ["claude_code", "codex", "custom"];
|
|
5656
|
+
var KEEP_WORKSPACES_MODES = ["never", "failed", "always"];
|
|
5657
|
+
function keepWorkspace(mode, outcome) {
|
|
5658
|
+
return mode === "always" || mode === "failed" && outcome === "failed";
|
|
5659
|
+
}
|
|
5617
5660
|
function shellQuote(value) {
|
|
5618
5661
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
5619
5662
|
}
|
|
@@ -5641,12 +5684,56 @@ function buildHarnessInvocation(spec, input) {
|
|
|
5641
5684
|
}
|
|
5642
5685
|
}
|
|
5643
5686
|
}
|
|
5687
|
+
var LAUNCH_ARG_MAX = 160;
|
|
5688
|
+
var SAFE_WORD = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
5689
|
+
function launchWord(word) {
|
|
5690
|
+
if (word.length > LAUNCH_ARG_MAX)
|
|
5691
|
+
return shellQuote(`${word.slice(0, LAUNCH_ARG_MAX)}\u2026 [+${word.length - LAUNCH_ARG_MAX} chars]`);
|
|
5692
|
+
return SAFE_WORD.test(word) ? word : shellQuote(word);
|
|
5693
|
+
}
|
|
5694
|
+
function formatLaunchCommand(invocation) {
|
|
5695
|
+
if (invocation.file === "sh" && invocation.args.length === 2 && invocation.args[0] === "-c")
|
|
5696
|
+
return invocation.args[1];
|
|
5697
|
+
return [invocation.file, ...invocation.args].map(launchWord).join(" ");
|
|
5698
|
+
}
|
|
5699
|
+
function formatLaunchBanner(invocation, input, meta) {
|
|
5700
|
+
const fields = [
|
|
5701
|
+
`harness=${meta.harness}`,
|
|
5702
|
+
`model=${input.model ?? "(fixed)"}`,
|
|
5703
|
+
`timeout=${meta.timeoutMinutes}m`,
|
|
5704
|
+
`cli=${meta.cliVersion}`,
|
|
5705
|
+
`workspace=${input.workspace}`
|
|
5706
|
+
];
|
|
5707
|
+
return `$ ${formatLaunchCommand(invocation)}
|
|
5708
|
+
# tines runner: ${fields.join(" ")}
|
|
5709
|
+
`;
|
|
5710
|
+
}
|
|
5711
|
+
function formatDuration(ms) {
|
|
5712
|
+
const seconds = Math.max(0, Math.round(ms / 1e3));
|
|
5713
|
+
return `${Math.floor(seconds / 60)}m${seconds % 60}s`;
|
|
5714
|
+
}
|
|
5715
|
+
function formatExitLine(exit) {
|
|
5716
|
+
const parts = [];
|
|
5717
|
+
if (exit.code !== null) parts.push(`code=${exit.code}`);
|
|
5718
|
+
if (exit.signal) parts.push(`signal=${exit.signal}`);
|
|
5719
|
+
if (parts.length === 0) parts.push("code=?");
|
|
5720
|
+
if (exit.timedOut) parts.push("(timed out)");
|
|
5721
|
+
return `# tines runner: exit ${parts.join(" ")} after ${formatDuration(exit.durationMs)}
|
|
5722
|
+
`;
|
|
5723
|
+
}
|
|
5724
|
+
function exitLineForRun(run, exit) {
|
|
5725
|
+
if (run.settled) return null;
|
|
5726
|
+
return formatExitLine({ ...exit, timedOut: run.timedOut });
|
|
5727
|
+
}
|
|
5644
5728
|
var RunTable = class {
|
|
5645
|
-
constructor(effects) {
|
|
5729
|
+
constructor(effects, opts = {}) {
|
|
5646
5730
|
this.effects = effects;
|
|
5731
|
+
this.keep = opts.keep ?? (() => false);
|
|
5647
5732
|
}
|
|
5648
5733
|
effects;
|
|
5649
5734
|
runs = /* @__PURE__ */ new Map();
|
|
5735
|
+
/** The daemon's keep decision, as data: `keepWorkspace` bound to its mode. */
|
|
5736
|
+
keep;
|
|
5650
5737
|
get size() {
|
|
5651
5738
|
return this.runs.size;
|
|
5652
5739
|
}
|
|
@@ -5667,11 +5754,19 @@ var RunTable = class {
|
|
|
5667
5754
|
persist() {
|
|
5668
5755
|
this.effects.persist();
|
|
5669
5756
|
}
|
|
5670
|
-
/**
|
|
5671
|
-
|
|
5757
|
+
/**
|
|
5758
|
+
* Removes the run and releases its local traces. Safe to call twice.
|
|
5759
|
+
*
|
|
5760
|
+
* The outcome defaults to `failed` because every route here that is not an
|
|
5761
|
+
* explicit completed finish is a failure: a supervisor cancel (which
|
|
5762
|
+
* reaches cleanup with no status at all), a clone failure on an
|
|
5763
|
+
* already-settled run, a daemon shutdown. Guessing `failed` also errs the
|
|
5764
|
+
* safe way — it keeps a directory rather than destroying evidence.
|
|
5765
|
+
*/
|
|
5766
|
+
cleanup(run, outcome = "failed") {
|
|
5672
5767
|
this.runs.delete(run.runId);
|
|
5673
5768
|
this.effects.persist();
|
|
5674
|
-
this.effects.release(run);
|
|
5769
|
+
this.effects.release(run, { keep: this.keep(outcome), outcome });
|
|
5675
5770
|
}
|
|
5676
5771
|
/**
|
|
5677
5772
|
* Ends a run: flush logs, finish-report, clean up. On a run someone
|
|
@@ -5682,7 +5777,9 @@ var RunTable = class {
|
|
|
5682
5777
|
async finishAndCleanup(run, status, error) {
|
|
5683
5778
|
if (run.settled) return this.cleanup(run);
|
|
5684
5779
|
run.settled = true;
|
|
5780
|
+
run.endNote ??= error;
|
|
5685
5781
|
run.drain?.();
|
|
5782
|
+
if (this.keep(status)) this.effects.noteKept?.(run);
|
|
5686
5783
|
await run.flush?.();
|
|
5687
5784
|
try {
|
|
5688
5785
|
await this.effects.finish(run, status, error);
|
|
@@ -5692,7 +5789,7 @@ var RunTable = class {
|
|
|
5692
5789
|
`finish report for run ${run.runId} not accepted: ${err instanceof Error ? err.message : String(err)}`
|
|
5693
5790
|
);
|
|
5694
5791
|
}
|
|
5695
|
-
this.cleanup(run);
|
|
5792
|
+
this.cleanup(run, status);
|
|
5696
5793
|
}
|
|
5697
5794
|
/**
|
|
5698
5795
|
* Marks a supervisor-settled run (`cancels`): kill, do NOT finish-report.
|
|
@@ -5833,7 +5930,7 @@ function binDirOf(prefix) {
|
|
|
5833
5930
|
}
|
|
5834
5931
|
function installedVersion(prefix) {
|
|
5835
5932
|
try {
|
|
5836
|
-
const pkg =
|
|
5933
|
+
const pkg = readFileSync5(join2(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
|
|
5837
5934
|
const version = JSON.parse(pkg).version;
|
|
5838
5935
|
return typeof version === "string" ? version : null;
|
|
5839
5936
|
} catch {
|
|
@@ -5910,7 +6007,15 @@ function message(err) {
|
|
|
5910
6007
|
}
|
|
5911
6008
|
|
|
5912
6009
|
// src/daemon/store.ts
|
|
5913
|
-
import {
|
|
6010
|
+
import {
|
|
6011
|
+
existsSync as existsSync3,
|
|
6012
|
+
mkdirSync as mkdirSync3,
|
|
6013
|
+
readdirSync as readdirSync2,
|
|
6014
|
+
readFileSync as readFileSync6,
|
|
6015
|
+
rmSync,
|
|
6016
|
+
statSync as statSync2,
|
|
6017
|
+
writeFileSync as writeFileSync2
|
|
6018
|
+
} from "node:fs";
|
|
5914
6019
|
import { homedir } from "node:os";
|
|
5915
6020
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
5916
6021
|
function defaultConfigDir() {
|
|
@@ -5919,7 +6024,7 @@ function defaultConfigDir() {
|
|
|
5919
6024
|
function readJsonFile(path2) {
|
|
5920
6025
|
if (!existsSync3(path2)) return null;
|
|
5921
6026
|
try {
|
|
5922
|
-
return JSON.parse(
|
|
6027
|
+
return JSON.parse(readFileSync6(path2, "utf8"));
|
|
5923
6028
|
} catch {
|
|
5924
6029
|
return null;
|
|
5925
6030
|
}
|
|
@@ -5970,10 +6075,10 @@ function saveDaemonState(path2, runs) {
|
|
|
5970
6075
|
}
|
|
5971
6076
|
function processStartTimeMs(pid) {
|
|
5972
6077
|
try {
|
|
5973
|
-
const stat =
|
|
6078
|
+
const stat = readFileSync6(`/proc/${pid}/stat`, "utf8");
|
|
5974
6079
|
const afterComm = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
5975
6080
|
const startTicks = Number(afterComm[19]);
|
|
5976
|
-
const btimeLine =
|
|
6081
|
+
const btimeLine = readFileSync6("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
|
|
5977
6082
|
const btime = Number(btimeLine?.slice("btime ".length));
|
|
5978
6083
|
if (!Number.isFinite(startTicks) || !Number.isFinite(btime)) return null;
|
|
5979
6084
|
return btime * 1e3 + startTicks / 100 * 1e3;
|
|
@@ -5981,18 +6086,93 @@ function processStartTimeMs(pid) {
|
|
|
5981
6086
|
return null;
|
|
5982
6087
|
}
|
|
5983
6088
|
}
|
|
6089
|
+
function workspacesDir(configDir) {
|
|
6090
|
+
return join3(configDir, "workspaces");
|
|
6091
|
+
}
|
|
6092
|
+
function keptMarkerPath(workspace) {
|
|
6093
|
+
return join3(workspace, "kept.json");
|
|
6094
|
+
}
|
|
6095
|
+
function writeKeptMarker(workspace, marker) {
|
|
6096
|
+
writeJsonFile(keptMarkerPath(workspace), marker);
|
|
6097
|
+
}
|
|
6098
|
+
function readKeptMarker(workspace) {
|
|
6099
|
+
const marker = readJsonFile(keptMarkerPath(workspace));
|
|
6100
|
+
if (!marker || typeof marker.run_id !== "string" || typeof marker.kept_at !== "string") {
|
|
6101
|
+
return null;
|
|
6102
|
+
}
|
|
6103
|
+
return marker;
|
|
6104
|
+
}
|
|
6105
|
+
function listKeptWorkspaces(configDir) {
|
|
6106
|
+
const root = workspacesDir(configDir);
|
|
6107
|
+
let names;
|
|
6108
|
+
try {
|
|
6109
|
+
names = readdirSync2(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6110
|
+
} catch {
|
|
6111
|
+
return [];
|
|
6112
|
+
}
|
|
6113
|
+
const kept = [];
|
|
6114
|
+
for (const name2 of names) {
|
|
6115
|
+
const path2 = join3(root, name2);
|
|
6116
|
+
const marker = readKeptMarker(path2);
|
|
6117
|
+
if (marker) kept.push({ ...marker, path: path2 });
|
|
6118
|
+
}
|
|
6119
|
+
return kept.sort((a, b) => Date.parse(b.kept_at) - Date.parse(a.kept_at));
|
|
6120
|
+
}
|
|
6121
|
+
function pruneKeptWorkspaces(configDir, opts) {
|
|
6122
|
+
const kept = listKeptWorkspaces(configDir);
|
|
6123
|
+
const now = (opts.now ?? Date.now)();
|
|
6124
|
+
const doomed = [];
|
|
6125
|
+
const survivors = [];
|
|
6126
|
+
for (const entry of kept) {
|
|
6127
|
+
const age = now - Date.parse(entry.kept_at);
|
|
6128
|
+
const expired = opts.maxAgeMs !== void 0 && Number.isFinite(age) && age > opts.maxAgeMs;
|
|
6129
|
+
if (opts.all || expired) doomed.push(entry);
|
|
6130
|
+
else survivors.push(entry);
|
|
6131
|
+
}
|
|
6132
|
+
if (!opts.all && opts.maxCount !== void 0) doomed.push(...survivors.slice(opts.maxCount));
|
|
6133
|
+
const removed = [];
|
|
6134
|
+
for (const entry of doomed) {
|
|
6135
|
+
try {
|
|
6136
|
+
rmSync(entry.path, { recursive: true, force: true });
|
|
6137
|
+
removed.push(entry);
|
|
6138
|
+
} catch {
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
return removed;
|
|
6142
|
+
}
|
|
6143
|
+
function directorySizeBytes(path2) {
|
|
6144
|
+
let total = 0;
|
|
6145
|
+
let entries;
|
|
6146
|
+
try {
|
|
6147
|
+
entries = readdirSync2(path2, { withFileTypes: true });
|
|
6148
|
+
} catch {
|
|
6149
|
+
return 0;
|
|
6150
|
+
}
|
|
6151
|
+
for (const entry of entries) {
|
|
6152
|
+
const child = join3(path2, entry.name);
|
|
6153
|
+
if (entry.isDirectory()) total += directorySizeBytes(child);
|
|
6154
|
+
else if (entry.isFile()) {
|
|
6155
|
+
try {
|
|
6156
|
+
total += statSync2(child).size;
|
|
6157
|
+
} catch {
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
return total;
|
|
6162
|
+
}
|
|
5984
6163
|
|
|
5985
6164
|
// src/daemon/daemon.ts
|
|
5986
6165
|
var CLI_REFRESH_TTL_MS = 10 * 6e4;
|
|
6166
|
+
var DAEMON_VERSION = cliVersion();
|
|
5987
6167
|
async function uploadRawLog(run) {
|
|
5988
6168
|
const path2 = run.rawSpoolPath;
|
|
5989
6169
|
if (!path2 || !run.rawUpload) return;
|
|
5990
6170
|
run.rawSpoolPath = void 0;
|
|
5991
6171
|
try {
|
|
5992
6172
|
await new Promise((resolve) => run.rawSpool?.end(resolve) ?? resolve());
|
|
5993
|
-
const size =
|
|
6173
|
+
const size = statSync3(path2).size;
|
|
5994
6174
|
if (size > 0) {
|
|
5995
|
-
let body =
|
|
6175
|
+
let body = readFileSync7(path2);
|
|
5996
6176
|
if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
|
|
5997
6177
|
const marker = Buffer.from(
|
|
5998
6178
|
`{"type":"tines_truncated","dropped_bytes":${body.byteLength - RUN_LOG_RAW_MAX_BYTES}}
|
|
@@ -6032,6 +6212,34 @@ function pidAlive(pid) {
|
|
|
6032
6212
|
}
|
|
6033
6213
|
async function runDaemon(opts) {
|
|
6034
6214
|
mkdirSync4(opts.configDir, { recursive: true });
|
|
6215
|
+
mkdirSync4(workspacesDir(opts.configDir), { recursive: true, mode: 448 });
|
|
6216
|
+
const sweepKeptWorkspaces = () => {
|
|
6217
|
+
const removed = pruneKeptWorkspaces(opts.configDir, {
|
|
6218
|
+
maxAgeMs: opts.keepWorkspacesForHours * 36e5,
|
|
6219
|
+
maxCount: opts.keepWorkspacesMax
|
|
6220
|
+
});
|
|
6221
|
+
if (removed.length > 0) {
|
|
6222
|
+
log(
|
|
6223
|
+
`pruned ${removed.length} kept workspace(s) past retention (${opts.keepWorkspacesForHours}h, max ${opts.keepWorkspacesMax})`
|
|
6224
|
+
);
|
|
6225
|
+
}
|
|
6226
|
+
};
|
|
6227
|
+
sweepKeptWorkspaces();
|
|
6228
|
+
const settleWorkspace = (workspace, keep, marker) => {
|
|
6229
|
+
if (!keep) {
|
|
6230
|
+
rmSync2(workspace, { recursive: true, force: true });
|
|
6231
|
+
return;
|
|
6232
|
+
}
|
|
6233
|
+
if (!existsSync4(workspace)) return;
|
|
6234
|
+
try {
|
|
6235
|
+
writeKeptMarker(workspace, { ...marker, kept_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6236
|
+
log(`run ${marker.run_id}: workspace kept at ${workspace}`);
|
|
6237
|
+
} catch (err) {
|
|
6238
|
+
log(
|
|
6239
|
+
`run ${marker.run_id}: workspace kept at ${workspace}, but kept.json could not be written (${message2(err)})`
|
|
6240
|
+
);
|
|
6241
|
+
}
|
|
6242
|
+
};
|
|
6035
6243
|
let creds = loadRunnerCredentials(opts.configDir, opts.url, opts.name);
|
|
6036
6244
|
if (creds) {
|
|
6037
6245
|
log(`reconnecting as runner "${opts.name}" (${creds.runner_id}) \u2014 token from ${opts.configDir}`);
|
|
@@ -6067,24 +6275,33 @@ async function runDaemon(opts) {
|
|
|
6067
6275
|
finish: async (run, status, error) => {
|
|
6068
6276
|
await client2.finishRun(run.runId, { status, ...error ? { error } : {} });
|
|
6069
6277
|
},
|
|
6070
|
-
release: (run) => {
|
|
6278
|
+
release: (run, { keep, outcome }) => {
|
|
6071
6279
|
if (run.timeout) clearTimeout(run.timeout);
|
|
6072
6280
|
run.renderer?.finish();
|
|
6073
|
-
|
|
6281
|
+
settleWorkspace(run.workspace, keep, {
|
|
6282
|
+
run_id: run.runId,
|
|
6283
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {},
|
|
6284
|
+
status: outcome,
|
|
6285
|
+
...run.endNote ? { error: run.endNote } : {}
|
|
6286
|
+
});
|
|
6074
6287
|
void uploadRawLog(run);
|
|
6288
|
+
sweepKeptWorkspaces();
|
|
6075
6289
|
},
|
|
6290
|
+
noteKept: (run) => run.batcher.append(`workspace kept at ${run.workspace}
|
|
6291
|
+
`),
|
|
6076
6292
|
persist: () => {
|
|
6077
6293
|
const entries = table2.values().filter((run) => run.child?.pid !== void 0).map((run) => ({
|
|
6078
6294
|
run_id: run.runId,
|
|
6079
6295
|
pid: run.child.pid,
|
|
6080
6296
|
workspace: run.workspace,
|
|
6081
6297
|
key_fingerprint: run.keyFingerprint,
|
|
6082
|
-
started_at: run.spawnedAt
|
|
6298
|
+
started_at: run.spawnedAt,
|
|
6299
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {}
|
|
6083
6300
|
}));
|
|
6084
6301
|
saveDaemonState(statePath, entries);
|
|
6085
6302
|
},
|
|
6086
6303
|
log
|
|
6087
|
-
});
|
|
6304
|
+
}, { keep: (outcome) => keepWorkspace(opts.keepWorkspaces, outcome) });
|
|
6088
6305
|
for (const orphan of loadDaemonState(statePath)) {
|
|
6089
6306
|
if (pidAlive(orphan.pid)) {
|
|
6090
6307
|
const processStart = processStartTimeMs(orphan.pid);
|
|
@@ -6103,12 +6320,18 @@ async function runDaemon(opts) {
|
|
|
6103
6320
|
});
|
|
6104
6321
|
} catch {
|
|
6105
6322
|
}
|
|
6106
|
-
|
|
6323
|
+
settleWorkspace(orphan.workspace, keepWorkspace(opts.keepWorkspaces, "failed"), {
|
|
6324
|
+
run_id: orphan.run_id,
|
|
6325
|
+
...orphan.issue_ref ? { issue_ref: orphan.issue_ref } : {},
|
|
6326
|
+
status: "failed",
|
|
6327
|
+
error: "daemon restarted; orphaned harness killed"
|
|
6328
|
+
});
|
|
6107
6329
|
}
|
|
6108
6330
|
saveDaemonState(statePath, []);
|
|
6109
6331
|
const killWithoutFinish = (runId) => {
|
|
6110
6332
|
const run = table2.markCanceled(runId);
|
|
6111
6333
|
if (!run) return;
|
|
6334
|
+
run.endNote ??= "canceled by supervisor";
|
|
6112
6335
|
log(`supervisor canceled run ${runId}; killing without finish-reporting`);
|
|
6113
6336
|
if (run.child?.pid) {
|
|
6114
6337
|
const pid = run.child.pid;
|
|
@@ -6119,10 +6342,12 @@ async function runDaemon(opts) {
|
|
|
6119
6342
|
const launch = async (assignment) => {
|
|
6120
6343
|
const runId = assignment.run.id;
|
|
6121
6344
|
if (table2.has(runId)) return;
|
|
6122
|
-
const workspace = join4(opts.configDir,
|
|
6345
|
+
const workspace = join4(workspacesDir(opts.configDir), runId);
|
|
6346
|
+
const issueLabel = assignment.run.issue_ref ? `${assignment.run.issue_ref.project_name}/${assignment.run.issue_ref.number}` : void 0;
|
|
6123
6347
|
const run = {
|
|
6124
6348
|
runId,
|
|
6125
6349
|
workspace,
|
|
6350
|
+
...issueLabel ? { issueLabel } : {},
|
|
6126
6351
|
canceled: false,
|
|
6127
6352
|
timedOut: false,
|
|
6128
6353
|
settled: false,
|
|
@@ -6135,9 +6360,9 @@ async function runDaemon(opts) {
|
|
|
6135
6360
|
};
|
|
6136
6361
|
run.flush = () => run.batcher.flush();
|
|
6137
6362
|
table2.track(run);
|
|
6138
|
-
log(`run ${runId} assigned (issue ${
|
|
6363
|
+
log(`run ${runId} assigned (issue ${issueLabel ?? assignment.run.issue_id}); materializing workspace`);
|
|
6139
6364
|
try {
|
|
6140
|
-
|
|
6365
|
+
rmSync2(workspace, { recursive: true, force: true });
|
|
6141
6366
|
mkdirSync4(workspace, { recursive: true });
|
|
6142
6367
|
writeFileSync3(join4(workspace, "prompt.md"), `${assignment.prompt}
|
|
6143
6368
|
`);
|
|
@@ -6172,14 +6397,22 @@ async function runDaemon(opts) {
|
|
|
6172
6397
|
` : `warning: no daemon-managed tines CLI; using whatever \`tines\` is on this machine's PATH
|
|
6173
6398
|
`
|
|
6174
6399
|
);
|
|
6400
|
+
const harnessInput = {
|
|
6401
|
+
workspace,
|
|
6402
|
+
promptFile: join4(workspace, "prompt.md"),
|
|
6403
|
+
prompt: assignment.prompt,
|
|
6404
|
+
model: assignment.run.model
|
|
6405
|
+
};
|
|
6175
6406
|
const invocation = buildHarnessInvocation(
|
|
6176
6407
|
{ harness: opts.harness, command: opts.command },
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6408
|
+
harnessInput
|
|
6409
|
+
);
|
|
6410
|
+
run.batcher.append(
|
|
6411
|
+
formatLaunchBanner(invocation, harnessInput, {
|
|
6412
|
+
harness: opts.harness,
|
|
6413
|
+
timeoutMinutes: assignment.timeout_minutes,
|
|
6414
|
+
cliVersion: DAEMON_VERSION
|
|
6415
|
+
})
|
|
6183
6416
|
);
|
|
6184
6417
|
const child = spawn2(invocation.file, invocation.args, {
|
|
6185
6418
|
cwd: workspace,
|
|
@@ -6226,6 +6459,13 @@ async function runDaemon(opts) {
|
|
|
6226
6459
|
void table2.finishAndCleanup(run, "failed", `failed to launch harness: ${message2(err)}`);
|
|
6227
6460
|
});
|
|
6228
6461
|
child.on("exit", (code, signal) => {
|
|
6462
|
+
run.drain?.();
|
|
6463
|
+
const closing = exitLineForRun(run, {
|
|
6464
|
+
code,
|
|
6465
|
+
signal,
|
|
6466
|
+
durationMs: Date.now() - (run.spawnedAt ?? Date.now())
|
|
6467
|
+
});
|
|
6468
|
+
if (closing) run.batcher.append(closing);
|
|
6229
6469
|
if (run.timedOut) {
|
|
6230
6470
|
void table2.finishAndCleanup(
|
|
6231
6471
|
run,
|
|
@@ -6284,7 +6524,12 @@ async function runDaemon(opts) {
|
|
|
6284
6524
|
if (err instanceof ApiError && err.status === 401) {
|
|
6285
6525
|
for (const run of table2.values()) {
|
|
6286
6526
|
if (run.child?.pid) killTree(run.child.pid, "SIGKILL");
|
|
6287
|
-
|
|
6527
|
+
settleWorkspace(run.workspace, keepWorkspace(opts.keepWorkspaces, "failed"), {
|
|
6528
|
+
run_id: run.runId,
|
|
6529
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {},
|
|
6530
|
+
status: "failed",
|
|
6531
|
+
error: "daemon token rejected"
|
|
6532
|
+
});
|
|
6288
6533
|
}
|
|
6289
6534
|
saveDaemonState(statePath, []);
|
|
6290
6535
|
clearRunnerCredentials(opts.configDir, opts.url, opts.name);
|
|
@@ -6522,6 +6767,20 @@ function register6(program3) {
|
|
|
6522
6767
|
runnerCmd.command("daemon").description("Run the local runner daemon: register/reconnect, poll for assigned runs, execute them").option("--name <name>", "runner name, unique per user (default: this hostname)").option("--harness <harness>", "claude-code | codex | custom", "claude-code").option("--command <template>", "custom harness command template ({prompt_file}, {workspace}, {model})").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(
|
|
6523
6768
|
"--no-cli-refresh",
|
|
6524
6769
|
"do not install/refresh the agent-facing tines CLI from npm (harnesses use the ambient PATH)"
|
|
6770
|
+
).option(
|
|
6771
|
+
"--keep-workspaces <mode>",
|
|
6772
|
+
"keep settled runs' workspaces for debugging: never | failed | always",
|
|
6773
|
+
"never"
|
|
6774
|
+
).option(
|
|
6775
|
+
"--keep-workspaces-for <hours>",
|
|
6776
|
+
"delete kept workspaces older than this",
|
|
6777
|
+
(v) => Number(v),
|
|
6778
|
+
72
|
|
6779
|
+
).option(
|
|
6780
|
+
"--keep-workspaces-max <n>",
|
|
6781
|
+
"keep at most this many workspaces (oldest removed first)",
|
|
6782
|
+
(v) => Number.parseInt(v, 10),
|
|
6783
|
+
20
|
|
6525
6784
|
)
|
|
6526
6785
|
).action(
|
|
6527
6786
|
async (opts) => {
|
|
@@ -6539,6 +6798,16 @@ function register6(program3) {
|
|
|
6539
6798
|
if (!Number.isInteger(opts.pollInterval) || opts.pollInterval < 1) {
|
|
6540
6799
|
die("--poll-interval must be a positive number of seconds");
|
|
6541
6800
|
}
|
|
6801
|
+
const keepWorkspaces = opts.keepWorkspaces;
|
|
6802
|
+
if (!KEEP_WORKSPACES_MODES.includes(keepWorkspaces)) {
|
|
6803
|
+
die(`--keep-workspaces must be ${KEEP_WORKSPACES_MODES.join(", ")}, got "${opts.keepWorkspaces}"`);
|
|
6804
|
+
}
|
|
6805
|
+
if (!Number.isFinite(opts.keepWorkspacesFor) || opts.keepWorkspacesFor <= 0) {
|
|
6806
|
+
die("--keep-workspaces-for must be a positive number of hours");
|
|
6807
|
+
}
|
|
6808
|
+
if (!Number.isInteger(opts.keepWorkspacesMax) || opts.keepWorkspacesMax < 1) {
|
|
6809
|
+
die("--keep-workspaces-max must be a positive integer");
|
|
6810
|
+
}
|
|
6542
6811
|
await runDaemon({
|
|
6543
6812
|
url: resolveUrl(opts).replace(/\/+$/, ""),
|
|
6544
6813
|
apiKey: resolveApiKey(opts),
|
|
@@ -6548,10 +6817,45 @@ function register6(program3) {
|
|
|
6548
6817
|
maxConcurrent: opts.maxConcurrent,
|
|
6549
6818
|
pollIntervalMs: opts.pollInterval * 1e3,
|
|
6550
6819
|
configDir: defaultConfigDir(),
|
|
6551
|
-
cliRefresh: opts.cliRefresh
|
|
6820
|
+
cliRefresh: opts.cliRefresh,
|
|
6821
|
+
keepWorkspaces,
|
|
6822
|
+
keepWorkspacesForHours: opts.keepWorkspacesFor,
|
|
6823
|
+
keepWorkspacesMax: opts.keepWorkspacesMax
|
|
6552
6824
|
});
|
|
6553
6825
|
}
|
|
6554
6826
|
);
|
|
6827
|
+
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) => {
|
|
6828
|
+
const configDir = defaultConfigDir();
|
|
6829
|
+
const kept = listKeptWorkspaces(configDir);
|
|
6830
|
+
const sized = kept.map((k) => ({ ...k, size_bytes: directorySizeBytes(k.path) }));
|
|
6831
|
+
if (opts.json) return printJson({ items: sized });
|
|
6832
|
+
if (sized.length === 0) {
|
|
6833
|
+
console.log(`no kept workspaces in ${workspacesDir(configDir)}`);
|
|
6834
|
+
return console.log(
|
|
6835
|
+
"the daemon keeps them only with --keep-workspaces failed (or always)."
|
|
6836
|
+
);
|
|
6837
|
+
}
|
|
6838
|
+
table([
|
|
6839
|
+
["RUN", "ISSUE", "STATUS", "AGE", "SIZE", "PATH"],
|
|
6840
|
+
...sized.map((k) => keptWorkspaceRow(k, k.size_bytes))
|
|
6841
|
+
]);
|
|
6842
|
+
});
|
|
6843
|
+
workspacesCmd.command("prune").description("Delete kept workspaces (never touches a live run's workspace)").option("--all", "delete every kept workspace").option("--older-than <hours>", "delete kept workspaces older than this", (v) => Number(v)).option("--json", "print JSON instead of a table").action((opts) => {
|
|
6844
|
+
if (opts.all === void 0 && opts.olderThan === void 0) {
|
|
6845
|
+
die("pass --all or --older-than <hours>");
|
|
6846
|
+
}
|
|
6847
|
+
if (opts.all && opts.olderThan !== void 0) die("--all cannot be combined with --older-than");
|
|
6848
|
+
if (opts.olderThan !== void 0 && (!Number.isFinite(opts.olderThan) || opts.olderThan < 0)) {
|
|
6849
|
+
die("--older-than must be a non-negative number of hours");
|
|
6850
|
+
}
|
|
6851
|
+
const removed = pruneKeptWorkspaces(defaultConfigDir(), {
|
|
6852
|
+
all: opts.all,
|
|
6853
|
+
...opts.olderThan !== void 0 ? { maxAgeMs: opts.olderThan * 36e5 } : {}
|
|
6854
|
+
});
|
|
6855
|
+
if (opts.json) return printJson({ items: removed });
|
|
6856
|
+
for (const entry of removed) console.log(`removed ${entry.path}`);
|
|
6857
|
+
console.log(`pruned ${removed.length} kept workspace${removed.length === 1 ? "" : "s"}`);
|
|
6858
|
+
});
|
|
6555
6859
|
const runsCmd = program3.command("runs").description("Agent runs: attempts at issues by runners");
|
|
6556
6860
|
withList(
|
|
6557
6861
|
runsCmd.command("list").description("List runs, newest first").option("-i, --issue <ref>", "filter to one issue (<project>/<number>)").option("-r, --runner <name>", "filter by runner name").option("--active", "only runs holding a claim (assigned/launching/running)")
|
|
@@ -6899,7 +7203,7 @@ function register8(program3) {
|
|
|
6899
7203
|
}
|
|
6900
7204
|
|
|
6901
7205
|
// src/commands/workflows.ts
|
|
6902
|
-
import { readFileSync as
|
|
7206
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
6903
7207
|
function readJsonBody(inline, file) {
|
|
6904
7208
|
if (inline !== void 0 && file !== void 0) {
|
|
6905
7209
|
die("pass the JSON inline or with --file, not both");
|
|
@@ -6908,14 +7212,14 @@ function readJsonBody(inline, file) {
|
|
|
6908
7212
|
if (file !== void 0 && file !== "-") {
|
|
6909
7213
|
let raw;
|
|
6910
7214
|
try {
|
|
6911
|
-
raw =
|
|
7215
|
+
raw = readFileSync8(file, "utf8");
|
|
6912
7216
|
} catch (err) {
|
|
6913
7217
|
die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6914
7218
|
}
|
|
6915
7219
|
return parseJsonObject(raw, file);
|
|
6916
7220
|
}
|
|
6917
7221
|
if (file === "-" || !process.stdin.isTTY) {
|
|
6918
|
-
const raw =
|
|
7222
|
+
const raw = readFileSync8(0, "utf8");
|
|
6919
7223
|
if (raw.trim() === "") {
|
|
6920
7224
|
if (file === "-") die("no JSON on stdin");
|
|
6921
7225
|
return void 0;
|
|
@@ -7069,14 +7373,6 @@ see \`tines workflows create --help\` for the expected shape`
|
|
|
7069
7373
|
}
|
|
7070
7374
|
|
|
7071
7375
|
// src/program.ts
|
|
7072
|
-
function cliVersion() {
|
|
7073
|
-
try {
|
|
7074
|
-
const manifest = new URL("../package.json", import.meta.url);
|
|
7075
|
-
return JSON.parse(readFileSync8(manifest, "utf8")).version ?? "0.0.0-unknown";
|
|
7076
|
-
} catch {
|
|
7077
|
-
return "0.0.0-unknown";
|
|
7078
|
-
}
|
|
7079
|
-
}
|
|
7080
7376
|
var program2 = new Command();
|
|
7081
7377
|
program2.name("tines").description("CLI for Tines").version(cliVersion()).enablePositionalOptions();
|
|
7082
7378
|
registerTime(program2);
|