tines 0.0.88 → 0.0.90
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 +266 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3514,6 +3514,7 @@ function utilizationLabel(quota, activeRuns, stateName = (id) => id) {
|
|
|
3514
3514
|
if (counts.size === 0) return `no active runs (roster default ${quota.default_limit} per state)`;
|
|
3515
3515
|
return [...counts.entries()].map(([stateId, { name: name2, n }]) => `${name2} ${n}/${quota.overrides[stateId] ?? quota.default_limit}`).join(" \xB7 ");
|
|
3516
3516
|
}
|
|
3517
|
+
var LIBRARY_MAX_BYTES = 5 * 1024 * 1024;
|
|
3517
3518
|
|
|
3518
3519
|
// ../shared/src/events.ts
|
|
3519
3520
|
var text = (t) => ({ kind: "text", text: t });
|
|
@@ -3887,7 +3888,13 @@ function createApiClient(options) {
|
|
|
3887
3888
|
// API keys (create/revoke require a browser session, not a key)
|
|
3888
3889
|
listApiKeys: () => get("/api/v1/api-keys"),
|
|
3889
3890
|
createApiKey: (body) => request("POST", "/api/v1/api-keys", body),
|
|
3890
|
-
revokeApiKey: (id) => request("DELETE", `/api/v1/api-keys/${id}`)
|
|
3891
|
+
revokeApiKey: (id) => request("DELETE", `/api/v1/api-keys/${id}`),
|
|
3892
|
+
// Library export / import (workflows + context; no tracker data, no secrets)
|
|
3893
|
+
exportLibrary: (opts = {}) => get(
|
|
3894
|
+
`/api/v1/export${opts.journals === false ? "?journals=false" : ""}`
|
|
3895
|
+
),
|
|
3896
|
+
/** Plan-then-apply; `dry_run: true` returns the preview the apply follows. */
|
|
3897
|
+
importLibrary: (body) => request("POST", "/api/v1/import", body)
|
|
3891
3898
|
};
|
|
3892
3899
|
}
|
|
3893
3900
|
|
|
@@ -4071,6 +4078,36 @@ function runRow(run) {
|
|
|
4071
4078
|
timestamp(run.created_at)
|
|
4072
4079
|
];
|
|
4073
4080
|
}
|
|
4081
|
+
function byteSize(bytes) {
|
|
4082
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
4083
|
+
let value = Math.max(0, bytes);
|
|
4084
|
+
let unit = 0;
|
|
4085
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
4086
|
+
value /= 1024;
|
|
4087
|
+
unit += 1;
|
|
4088
|
+
}
|
|
4089
|
+
const rounded = unit <= 1 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
4090
|
+
return `${unit <= 1 ? rounded : rounded.toFixed(1)} ${units[unit]}`;
|
|
4091
|
+
}
|
|
4092
|
+
function ageLabel(isoTimestamp, now = Date.now()) {
|
|
4093
|
+
const then = Date.parse(isoTimestamp);
|
|
4094
|
+
if (!Number.isFinite(then)) return "\u2014";
|
|
4095
|
+
const seconds = Math.max(0, Math.round((now - then) / 1e3));
|
|
4096
|
+
if (seconds < 60) return `${seconds}s`;
|
|
4097
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
4098
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
|
4099
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
4100
|
+
}
|
|
4101
|
+
function keptWorkspaceRow(kept, sizeBytes, now = Date.now()) {
|
|
4102
|
+
return [
|
|
4103
|
+
kept.run_id,
|
|
4104
|
+
kept.issue_ref ?? "\u2014",
|
|
4105
|
+
kept.status,
|
|
4106
|
+
ageLabel(kept.kept_at, now),
|
|
4107
|
+
byteSize(sizeBytes),
|
|
4108
|
+
kept.path
|
|
4109
|
+
];
|
|
4110
|
+
}
|
|
4074
4111
|
function formatTable(rows) {
|
|
4075
4112
|
if (rows.length === 0) return "";
|
|
4076
4113
|
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
|
|
@@ -5494,10 +5531,11 @@ import { hostname as hostname2 } from "node:os";
|
|
|
5494
5531
|
import { spawn as spawn2 } from "node:child_process";
|
|
5495
5532
|
import {
|
|
5496
5533
|
createWriteStream,
|
|
5534
|
+
existsSync as existsSync4,
|
|
5497
5535
|
mkdirSync as mkdirSync4,
|
|
5498
5536
|
readFileSync as readFileSync6,
|
|
5499
|
-
rmSync,
|
|
5500
|
-
statSync as
|
|
5537
|
+
rmSync as rmSync2,
|
|
5538
|
+
statSync as statSync3,
|
|
5501
5539
|
unlinkSync,
|
|
5502
5540
|
writeFileSync as writeFileSync3
|
|
5503
5541
|
} from "node:fs";
|
|
@@ -5607,6 +5645,10 @@ import { dirname as dirname2, join as join2 } from "node:path";
|
|
|
5607
5645
|
// src/daemon/support.ts
|
|
5608
5646
|
import { delimiter } from "node:path";
|
|
5609
5647
|
var HARNESS_KINDS = ["claude_code", "codex", "custom"];
|
|
5648
|
+
var KEEP_WORKSPACES_MODES = ["never", "failed", "always"];
|
|
5649
|
+
function keepWorkspace(mode, outcome) {
|
|
5650
|
+
return mode === "always" || mode === "failed" && outcome === "failed";
|
|
5651
|
+
}
|
|
5610
5652
|
function shellQuote(value) {
|
|
5611
5653
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
5612
5654
|
}
|
|
@@ -5635,11 +5677,14 @@ function buildHarnessInvocation(spec, input) {
|
|
|
5635
5677
|
}
|
|
5636
5678
|
}
|
|
5637
5679
|
var RunTable = class {
|
|
5638
|
-
constructor(effects) {
|
|
5680
|
+
constructor(effects, opts = {}) {
|
|
5639
5681
|
this.effects = effects;
|
|
5682
|
+
this.keep = opts.keep ?? (() => false);
|
|
5640
5683
|
}
|
|
5641
5684
|
effects;
|
|
5642
5685
|
runs = /* @__PURE__ */ new Map();
|
|
5686
|
+
/** The daemon's keep decision, as data: `keepWorkspace` bound to its mode. */
|
|
5687
|
+
keep;
|
|
5643
5688
|
get size() {
|
|
5644
5689
|
return this.runs.size;
|
|
5645
5690
|
}
|
|
@@ -5660,11 +5705,19 @@ var RunTable = class {
|
|
|
5660
5705
|
persist() {
|
|
5661
5706
|
this.effects.persist();
|
|
5662
5707
|
}
|
|
5663
|
-
/**
|
|
5664
|
-
|
|
5708
|
+
/**
|
|
5709
|
+
* Removes the run and releases its local traces. Safe to call twice.
|
|
5710
|
+
*
|
|
5711
|
+
* The outcome defaults to `failed` because every route here that is not an
|
|
5712
|
+
* explicit completed finish is a failure: a supervisor cancel (which
|
|
5713
|
+
* reaches cleanup with no status at all), a clone failure on an
|
|
5714
|
+
* already-settled run, a daemon shutdown. Guessing `failed` also errs the
|
|
5715
|
+
* safe way — it keeps a directory rather than destroying evidence.
|
|
5716
|
+
*/
|
|
5717
|
+
cleanup(run, outcome = "failed") {
|
|
5665
5718
|
this.runs.delete(run.runId);
|
|
5666
5719
|
this.effects.persist();
|
|
5667
|
-
this.effects.release(run);
|
|
5720
|
+
this.effects.release(run, { keep: this.keep(outcome), outcome });
|
|
5668
5721
|
}
|
|
5669
5722
|
/**
|
|
5670
5723
|
* Ends a run: flush logs, finish-report, clean up. On a run someone
|
|
@@ -5675,7 +5728,9 @@ var RunTable = class {
|
|
|
5675
5728
|
async finishAndCleanup(run, status, error) {
|
|
5676
5729
|
if (run.settled) return this.cleanup(run);
|
|
5677
5730
|
run.settled = true;
|
|
5731
|
+
run.endNote ??= error;
|
|
5678
5732
|
run.drain?.();
|
|
5733
|
+
if (this.keep(status)) this.effects.noteKept?.(run);
|
|
5679
5734
|
await run.flush?.();
|
|
5680
5735
|
try {
|
|
5681
5736
|
await this.effects.finish(run, status, error);
|
|
@@ -5685,7 +5740,7 @@ var RunTable = class {
|
|
|
5685
5740
|
`finish report for run ${run.runId} not accepted: ${err instanceof Error ? err.message : String(err)}`
|
|
5686
5741
|
);
|
|
5687
5742
|
}
|
|
5688
|
-
this.cleanup(run);
|
|
5743
|
+
this.cleanup(run, status);
|
|
5689
5744
|
}
|
|
5690
5745
|
/**
|
|
5691
5746
|
* Marks a supervisor-settled run (`cancels`): kill, do NOT finish-report.
|
|
@@ -5903,7 +5958,15 @@ function message(err) {
|
|
|
5903
5958
|
}
|
|
5904
5959
|
|
|
5905
5960
|
// src/daemon/store.ts
|
|
5906
|
-
import {
|
|
5961
|
+
import {
|
|
5962
|
+
existsSync as existsSync3,
|
|
5963
|
+
mkdirSync as mkdirSync3,
|
|
5964
|
+
readdirSync as readdirSync2,
|
|
5965
|
+
readFileSync as readFileSync5,
|
|
5966
|
+
rmSync,
|
|
5967
|
+
statSync as statSync2,
|
|
5968
|
+
writeFileSync as writeFileSync2
|
|
5969
|
+
} from "node:fs";
|
|
5907
5970
|
import { homedir } from "node:os";
|
|
5908
5971
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
5909
5972
|
function defaultConfigDir() {
|
|
@@ -5974,6 +6037,80 @@ function processStartTimeMs(pid) {
|
|
|
5974
6037
|
return null;
|
|
5975
6038
|
}
|
|
5976
6039
|
}
|
|
6040
|
+
function workspacesDir(configDir) {
|
|
6041
|
+
return join3(configDir, "workspaces");
|
|
6042
|
+
}
|
|
6043
|
+
function keptMarkerPath(workspace) {
|
|
6044
|
+
return join3(workspace, "kept.json");
|
|
6045
|
+
}
|
|
6046
|
+
function writeKeptMarker(workspace, marker) {
|
|
6047
|
+
writeJsonFile(keptMarkerPath(workspace), marker);
|
|
6048
|
+
}
|
|
6049
|
+
function readKeptMarker(workspace) {
|
|
6050
|
+
const marker = readJsonFile(keptMarkerPath(workspace));
|
|
6051
|
+
if (!marker || typeof marker.run_id !== "string" || typeof marker.kept_at !== "string") {
|
|
6052
|
+
return null;
|
|
6053
|
+
}
|
|
6054
|
+
return marker;
|
|
6055
|
+
}
|
|
6056
|
+
function listKeptWorkspaces(configDir) {
|
|
6057
|
+
const root = workspacesDir(configDir);
|
|
6058
|
+
let names;
|
|
6059
|
+
try {
|
|
6060
|
+
names = readdirSync2(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6061
|
+
} catch {
|
|
6062
|
+
return [];
|
|
6063
|
+
}
|
|
6064
|
+
const kept = [];
|
|
6065
|
+
for (const name2 of names) {
|
|
6066
|
+
const path2 = join3(root, name2);
|
|
6067
|
+
const marker = readKeptMarker(path2);
|
|
6068
|
+
if (marker) kept.push({ ...marker, path: path2 });
|
|
6069
|
+
}
|
|
6070
|
+
return kept.sort((a, b) => Date.parse(b.kept_at) - Date.parse(a.kept_at));
|
|
6071
|
+
}
|
|
6072
|
+
function pruneKeptWorkspaces(configDir, opts) {
|
|
6073
|
+
const kept = listKeptWorkspaces(configDir);
|
|
6074
|
+
const now = (opts.now ?? Date.now)();
|
|
6075
|
+
const doomed = [];
|
|
6076
|
+
const survivors = [];
|
|
6077
|
+
for (const entry of kept) {
|
|
6078
|
+
const age = now - Date.parse(entry.kept_at);
|
|
6079
|
+
const expired = opts.maxAgeMs !== void 0 && Number.isFinite(age) && age > opts.maxAgeMs;
|
|
6080
|
+
if (opts.all || expired) doomed.push(entry);
|
|
6081
|
+
else survivors.push(entry);
|
|
6082
|
+
}
|
|
6083
|
+
if (!opts.all && opts.maxCount !== void 0) doomed.push(...survivors.slice(opts.maxCount));
|
|
6084
|
+
const removed = [];
|
|
6085
|
+
for (const entry of doomed) {
|
|
6086
|
+
try {
|
|
6087
|
+
rmSync(entry.path, { recursive: true, force: true });
|
|
6088
|
+
removed.push(entry);
|
|
6089
|
+
} catch {
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
return removed;
|
|
6093
|
+
}
|
|
6094
|
+
function directorySizeBytes(path2) {
|
|
6095
|
+
let total = 0;
|
|
6096
|
+
let entries;
|
|
6097
|
+
try {
|
|
6098
|
+
entries = readdirSync2(path2, { withFileTypes: true });
|
|
6099
|
+
} catch {
|
|
6100
|
+
return 0;
|
|
6101
|
+
}
|
|
6102
|
+
for (const entry of entries) {
|
|
6103
|
+
const child = join3(path2, entry.name);
|
|
6104
|
+
if (entry.isDirectory()) total += directorySizeBytes(child);
|
|
6105
|
+
else if (entry.isFile()) {
|
|
6106
|
+
try {
|
|
6107
|
+
total += statSync2(child).size;
|
|
6108
|
+
} catch {
|
|
6109
|
+
}
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
6112
|
+
return total;
|
|
6113
|
+
}
|
|
5977
6114
|
|
|
5978
6115
|
// src/daemon/daemon.ts
|
|
5979
6116
|
var CLI_REFRESH_TTL_MS = 10 * 6e4;
|
|
@@ -5983,7 +6120,7 @@ async function uploadRawLog(run) {
|
|
|
5983
6120
|
run.rawSpoolPath = void 0;
|
|
5984
6121
|
try {
|
|
5985
6122
|
await new Promise((resolve) => run.rawSpool?.end(resolve) ?? resolve());
|
|
5986
|
-
const size =
|
|
6123
|
+
const size = statSync3(path2).size;
|
|
5987
6124
|
if (size > 0) {
|
|
5988
6125
|
let body = readFileSync6(path2);
|
|
5989
6126
|
if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
|
|
@@ -6025,6 +6162,34 @@ function pidAlive(pid) {
|
|
|
6025
6162
|
}
|
|
6026
6163
|
async function runDaemon(opts) {
|
|
6027
6164
|
mkdirSync4(opts.configDir, { recursive: true });
|
|
6165
|
+
mkdirSync4(workspacesDir(opts.configDir), { recursive: true, mode: 448 });
|
|
6166
|
+
const sweepKeptWorkspaces = () => {
|
|
6167
|
+
const removed = pruneKeptWorkspaces(opts.configDir, {
|
|
6168
|
+
maxAgeMs: opts.keepWorkspacesForHours * 36e5,
|
|
6169
|
+
maxCount: opts.keepWorkspacesMax
|
|
6170
|
+
});
|
|
6171
|
+
if (removed.length > 0) {
|
|
6172
|
+
log(
|
|
6173
|
+
`pruned ${removed.length} kept workspace(s) past retention (${opts.keepWorkspacesForHours}h, max ${opts.keepWorkspacesMax})`
|
|
6174
|
+
);
|
|
6175
|
+
}
|
|
6176
|
+
};
|
|
6177
|
+
sweepKeptWorkspaces();
|
|
6178
|
+
const settleWorkspace = (workspace, keep, marker) => {
|
|
6179
|
+
if (!keep) {
|
|
6180
|
+
rmSync2(workspace, { recursive: true, force: true });
|
|
6181
|
+
return;
|
|
6182
|
+
}
|
|
6183
|
+
if (!existsSync4(workspace)) return;
|
|
6184
|
+
try {
|
|
6185
|
+
writeKeptMarker(workspace, { ...marker, kept_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6186
|
+
log(`run ${marker.run_id}: workspace kept at ${workspace}`);
|
|
6187
|
+
} catch (err) {
|
|
6188
|
+
log(
|
|
6189
|
+
`run ${marker.run_id}: workspace kept at ${workspace}, but kept.json could not be written (${message2(err)})`
|
|
6190
|
+
);
|
|
6191
|
+
}
|
|
6192
|
+
};
|
|
6028
6193
|
let creds = loadRunnerCredentials(opts.configDir, opts.url, opts.name);
|
|
6029
6194
|
if (creds) {
|
|
6030
6195
|
log(`reconnecting as runner "${opts.name}" (${creds.runner_id}) \u2014 token from ${opts.configDir}`);
|
|
@@ -6060,24 +6225,33 @@ async function runDaemon(opts) {
|
|
|
6060
6225
|
finish: async (run, status, error) => {
|
|
6061
6226
|
await client2.finishRun(run.runId, { status, ...error ? { error } : {} });
|
|
6062
6227
|
},
|
|
6063
|
-
release: (run) => {
|
|
6228
|
+
release: (run, { keep, outcome }) => {
|
|
6064
6229
|
if (run.timeout) clearTimeout(run.timeout);
|
|
6065
6230
|
run.renderer?.finish();
|
|
6066
|
-
|
|
6231
|
+
settleWorkspace(run.workspace, keep, {
|
|
6232
|
+
run_id: run.runId,
|
|
6233
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {},
|
|
6234
|
+
status: outcome,
|
|
6235
|
+
...run.endNote ? { error: run.endNote } : {}
|
|
6236
|
+
});
|
|
6067
6237
|
void uploadRawLog(run);
|
|
6238
|
+
sweepKeptWorkspaces();
|
|
6068
6239
|
},
|
|
6240
|
+
noteKept: (run) => run.batcher.append(`workspace kept at ${run.workspace}
|
|
6241
|
+
`),
|
|
6069
6242
|
persist: () => {
|
|
6070
6243
|
const entries = table2.values().filter((run) => run.child?.pid !== void 0).map((run) => ({
|
|
6071
6244
|
run_id: run.runId,
|
|
6072
6245
|
pid: run.child.pid,
|
|
6073
6246
|
workspace: run.workspace,
|
|
6074
6247
|
key_fingerprint: run.keyFingerprint,
|
|
6075
|
-
started_at: run.spawnedAt
|
|
6248
|
+
started_at: run.spawnedAt,
|
|
6249
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {}
|
|
6076
6250
|
}));
|
|
6077
6251
|
saveDaemonState(statePath, entries);
|
|
6078
6252
|
},
|
|
6079
6253
|
log
|
|
6080
|
-
});
|
|
6254
|
+
}, { keep: (outcome) => keepWorkspace(opts.keepWorkspaces, outcome) });
|
|
6081
6255
|
for (const orphan of loadDaemonState(statePath)) {
|
|
6082
6256
|
if (pidAlive(orphan.pid)) {
|
|
6083
6257
|
const processStart = processStartTimeMs(orphan.pid);
|
|
@@ -6096,12 +6270,18 @@ async function runDaemon(opts) {
|
|
|
6096
6270
|
});
|
|
6097
6271
|
} catch {
|
|
6098
6272
|
}
|
|
6099
|
-
|
|
6273
|
+
settleWorkspace(orphan.workspace, keepWorkspace(opts.keepWorkspaces, "failed"), {
|
|
6274
|
+
run_id: orphan.run_id,
|
|
6275
|
+
...orphan.issue_ref ? { issue_ref: orphan.issue_ref } : {},
|
|
6276
|
+
status: "failed",
|
|
6277
|
+
error: "daemon restarted; orphaned harness killed"
|
|
6278
|
+
});
|
|
6100
6279
|
}
|
|
6101
6280
|
saveDaemonState(statePath, []);
|
|
6102
6281
|
const killWithoutFinish = (runId) => {
|
|
6103
6282
|
const run = table2.markCanceled(runId);
|
|
6104
6283
|
if (!run) return;
|
|
6284
|
+
run.endNote ??= "canceled by supervisor";
|
|
6105
6285
|
log(`supervisor canceled run ${runId}; killing without finish-reporting`);
|
|
6106
6286
|
if (run.child?.pid) {
|
|
6107
6287
|
const pid = run.child.pid;
|
|
@@ -6112,10 +6292,12 @@ async function runDaemon(opts) {
|
|
|
6112
6292
|
const launch = async (assignment) => {
|
|
6113
6293
|
const runId = assignment.run.id;
|
|
6114
6294
|
if (table2.has(runId)) return;
|
|
6115
|
-
const workspace = join4(opts.configDir,
|
|
6295
|
+
const workspace = join4(workspacesDir(opts.configDir), runId);
|
|
6296
|
+
const issueLabel = assignment.run.issue_ref ? `${assignment.run.issue_ref.project_name}/${assignment.run.issue_ref.number}` : void 0;
|
|
6116
6297
|
const run = {
|
|
6117
6298
|
runId,
|
|
6118
6299
|
workspace,
|
|
6300
|
+
...issueLabel ? { issueLabel } : {},
|
|
6119
6301
|
canceled: false,
|
|
6120
6302
|
timedOut: false,
|
|
6121
6303
|
settled: false,
|
|
@@ -6128,9 +6310,9 @@ async function runDaemon(opts) {
|
|
|
6128
6310
|
};
|
|
6129
6311
|
run.flush = () => run.batcher.flush();
|
|
6130
6312
|
table2.track(run);
|
|
6131
|
-
log(`run ${runId} assigned (issue ${
|
|
6313
|
+
log(`run ${runId} assigned (issue ${issueLabel ?? assignment.run.issue_id}); materializing workspace`);
|
|
6132
6314
|
try {
|
|
6133
|
-
|
|
6315
|
+
rmSync2(workspace, { recursive: true, force: true });
|
|
6134
6316
|
mkdirSync4(workspace, { recursive: true });
|
|
6135
6317
|
writeFileSync3(join4(workspace, "prompt.md"), `${assignment.prompt}
|
|
6136
6318
|
`);
|
|
@@ -6277,7 +6459,12 @@ async function runDaemon(opts) {
|
|
|
6277
6459
|
if (err instanceof ApiError && err.status === 401) {
|
|
6278
6460
|
for (const run of table2.values()) {
|
|
6279
6461
|
if (run.child?.pid) killTree(run.child.pid, "SIGKILL");
|
|
6280
|
-
|
|
6462
|
+
settleWorkspace(run.workspace, keepWorkspace(opts.keepWorkspaces, "failed"), {
|
|
6463
|
+
run_id: run.runId,
|
|
6464
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {},
|
|
6465
|
+
status: "failed",
|
|
6466
|
+
error: "daemon token rejected"
|
|
6467
|
+
});
|
|
6281
6468
|
}
|
|
6282
6469
|
saveDaemonState(statePath, []);
|
|
6283
6470
|
clearRunnerCredentials(opts.configDir, opts.url, opts.name);
|
|
@@ -6515,6 +6702,20 @@ function register6(program3) {
|
|
|
6515
6702
|
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(
|
|
6516
6703
|
"--no-cli-refresh",
|
|
6517
6704
|
"do not install/refresh the agent-facing tines CLI from npm (harnesses use the ambient PATH)"
|
|
6705
|
+
).option(
|
|
6706
|
+
"--keep-workspaces <mode>",
|
|
6707
|
+
"keep settled runs' workspaces for debugging: never | failed | always",
|
|
6708
|
+
"never"
|
|
6709
|
+
).option(
|
|
6710
|
+
"--keep-workspaces-for <hours>",
|
|
6711
|
+
"delete kept workspaces older than this",
|
|
6712
|
+
(v) => Number(v),
|
|
6713
|
+
72
|
|
6714
|
+
).option(
|
|
6715
|
+
"--keep-workspaces-max <n>",
|
|
6716
|
+
"keep at most this many workspaces (oldest removed first)",
|
|
6717
|
+
(v) => Number.parseInt(v, 10),
|
|
6718
|
+
20
|
|
6518
6719
|
)
|
|
6519
6720
|
).action(
|
|
6520
6721
|
async (opts) => {
|
|
@@ -6532,6 +6733,16 @@ function register6(program3) {
|
|
|
6532
6733
|
if (!Number.isInteger(opts.pollInterval) || opts.pollInterval < 1) {
|
|
6533
6734
|
die("--poll-interval must be a positive number of seconds");
|
|
6534
6735
|
}
|
|
6736
|
+
const keepWorkspaces = opts.keepWorkspaces;
|
|
6737
|
+
if (!KEEP_WORKSPACES_MODES.includes(keepWorkspaces)) {
|
|
6738
|
+
die(`--keep-workspaces must be ${KEEP_WORKSPACES_MODES.join(", ")}, got "${opts.keepWorkspaces}"`);
|
|
6739
|
+
}
|
|
6740
|
+
if (!Number.isFinite(opts.keepWorkspacesFor) || opts.keepWorkspacesFor <= 0) {
|
|
6741
|
+
die("--keep-workspaces-for must be a positive number of hours");
|
|
6742
|
+
}
|
|
6743
|
+
if (!Number.isInteger(opts.keepWorkspacesMax) || opts.keepWorkspacesMax < 1) {
|
|
6744
|
+
die("--keep-workspaces-max must be a positive integer");
|
|
6745
|
+
}
|
|
6535
6746
|
await runDaemon({
|
|
6536
6747
|
url: resolveUrl(opts).replace(/\/+$/, ""),
|
|
6537
6748
|
apiKey: resolveApiKey(opts),
|
|
@@ -6541,10 +6752,45 @@ function register6(program3) {
|
|
|
6541
6752
|
maxConcurrent: opts.maxConcurrent,
|
|
6542
6753
|
pollIntervalMs: opts.pollInterval * 1e3,
|
|
6543
6754
|
configDir: defaultConfigDir(),
|
|
6544
|
-
cliRefresh: opts.cliRefresh
|
|
6755
|
+
cliRefresh: opts.cliRefresh,
|
|
6756
|
+
keepWorkspaces,
|
|
6757
|
+
keepWorkspacesForHours: opts.keepWorkspacesFor,
|
|
6758
|
+
keepWorkspacesMax: opts.keepWorkspacesMax
|
|
6545
6759
|
});
|
|
6546
6760
|
}
|
|
6547
6761
|
);
|
|
6762
|
+
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) => {
|
|
6763
|
+
const configDir = defaultConfigDir();
|
|
6764
|
+
const kept = listKeptWorkspaces(configDir);
|
|
6765
|
+
const sized = kept.map((k) => ({ ...k, size_bytes: directorySizeBytes(k.path) }));
|
|
6766
|
+
if (opts.json) return printJson({ items: sized });
|
|
6767
|
+
if (sized.length === 0) {
|
|
6768
|
+
console.log(`no kept workspaces in ${workspacesDir(configDir)}`);
|
|
6769
|
+
return console.log(
|
|
6770
|
+
"the daemon keeps them only with --keep-workspaces failed (or always)."
|
|
6771
|
+
);
|
|
6772
|
+
}
|
|
6773
|
+
table([
|
|
6774
|
+
["RUN", "ISSUE", "STATUS", "AGE", "SIZE", "PATH"],
|
|
6775
|
+
...sized.map((k) => keptWorkspaceRow(k, k.size_bytes))
|
|
6776
|
+
]);
|
|
6777
|
+
});
|
|
6778
|
+
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) => {
|
|
6779
|
+
if (opts.all === void 0 && opts.olderThan === void 0) {
|
|
6780
|
+
die("pass --all or --older-than <hours>");
|
|
6781
|
+
}
|
|
6782
|
+
if (opts.all && opts.olderThan !== void 0) die("--all cannot be combined with --older-than");
|
|
6783
|
+
if (opts.olderThan !== void 0 && (!Number.isFinite(opts.olderThan) || opts.olderThan < 0)) {
|
|
6784
|
+
die("--older-than must be a non-negative number of hours");
|
|
6785
|
+
}
|
|
6786
|
+
const removed = pruneKeptWorkspaces(defaultConfigDir(), {
|
|
6787
|
+
all: opts.all,
|
|
6788
|
+
...opts.olderThan !== void 0 ? { maxAgeMs: opts.olderThan * 36e5 } : {}
|
|
6789
|
+
});
|
|
6790
|
+
if (opts.json) return printJson({ items: removed });
|
|
6791
|
+
for (const entry of removed) console.log(`removed ${entry.path}`);
|
|
6792
|
+
console.log(`pruned ${removed.length} kept workspace${removed.length === 1 ? "" : "s"}`);
|
|
6793
|
+
});
|
|
6548
6794
|
const runsCmd = program3.command("runs").description("Agent runs: attempts at issues by runners");
|
|
6549
6795
|
withList(
|
|
6550
6796
|
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)")
|