tines 0.0.91 → 0.0.93
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 +56 -0
- package/dist/index.js +807 -476
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3434,7 +3434,8 @@ function repoDirFromUrl(url) {
|
|
|
3434
3434
|
const stripped = url.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
3435
3435
|
const lastSlash = Math.max(stripped.lastIndexOf("/"), stripped.lastIndexOf(":"));
|
|
3436
3436
|
const base = stripped.slice(lastSlash + 1).replace(/\.git$/, "");
|
|
3437
|
-
if (!base || base === "." || base === ".." || base.includes("\\") || base.includes("="))
|
|
3437
|
+
if (!base || base === "." || base === ".." || base.includes("\\") || base.includes("="))
|
|
3438
|
+
return "repo";
|
|
3438
3439
|
return base;
|
|
3439
3440
|
}
|
|
3440
3441
|
var ARTIFACT_FILE_MAX_BYTES = 25 * 1024 * 1024;
|
|
@@ -3468,7 +3469,13 @@ var RUN_LOG_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
|
3468
3469
|
var RUN_LOG_RAW_MAX_BYTES = 64 * 1024 * 1024;
|
|
3469
3470
|
var MODEL_PREDECESSORS = {
|
|
3470
3471
|
"claude-fable-5": ["claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"],
|
|
3471
|
-
"claude-opus-5": [
|
|
3472
|
+
"claude-opus-5": [
|
|
3473
|
+
"claude-opus-4-8",
|
|
3474
|
+
"claude-opus-4-7",
|
|
3475
|
+
"claude-opus-4-6",
|
|
3476
|
+
"claude-opus-4-5",
|
|
3477
|
+
"claude-opus-4-1"
|
|
3478
|
+
],
|
|
3472
3479
|
"claude-sonnet-5": ["claude-sonnet-4-6", "claude-sonnet-4-5", "claude-3-7-sonnet-latest"],
|
|
3473
3480
|
"claude-haiku-4-5": ["claude-3-5-haiku-latest"],
|
|
3474
3481
|
"gemini-2.5-pro": ["gemini-1.5-pro"],
|
|
@@ -3509,7 +3516,9 @@ function utilizationLabel(quota, activeRuns, stateName = (id) => id) {
|
|
|
3509
3516
|
if (!counts.has(stateId)) counts.set(stateId, { name: stateName(stateId), n: 0 });
|
|
3510
3517
|
}
|
|
3511
3518
|
if (counts.size === 0) return `no active runs (roster default ${quota.default_limit} per state)`;
|
|
3512
|
-
return [...counts.entries()].map(
|
|
3519
|
+
return [...counts.entries()].map(
|
|
3520
|
+
([stateId, { name: name2, n }]) => `${name2} ${n}/${quota.overrides[stateId] ?? quota.default_limit}`
|
|
3521
|
+
).join(" \xB7 ");
|
|
3513
3522
|
}
|
|
3514
3523
|
var LIBRARY_MAX_BYTES = 5 * 1024 * 1024;
|
|
3515
3524
|
|
|
@@ -3610,9 +3619,15 @@ var DESCRIBERS = {
|
|
|
3610
3619
|
name(p.runner_name),
|
|
3611
3620
|
text(`fail to launch (${str(p.consecutive_failures)} consecutive): ${str(p.error)}`)
|
|
3612
3621
|
],
|
|
3613
|
-
"routing_rule.created": (ev, p) => [
|
|
3614
|
-
|
|
3615
|
-
|
|
3622
|
+
"routing_rule.created": (ev, p) => [
|
|
3623
|
+
text(`${action(ev.type)} the ${str(p.scope_label)} routing rule`)
|
|
3624
|
+
],
|
|
3625
|
+
"routing_rule.updated": (ev, p) => [
|
|
3626
|
+
text(`${action(ev.type)} the ${str(p.scope_label)} routing rule`)
|
|
3627
|
+
],
|
|
3628
|
+
"routing_rule.deleted": (ev, p) => [
|
|
3629
|
+
text(`${action(ev.type)} the ${str(p.scope_label)} routing rule`)
|
|
3630
|
+
],
|
|
3616
3631
|
"settings.updated": (_ev, p) => [
|
|
3617
3632
|
text(`updated supervisor settings (${joinChanged(p.changed, ", ") || "no changes"})`)
|
|
3618
3633
|
],
|
|
@@ -3720,7 +3735,10 @@ function createApiClient(options) {
|
|
|
3720
3735
|
async function raw(method, path2, opts = {}) {
|
|
3721
3736
|
const headers = { ...opts.headers ?? {} };
|
|
3722
3737
|
if (options.apiKey) headers.authorization = `Bearer ${options.apiKey}`;
|
|
3723
|
-
const body = opts.body instanceof Uint8Array ? opts.body.buffer.slice(
|
|
3738
|
+
const body = opts.body instanceof Uint8Array ? opts.body.buffer.slice(
|
|
3739
|
+
opts.body.byteOffset,
|
|
3740
|
+
opts.body.byteOffset + opts.body.byteLength
|
|
3741
|
+
) : opts.body;
|
|
3724
3742
|
const res = await fetchFn(`${base}${path2}`, { method, headers, body });
|
|
3725
3743
|
if (!res.ok) {
|
|
3726
3744
|
let parsed = null;
|
|
@@ -3816,7 +3834,11 @@ function createApiClient(options) {
|
|
|
3816
3834
|
uploadArtifactFolder: async (issueId, name2, files) => {
|
|
3817
3835
|
const form = new FormData();
|
|
3818
3836
|
for (const file of files) {
|
|
3819
|
-
form.append(
|
|
3837
|
+
form.append(
|
|
3838
|
+
"file",
|
|
3839
|
+
new Blob([file.bytes], { type: file.contentType }),
|
|
3840
|
+
file.path
|
|
3841
|
+
);
|
|
3820
3842
|
}
|
|
3821
3843
|
const res = await raw("PUT", artifactPath(issueId, name2, "/folder"), { body: form });
|
|
3822
3844
|
return await res.json();
|
|
@@ -3871,7 +3893,10 @@ function createApiClient(options) {
|
|
|
3871
3893
|
/** Daemon-only: uploads the raw harness stream for a settled run. */
|
|
3872
3894
|
putRunLogRaw: (id, body) => raw("PUT", `/api/v1/runs/${id}/log/raw`, {
|
|
3873
3895
|
body,
|
|
3874
|
-
headers: {
|
|
3896
|
+
headers: {
|
|
3897
|
+
"content-type": "application/x-ndjson",
|
|
3898
|
+
"content-length": String(body.byteLength)
|
|
3899
|
+
}
|
|
3875
3900
|
}),
|
|
3876
3901
|
cancelRun: (id) => request("POST", `/api/v1/runs/${id}/cancel`),
|
|
3877
3902
|
// Routing rules (one per exact scope; responses carry shadow hints)
|
|
@@ -3887,9 +3912,7 @@ function createApiClient(options) {
|
|
|
3887
3912
|
createApiKey: (body) => request("POST", "/api/v1/api-keys", body),
|
|
3888
3913
|
revokeApiKey: (id) => request("DELETE", `/api/v1/api-keys/${id}`),
|
|
3889
3914
|
// Library export / import (workflows + context; no tracker data, no secrets)
|
|
3890
|
-
exportLibrary: (opts = {}) => get(
|
|
3891
|
-
`/api/v1/export${opts.journals === false ? "?journals=false" : ""}`
|
|
3892
|
-
),
|
|
3915
|
+
exportLibrary: (opts = {}) => get(`/api/v1/export${opts.journals === false ? "?journals=false" : ""}`),
|
|
3893
3916
|
/** Plan-then-apply; `dry_run: true` returns the preview the apply follows. */
|
|
3894
3917
|
importLibrary: (body) => request("POST", "/api/v1/import", body)
|
|
3895
3918
|
};
|
|
@@ -4050,7 +4073,9 @@ function runnerStatusLabel(runner) {
|
|
|
4050
4073
|
}
|
|
4051
4074
|
function ruleTargetsLabel(rule) {
|
|
4052
4075
|
if (rule.targets.length === 0) return "(no targets)";
|
|
4053
|
-
return rule.targets.map(
|
|
4076
|
+
return rule.targets.map(
|
|
4077
|
+
(t) => `${t.runner_name}${t.tier ? `:${t.tier}` : ""}${t.runner_status === "paused" ? " (paused)" : ""}`
|
|
4078
|
+
).join(" \u2192 ");
|
|
4054
4079
|
}
|
|
4055
4080
|
function quotaLabel(quota, stateName) {
|
|
4056
4081
|
if (quota.type === "global_cap") return `global cap: at most ${quota.limit} concurrent runs`;
|
|
@@ -4108,7 +4133,9 @@ function keptWorkspaceRow(kept, sizeBytes, now = Date.now()) {
|
|
|
4108
4133
|
function formatTable(rows) {
|
|
4109
4134
|
if (rows.length === 0) return "";
|
|
4110
4135
|
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
|
|
4111
|
-
return rows.map(
|
|
4136
|
+
return rows.map(
|
|
4137
|
+
(row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd()
|
|
4138
|
+
).join("\n");
|
|
4112
4139
|
}
|
|
4113
4140
|
|
|
4114
4141
|
// src/refs.ts
|
|
@@ -4124,7 +4151,9 @@ function parseJsonObject(raw, source) {
|
|
|
4124
4151
|
try {
|
|
4125
4152
|
value = JSON.parse(raw);
|
|
4126
4153
|
} catch (err) {
|
|
4127
|
-
throw new CliError(
|
|
4154
|
+
throw new CliError(
|
|
4155
|
+
`invalid JSON from ${source}: ${err instanceof Error ? err.message : String(err)}`
|
|
4156
|
+
);
|
|
4128
4157
|
}
|
|
4129
4158
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
4130
4159
|
throw new CliError(`expected a JSON object from ${source}`);
|
|
@@ -4161,7 +4190,9 @@ function parseFileSpec(spec) {
|
|
|
4161
4190
|
const path2 = spec.slice(0, sep);
|
|
4162
4191
|
const source = spec.slice(sep + 1);
|
|
4163
4192
|
if (!source.startsWith("@")) {
|
|
4164
|
-
throw new CliError(
|
|
4193
|
+
throw new CliError(
|
|
4194
|
+
`skill file content always comes from a local file: --file ${path2}=@<local-file>`
|
|
4195
|
+
);
|
|
4165
4196
|
}
|
|
4166
4197
|
const file = source.slice(1);
|
|
4167
4198
|
try {
|
|
@@ -4182,16 +4213,57 @@ function parseTargetSpec(spec) {
|
|
|
4182
4213
|
return { name: name2, tier };
|
|
4183
4214
|
}
|
|
4184
4215
|
|
|
4185
|
-
// src/
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4216
|
+
// src/config.ts
|
|
4217
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "node:fs";
|
|
4218
|
+
import { homedir } from "node:os";
|
|
4219
|
+
import { dirname, join } from "node:path";
|
|
4220
|
+
function defaultConfigDir() {
|
|
4221
|
+
return process.env.TINES_CONFIG_DIR ?? join(homedir(), ".config", "tines");
|
|
4222
|
+
}
|
|
4223
|
+
function readJsonFile(path2) {
|
|
4224
|
+
if (!existsSync(path2)) return null;
|
|
4225
|
+
try {
|
|
4226
|
+
return JSON.parse(readFileSync3(path2, "utf8"));
|
|
4227
|
+
} catch {
|
|
4228
|
+
return null;
|
|
4193
4229
|
}
|
|
4194
|
-
|
|
4230
|
+
}
|
|
4231
|
+
function writeJsonFile(path2, value, { secret = false } = {}) {
|
|
4232
|
+
mkdirSync(dirname(path2), { recursive: true });
|
|
4233
|
+
writeFileSync(path2, `${JSON.stringify(value, null, 2)}
|
|
4234
|
+
`, secret ? { mode: 384 } : {});
|
|
4235
|
+
}
|
|
4236
|
+
function configPath(dir = defaultConfigDir()) {
|
|
4237
|
+
return join(dir, "config.json");
|
|
4238
|
+
}
|
|
4239
|
+
function loadCliConfig(dir = defaultConfigDir()) {
|
|
4240
|
+
const raw = readJsonFile(configPath(dir));
|
|
4241
|
+
const config = {};
|
|
4242
|
+
if (typeof raw?.url === "string" && raw.url !== "") config.url = raw.url;
|
|
4243
|
+
if (typeof raw?.api_key === "string" && raw.api_key !== "") config.api_key = raw.api_key;
|
|
4244
|
+
return config;
|
|
4245
|
+
}
|
|
4246
|
+
function saveCliConfig(dir, patch) {
|
|
4247
|
+
const next = { ...loadCliConfig(dir) };
|
|
4248
|
+
if (patch.url !== void 0) next.url = patch.url.replace(/\/+$/, "");
|
|
4249
|
+
if (patch.api_key !== void 0) next.api_key = patch.api_key;
|
|
4250
|
+
writeJsonFile(configPath(dir), next, { secret: true });
|
|
4251
|
+
return next;
|
|
4252
|
+
}
|
|
4253
|
+
function clearCliConfig(dir = defaultConfigDir()) {
|
|
4254
|
+
const path2 = configPath(dir);
|
|
4255
|
+
if (!existsSync(path2)) return false;
|
|
4256
|
+
unlinkSync(path2);
|
|
4257
|
+
return true;
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
// src/common.ts
|
|
4261
|
+
var DEFAULT_URL = "https://tines.tbuckley.dev";
|
|
4262
|
+
function withCommon(cmd) {
|
|
4263
|
+
return cmd.option(
|
|
4264
|
+
"-u, --url <url>",
|
|
4265
|
+
`base URL of the Tines API (or set TINES_API_URL, or run \`tines login\`; default ${DEFAULT_URL})`
|
|
4266
|
+
).option("--api-key <key>", "API key (or set TINES_API_KEY, or run `tines login`)").option("--json", "output the raw JSON response");
|
|
4195
4267
|
}
|
|
4196
4268
|
function withList(cmd) {
|
|
4197
4269
|
return withCommon(
|
|
@@ -4207,11 +4279,25 @@ function withList(cmd) {
|
|
|
4207
4279
|
)
|
|
4208
4280
|
);
|
|
4209
4281
|
}
|
|
4282
|
+
function resolveUrlSetting(opts) {
|
|
4283
|
+
if (opts.url) return { value: opts.url, source: "flag" };
|
|
4284
|
+
if (process.env.TINES_API_URL) return { value: process.env.TINES_API_URL, source: "env" };
|
|
4285
|
+
const stored = loadCliConfig().url;
|
|
4286
|
+
if (stored) return { value: stored, source: "config" };
|
|
4287
|
+
return { value: DEFAULT_URL, source: "default" };
|
|
4288
|
+
}
|
|
4289
|
+
function resolveApiKeySetting(opts) {
|
|
4290
|
+
if (opts.apiKey) return { value: opts.apiKey, source: "flag" };
|
|
4291
|
+
if (process.env.TINES_API_KEY) return { value: process.env.TINES_API_KEY, source: "env" };
|
|
4292
|
+
const stored = loadCliConfig().api_key;
|
|
4293
|
+
if (stored) return { value: stored, source: "config" };
|
|
4294
|
+
return { value: void 0, source: "default" };
|
|
4295
|
+
}
|
|
4210
4296
|
function resolveUrl(opts) {
|
|
4211
|
-
return opts.
|
|
4297
|
+
return resolveUrlSetting(opts).value;
|
|
4212
4298
|
}
|
|
4213
4299
|
function resolveApiKey(opts) {
|
|
4214
|
-
return opts.
|
|
4300
|
+
return resolveApiKeySetting(opts).value;
|
|
4215
4301
|
}
|
|
4216
4302
|
function client(opts) {
|
|
4217
4303
|
return createApiClient({ baseUrl: resolveUrl(opts), apiKey: resolveApiKey(opts) });
|
|
@@ -4330,7 +4416,8 @@ async function resolveRunner(api, ref) {
|
|
|
4330
4416
|
async function resolveScopeFlags(api, opts) {
|
|
4331
4417
|
const scope = {};
|
|
4332
4418
|
if (opts.project !== void 0) scope.project_id = (await resolveProject(api, opts.project)).id;
|
|
4333
|
-
if (opts.state !== void 0)
|
|
4419
|
+
if (opts.state !== void 0)
|
|
4420
|
+
scope.workflow_state_id = (await resolveStateFlag(api, opts.state)).state.id;
|
|
4334
4421
|
if (opts.issue !== void 0) scope.issue_id = (await resolveIssue(api, opts.issue)).id;
|
|
4335
4422
|
return scope;
|
|
4336
4423
|
}
|
|
@@ -4360,7 +4447,9 @@ files (seeded at skills/${item.name}/):`);
|
|
|
4360
4447
|
console.log(`
|
|
4361
4448
|
url: ${item.repo_url}`);
|
|
4362
4449
|
if (item.repo_branch) console.log(`branch: ${item.repo_branch}`);
|
|
4363
|
-
console.log(
|
|
4450
|
+
console.log(
|
|
4451
|
+
`dir: ${item.repo_dir ?? `${repoDirFromUrl(item.repo_url ?? "")} (derived from the URL)`}`
|
|
4452
|
+
);
|
|
4364
4453
|
}
|
|
4365
4454
|
}
|
|
4366
4455
|
var SCOPE_FLAGS_HELP = `
|
|
@@ -4373,57 +4462,72 @@ function withScopeFlags(cmd) {
|
|
|
4373
4462
|
return cmd.option("-p, --project <name>", "scope: project name or id").option("-s, --state <workflow/state>", "scope: workflow-qualified state").option("-i, --issue <ref>", "scope: issue (<project>/<number>)").addHelpText("after", SCOPE_FLAGS_HELP);
|
|
4374
4463
|
}
|
|
4375
4464
|
function register(program3) {
|
|
4376
|
-
const context = program3.command("context").description(
|
|
4465
|
+
const context = program3.command("context").description(
|
|
4466
|
+
"Manage context items (prompts, skills, repo pointers) scoped to projects, states, and issues"
|
|
4467
|
+
);
|
|
4377
4468
|
withList(
|
|
4378
4469
|
withScopeFlags(
|
|
4379
|
-
context.command("list").description(
|
|
4470
|
+
context.command("list").description(
|
|
4471
|
+
"List context items (scope filters match every item whose scope includes the element)"
|
|
4472
|
+
).option("-k, --kind <kind>", "filter by kind: prompt, skill, or repo").option("--exact", "only items whose scope sets exactly the given dimensions").option("-q, --search <text>", "search names and descriptions")
|
|
4380
4473
|
)
|
|
4381
|
-
).action(
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
[
|
|
4400
|
-
|
|
4401
|
-
i
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
withCommon(context.command("show <id>").description("Show a context item (skills include their files)")).action(
|
|
4412
|
-
async (id, opts) => {
|
|
4413
|
-
const item = await client(opts).getContextItem(id);
|
|
4414
|
-
if (opts.json) return printJson(item);
|
|
4415
|
-
printContextItem(item);
|
|
4474
|
+
).action(
|
|
4475
|
+
async (opts) => {
|
|
4476
|
+
const api = client(opts);
|
|
4477
|
+
const scope = await resolveScopeFlags(api, opts);
|
|
4478
|
+
const res = await fetchList(
|
|
4479
|
+
opts,
|
|
4480
|
+
(page) => api.listContext({
|
|
4481
|
+
kind: opts.kind,
|
|
4482
|
+
project: scope.project_id ?? void 0,
|
|
4483
|
+
state: scope.workflow_state_id ?? void 0,
|
|
4484
|
+
issue: scope.issue_id ?? void 0,
|
|
4485
|
+
q: opts.search,
|
|
4486
|
+
exact: opts.exact ? true : void 0,
|
|
4487
|
+
...page
|
|
4488
|
+
})
|
|
4489
|
+
);
|
|
4490
|
+
printList(res, opts, (items) => {
|
|
4491
|
+
if (items.length === 0) return console.log("no context items");
|
|
4492
|
+
table([
|
|
4493
|
+
["KIND", "NAME", "SCOPE", "PAYLOAD", "UPDATED", "ID"],
|
|
4494
|
+
...items.map((i) => [
|
|
4495
|
+
i.kind,
|
|
4496
|
+
i.name,
|
|
4497
|
+
i.scope.label,
|
|
4498
|
+
contextItemSummary(i),
|
|
4499
|
+
timestamp(i.updated_at),
|
|
4500
|
+
i.id
|
|
4501
|
+
])
|
|
4502
|
+
]);
|
|
4503
|
+
});
|
|
4416
4504
|
}
|
|
4417
4505
|
);
|
|
4506
|
+
withCommon(
|
|
4507
|
+
context.command("show <id>").description("Show a context item (skills include their files)")
|
|
4508
|
+
).action(async (id, opts) => {
|
|
4509
|
+
const item = await client(opts).getContextItem(id);
|
|
4510
|
+
if (opts.json) return printJson(item);
|
|
4511
|
+
printContextItem(item);
|
|
4512
|
+
});
|
|
4418
4513
|
withCommon(
|
|
4419
4514
|
withScopeFlags(
|
|
4420
|
-
context.command("create").description("Create a context item scoped to a project, state, and/or issue").requiredOption("-k, --kind <kind>", "prompt, skill, or repo").requiredOption(
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4515
|
+
context.command("create").description("Create a context item scoped to a project, state, and/or issue").requiredOption("-k, --kind <kind>", "prompt, skill, or repo").requiredOption(
|
|
4516
|
+
"-n, --name <name>",
|
|
4517
|
+
"item name (slug-like for skills; the dedup/override key)"
|
|
4518
|
+
).option("-d, --description <text>", "one-liner shown in lists").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option(
|
|
4519
|
+
"--file <path>=@<local>",
|
|
4520
|
+
"skill file: workspace path = local file (repeatable)",
|
|
4521
|
+
collect,
|
|
4522
|
+
[]
|
|
4523
|
+
).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch to check out").option("--dir <dir>", "repo: checkout directory (defaults to the URL's basename)")
|
|
4524
|
+
)
|
|
4424
4525
|
).action(
|
|
4425
4526
|
async (opts) => {
|
|
4426
|
-
|
|
4527
|
+
if (opts.kind === "repo" && opts.repoUrl === void 0) {
|
|
4528
|
+
die("--kind repo needs --repo-url <clone-url> (--url is the API base URL)");
|
|
4529
|
+
}
|
|
4530
|
+
const api = client(opts);
|
|
4427
4531
|
const scope = await resolveScopeFlags(api, opts);
|
|
4428
4532
|
const body = {
|
|
4429
4533
|
kind: opts.kind,
|
|
@@ -4434,7 +4538,7 @@ function register(program3) {
|
|
|
4434
4538
|
if (opts.body !== void 0) body.body = readBodyValue(opts.body);
|
|
4435
4539
|
if (opts.file.length > 0) body.files = opts.file.map(parseFileSpec);
|
|
4436
4540
|
if (opts.kind === "skill" && body.files === void 0) body.files = [];
|
|
4437
|
-
if (opts.
|
|
4541
|
+
if (opts.repoUrl !== void 0) body.repo_url = opts.repoUrl;
|
|
4438
4542
|
if (opts.branch !== void 0) body.repo_branch = opts.branch;
|
|
4439
4543
|
if (opts.dir !== void 0) body.repo_dir = opts.dir;
|
|
4440
4544
|
const item = await api.createContextItem(body);
|
|
@@ -4444,17 +4548,20 @@ function register(program3) {
|
|
|
4444
4548
|
);
|
|
4445
4549
|
withCommon(
|
|
4446
4550
|
withScopeFlags(
|
|
4447
|
-
context.command("edit <id>").description("Edit a context item: payload, name, description, or scope").option("-n, --name <name>", "rename the item").option("-d, --description <text>", "set the description").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option("--file <path>=@<local>", "add or replace a skill file (repeatable)", collect, []).option("--remove-file <path>", "remove a skill file (repeatable)", collect, []).option("--url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch (empty string clears it)").option("--dir <dir>", "repo: checkout directory (empty string restores the URL default)").option(
|
|
4551
|
+
context.command("edit <id>").description("Edit a context item: payload, name, description, or scope").option("-n, --name <name>", "rename the item").option("-d, --description <text>", "set the description").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option("--file <path>=@<local>", "add or replace a skill file (repeatable)", collect, []).option("--remove-file <path>", "remove a skill file (repeatable)", collect, []).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch (empty string clears it)").option("--dir <dir>", "repo: checkout directory (empty string restores the URL default)").option(
|
|
4552
|
+
"--unset <dimension>",
|
|
4553
|
+
"drop a scope dimension: project, state, or issue (repeatable)",
|
|
4554
|
+
collect,
|
|
4555
|
+
[]
|
|
4556
|
+
).option(
|
|
4448
4557
|
"--expect-version <n>",
|
|
4449
4558
|
"fail (409) unless the item is still at this version",
|
|
4450
4559
|
(v) => Number.parseInt(v, 10)
|
|
4451
4560
|
)
|
|
4452
|
-
)
|
|
4453
|
-
// --url is the repo pointer here; the API base comes from TINES_API_URL.
|
|
4454
|
-
{ baseUrlFlag: false }
|
|
4561
|
+
)
|
|
4455
4562
|
).action(
|
|
4456
4563
|
async (id, opts) => {
|
|
4457
|
-
const api = client(
|
|
4564
|
+
const api = client(opts);
|
|
4458
4565
|
const body = {};
|
|
4459
4566
|
if (opts.expectVersion !== void 0) body.expected_version = opts.expectVersion;
|
|
4460
4567
|
if (opts.name !== void 0) body.name = opts.name;
|
|
@@ -4470,12 +4577,15 @@ function register(program3) {
|
|
|
4470
4577
|
if (opts.body !== void 0) body.body = readBodyValue(opts.body);
|
|
4471
4578
|
if (opts.file.length > 0 || opts.removeFile.length > 0) {
|
|
4472
4579
|
const current = await api.getContextItem(id);
|
|
4473
|
-
if (current.kind !== "skill")
|
|
4580
|
+
if (current.kind !== "skill")
|
|
4581
|
+
die(`--file/--remove-file only apply to skills (this is a ${current.kind})`);
|
|
4474
4582
|
if (body.expected_version === void 0) body.expected_version = current.version;
|
|
4475
4583
|
const files = new Map((current.files ?? []).map((f) => [f.path, f.content]));
|
|
4476
4584
|
for (const path2 of opts.removeFile) {
|
|
4477
4585
|
if (!files.delete(path2)) {
|
|
4478
|
-
die(
|
|
4586
|
+
die(
|
|
4587
|
+
`no file "${path2}" in skill "${current.name}" (have: ${[...files.keys()].join(", ") || "none"})`
|
|
4588
|
+
);
|
|
4479
4589
|
}
|
|
4480
4590
|
}
|
|
4481
4591
|
for (const spec of opts.file) {
|
|
@@ -4484,11 +4594,13 @@ function register(program3) {
|
|
|
4484
4594
|
}
|
|
4485
4595
|
body.files = [...files.entries()].map(([path2, content]) => ({ path: path2, content }));
|
|
4486
4596
|
}
|
|
4487
|
-
if (opts.
|
|
4597
|
+
if (opts.repoUrl !== void 0) body.repo_url = opts.repoUrl;
|
|
4488
4598
|
if (opts.branch !== void 0) body.repo_branch = opts.branch === "" ? null : opts.branch;
|
|
4489
4599
|
if (opts.dir !== void 0) body.repo_dir = opts.dir === "" ? null : opts.dir;
|
|
4490
4600
|
if (Object.keys(body).length === 0) {
|
|
4491
|
-
die(
|
|
4601
|
+
die(
|
|
4602
|
+
"nothing to update: pass payload flags, --name/--description, scope flags, and/or --unset"
|
|
4603
|
+
);
|
|
4492
4604
|
}
|
|
4493
4605
|
const item = await api.updateContextItem(id, body);
|
|
4494
4606
|
if (opts.json) return printJson(item);
|
|
@@ -4507,7 +4619,9 @@ function register(program3) {
|
|
|
4507
4619
|
context.command("init").description('Seed the global "agent-guidelines" prompt (a no-op if it already exists)')
|
|
4508
4620
|
).action(async (opts) => {
|
|
4509
4621
|
const api = client(opts);
|
|
4510
|
-
const items = await listAll(
|
|
4622
|
+
const items = await listAll(
|
|
4623
|
+
(page) => api.listContext({ kind: "prompt", exact: true, ...page })
|
|
4624
|
+
);
|
|
4511
4625
|
const existing = items.find((i) => i.name === AGENT_GUIDELINES_NAME);
|
|
4512
4626
|
if (existing) {
|
|
4513
4627
|
if (opts.json) return printJson(existing);
|
|
@@ -4529,8 +4643,8 @@ function register(program3) {
|
|
|
4529
4643
|
}
|
|
4530
4644
|
|
|
4531
4645
|
// src/commands/issues.ts
|
|
4532
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync as
|
|
4533
|
-
import { basename, dirname, join } from "node:path";
|
|
4646
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4647
|
+
import { basename, dirname as dirname2, join as join2 } from "node:path";
|
|
4534
4648
|
|
|
4535
4649
|
// src/help-guard.ts
|
|
4536
4650
|
function helpGuard(command, markdown) {
|
|
@@ -4569,12 +4683,15 @@ function buildRecurrence(opts) {
|
|
|
4569
4683
|
if (hourly !== void 0) {
|
|
4570
4684
|
if (opts.on !== void 0) throw new CliError("an hourly recurrence does not take --on");
|
|
4571
4685
|
const every = typeof hourly === "number" ? hourly : Number.parseInt(hourly, 10);
|
|
4572
|
-
if (every < 1 || every > 23)
|
|
4686
|
+
if (every < 1 || every > 23)
|
|
4687
|
+
throw new CliError(`--every <N>h needs N between 1 and 23, got "${opts.every}"`);
|
|
4573
4688
|
let minute = 0;
|
|
4574
4689
|
if (opts.at !== void 0) {
|
|
4575
4690
|
const m = opts.at.match(/^:?(\d{1,2})$/);
|
|
4576
4691
|
if (!m || Number.parseInt(m[1], 10) > 59) {
|
|
4577
|
-
throw new CliError(
|
|
4692
|
+
throw new CliError(
|
|
4693
|
+
`with an hourly recurrence, --at is the minute past the hour (0-59 or :MM), got "${opts.at}"`
|
|
4694
|
+
);
|
|
4578
4695
|
}
|
|
4579
4696
|
minute = Number.parseInt(m[1], 10);
|
|
4580
4697
|
}
|
|
@@ -4599,7 +4716,9 @@ function buildRecurrence(opts) {
|
|
|
4599
4716
|
return { preset: { kind: "monthly", time, day_of_month: day } };
|
|
4600
4717
|
}
|
|
4601
4718
|
default:
|
|
4602
|
-
throw new CliError(
|
|
4719
|
+
throw new CliError(
|
|
4720
|
+
`--every must be hourly, <N>h, daily, weekly, or monthly, got "${opts.every}"`
|
|
4721
|
+
);
|
|
4603
4722
|
}
|
|
4604
4723
|
}
|
|
4605
4724
|
|
|
@@ -4608,7 +4727,9 @@ var systemTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
4608
4727
|
function printIssueLinks(links) {
|
|
4609
4728
|
if (links.blocked_by.length > 0) {
|
|
4610
4729
|
console.log("\nblocked by:");
|
|
4611
|
-
table(
|
|
4730
|
+
table(
|
|
4731
|
+
linkRows(links.blocked_by, (e) => e.effective_state.category === "done" ? "" : "(open)")
|
|
4732
|
+
);
|
|
4612
4733
|
}
|
|
4613
4734
|
if (links.blocks.length > 0) {
|
|
4614
4735
|
console.log("\nblocks:");
|
|
@@ -4630,7 +4751,9 @@ function printIssueDetail(issue) {
|
|
|
4630
4751
|
`state: ${issue.effective_state.name} (${issue.effective_state.category})${dup ? ` (via ${issueRef(dup)} \u2014 duplicate)` : ""} workflow: ${issue.workflow.name} updated: ${timestamp(issue.updated_at)}`
|
|
4631
4752
|
);
|
|
4632
4753
|
if (dup) {
|
|
4633
|
-
console.log(
|
|
4754
|
+
console.log(
|
|
4755
|
+
`own state: ${issue.state.name} (${issue.state.category}) \u2014 dormant while this is a duplicate`
|
|
4756
|
+
);
|
|
4634
4757
|
}
|
|
4635
4758
|
console.log(`id: ${issue.id}`);
|
|
4636
4759
|
printIssueLinks(issue.links);
|
|
@@ -4639,8 +4762,10 @@ function printIssueDetail(issue) {
|
|
|
4639
4762
|
${issue.description}`);
|
|
4640
4763
|
}
|
|
4641
4764
|
const allowed = issue.allowed_transitions.map((t) => `"${t.name}" \u2192 ${t.to_state.name}`);
|
|
4642
|
-
console.log(
|
|
4643
|
-
|
|
4765
|
+
console.log(
|
|
4766
|
+
`
|
|
4767
|
+
allowed actions: ${allowed.length ? allowed.join(", ") : "none (terminal state)"}`
|
|
4768
|
+
);
|
|
4644
4769
|
if (issue.comments.length > 0) {
|
|
4645
4770
|
console.log(`
|
|
4646
4771
|
comments (${issue.comments.length}):`);
|
|
@@ -4653,11 +4778,15 @@ function walkFolder(dir) {
|
|
|
4653
4778
|
const files = [];
|
|
4654
4779
|
const walk = (abs, rel) => {
|
|
4655
4780
|
for (const entry of readdirSync(abs, { withFileTypes: true })) {
|
|
4656
|
-
const nextAbs =
|
|
4781
|
+
const nextAbs = join2(abs, entry.name);
|
|
4657
4782
|
const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
4658
4783
|
if (entry.isDirectory()) walk(nextAbs, nextRel);
|
|
4659
4784
|
else if (entry.isFile()) {
|
|
4660
|
-
files.push({
|
|
4785
|
+
files.push({
|
|
4786
|
+
path: nextRel,
|
|
4787
|
+
contentType: sniffContentType(entry.name),
|
|
4788
|
+
bytes: readFileSync4(nextAbs)
|
|
4789
|
+
});
|
|
4661
4790
|
}
|
|
4662
4791
|
}
|
|
4663
4792
|
};
|
|
@@ -4691,7 +4820,9 @@ matched rule: ${ex.matched_rule.scope_label}`);
|
|
|
4691
4820
|
);
|
|
4692
4821
|
}
|
|
4693
4822
|
if (ex.queue_position !== null && ex.queue_position > 0) {
|
|
4694
|
-
console.log(
|
|
4823
|
+
console.log(
|
|
4824
|
+
`queue: ${ex.queue_position} eligible issue${ex.queue_position === 1 ? "" : "s"} ahead of this one`
|
|
4825
|
+
);
|
|
4695
4826
|
}
|
|
4696
4827
|
if (ex.active_run) {
|
|
4697
4828
|
console.log(
|
|
@@ -4707,7 +4838,10 @@ matched rule: ${ex.matched_rule.scope_label}`);
|
|
|
4707
4838
|
function register2(program3) {
|
|
4708
4839
|
const issues = program3.command("issues").description("Work with issues");
|
|
4709
4840
|
withList(
|
|
4710
|
-
issues.command("list").description("List issues across projects (hides done issues unless --all)").option("-p, --project <name>", "filter by project name or id").option("-s, --state <name>", "filter by state name or id").option("-c, --category <cat>", "filter by state category").option("-w, --workflow <id-or-name>", "filter by workflow").option("-a, --all", "include issues in done states").option(
|
|
4841
|
+
issues.command("list").description("List issues across projects (hides done issues unless --all)").option("-p, --project <name>", "filter by project name or id").option("-s, --state <name>", "filter by state name or id").option("-c, --category <cat>", "filter by state category").option("-w, --workflow <id-or-name>", "filter by workflow").option("-a, --all", "include issues in done states").option(
|
|
4842
|
+
"--ready",
|
|
4843
|
+
"only issues that are actionable now (not done, not a duplicate, no open blockers)"
|
|
4844
|
+
).option("-q, --search <text>", "search titles and descriptions")
|
|
4711
4845
|
).action(
|
|
4712
4846
|
async (opts) => {
|
|
4713
4847
|
const api = client(opts);
|
|
@@ -4743,7 +4877,19 @@ function register2(program3) {
|
|
|
4743
4877
|
}
|
|
4744
4878
|
);
|
|
4745
4879
|
withCommon(
|
|
4746
|
-
issues.command("create <project>").description("Create an issue in a project, optionally with a recurrence (a scheduled task)").requiredOption(
|
|
4880
|
+
issues.command("create <project>").description("Create an issue in a project, optionally with a recurrence (a scheduled task)").requiredOption(
|
|
4881
|
+
"-t, --title <title>",
|
|
4882
|
+
"issue title (doubles as the title template with a recurrence)"
|
|
4883
|
+
).option("-d, --description <markdown>", `issue description (Markdown) \u2014 ${BODY_VALUE_HELP}`).option(
|
|
4884
|
+
"-w, --workflow <id-or-name>",
|
|
4885
|
+
"workflow (defaults to project default, else standard)"
|
|
4886
|
+
).option("-s, --state <name>", "starting state (defaults to the workflow's initial state)").option(
|
|
4887
|
+
"--every <preset>",
|
|
4888
|
+
'repeat hourly (or every N hours: "6h"), daily, weekly, or monthly'
|
|
4889
|
+
).option(
|
|
4890
|
+
"--at <when>",
|
|
4891
|
+
"preset time of day HH:MM (default 09:00); for hourly, the minute past the hour :MM (default :00)"
|
|
4892
|
+
).option("--on <when>", "weekday (weekly) or day of month (monthly)").option("--cron <expr>", "5-field cron expression (alternative to --every/--at/--on)").option("--tz <iana>", "schedule timezone (defaults to the system timezone)").option("--if-closed", "only create a new instance when all previous instances are closed").option("--schedule-name <name>", "schedule name, unique per project (defaults to the title)")
|
|
4747
4893
|
).action(
|
|
4748
4894
|
async (projectRef, opts) => {
|
|
4749
4895
|
const description = opts.description !== void 0 ? readBodyValue(opts.description) : void 0;
|
|
@@ -4799,7 +4945,8 @@ function register2(program3) {
|
|
|
4799
4945
|
if (opts.title !== void 0) body.title = opts.title;
|
|
4800
4946
|
if (description !== void 0) body.description = description;
|
|
4801
4947
|
if (opts.state !== void 0) body.state = opts.state;
|
|
4802
|
-
if (opts.workflow !== void 0)
|
|
4948
|
+
if (opts.workflow !== void 0)
|
|
4949
|
+
body.workflow_id = (await resolveWorkflow(api, opts.workflow)).id;
|
|
4803
4950
|
if (Object.keys(body).length === 0) {
|
|
4804
4951
|
die("nothing to update: pass --title, --description, --state, and/or --workflow");
|
|
4805
4952
|
}
|
|
@@ -4841,15 +4988,17 @@ function register2(program3) {
|
|
|
4841
4988
|
});
|
|
4842
4989
|
withCommon(
|
|
4843
4990
|
issues.command("comment-edit <ref> <comment-id> <markdown>").description(`Replace the body of your own comment \u2014 Markdown body: ${BODY_VALUE_HELP}`).passThroughOptions()
|
|
4844
|
-
).action(
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4991
|
+
).action(
|
|
4992
|
+
async (ref, commentId, markdown, opts, command) => {
|
|
4993
|
+
if (helpGuard(command, markdown)) return;
|
|
4994
|
+
const body = readBodyValue(markdown);
|
|
4995
|
+
const api = client(opts);
|
|
4996
|
+
const issue = await resolveIssue(api, ref);
|
|
4997
|
+
const comment = await api.updateComment(issue.id, commentId, { body });
|
|
4998
|
+
if (opts.json) return printJson(comment);
|
|
4999
|
+
console.log(`edited comment ${comment.id} on ${issue.project_name}/#${issue.number}`);
|
|
5000
|
+
}
|
|
5001
|
+
);
|
|
4853
5002
|
withCommon(
|
|
4854
5003
|
issues.command("comment-delete <ref> <comment-id>").description("Delete your own comment (the event keeps the record of the deletion)")
|
|
4855
5004
|
).action(async (ref, commentId, opts) => {
|
|
@@ -4902,7 +5051,12 @@ function register2(program3) {
|
|
|
4902
5051
|
);
|
|
4903
5052
|
});
|
|
4904
5053
|
withCommon(
|
|
4905
|
-
issues.command("context <ref>").description(
|
|
5054
|
+
issues.command("context <ref>").description(
|
|
5055
|
+
"Print an issue's effective context (the assembled bundle for its current state)"
|
|
5056
|
+
).option(
|
|
5057
|
+
"--out <dir>",
|
|
5058
|
+
"write the bundle to a directory: prompt.md, skills/<name>/\u2026, repos.json"
|
|
5059
|
+
).option("--force", "allow --out into a non-empty directory")
|
|
4906
5060
|
).action(async (ref, opts) => {
|
|
4907
5061
|
const api = client(opts);
|
|
4908
5062
|
const issue = await resolveIssue(api, ref);
|
|
@@ -4915,13 +5069,19 @@ function register2(program3) {
|
|
|
4915
5069
|
skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
4916
5070
|
}
|
|
4917
5071
|
for (const repo of context.repos) {
|
|
4918
|
-
console.log(
|
|
5072
|
+
console.log(
|
|
5073
|
+
`repo: ${repo.name} ${repo.url}${repo.branch ? `#${repo.branch}` : ""} \u2192 ${repo.dir}/`
|
|
5074
|
+
);
|
|
4919
5075
|
}
|
|
4920
5076
|
for (const o of context.overridden) {
|
|
4921
|
-
console.log(
|
|
5077
|
+
console.log(
|
|
5078
|
+
`overridden: ${o.kind} "${o.name}" [${o.scope.label}] (overridden by ${o.overridden_by})`
|
|
5079
|
+
);
|
|
4922
5080
|
}
|
|
4923
5081
|
for (const c of context.conflicts) {
|
|
4924
|
-
console.log(
|
|
5082
|
+
console.log(
|
|
5083
|
+
`conflict: repos ${c.item_ids.join(", ")} all resolve to checkout dir "${c.dir}"`
|
|
5084
|
+
);
|
|
4925
5085
|
}
|
|
4926
5086
|
return;
|
|
4927
5087
|
}
|
|
@@ -4930,20 +5090,23 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
|
4930
5090
|
`refusing to write: checkout-directory conflict${context.conflicts.length === 1 ? "" : "s"} among the effective repos (${context.conflicts.map((c) => `"${c.dir}": ${c.item_ids.join(", ")}`).join("; ")}); rename or re-dir the items first`
|
|
4931
5091
|
);
|
|
4932
5092
|
}
|
|
4933
|
-
if (
|
|
5093
|
+
if (existsSync2(opts.out) && readdirSync(opts.out).length > 0 && !opts.force) {
|
|
4934
5094
|
die(`refusing to write into non-empty directory ${opts.out} (pass --force to override)`);
|
|
4935
5095
|
}
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
5096
|
+
mkdirSync2(opts.out, { recursive: true });
|
|
5097
|
+
writeFileSync2(
|
|
5098
|
+
join2(opts.out, "prompt.md"),
|
|
5099
|
+
context.prompt.text ? `${context.prompt.text}
|
|
5100
|
+
` : ""
|
|
5101
|
+
);
|
|
4939
5102
|
for (const skill of context.skills) {
|
|
4940
5103
|
for (const file of skill.files) {
|
|
4941
|
-
const target =
|
|
4942
|
-
|
|
4943
|
-
|
|
5104
|
+
const target = join2(opts.out, "skills", skill.name, file.path);
|
|
5105
|
+
mkdirSync2(dirname2(target), { recursive: true });
|
|
5106
|
+
writeFileSync2(target, file.content);
|
|
4944
5107
|
}
|
|
4945
5108
|
}
|
|
4946
|
-
|
|
5109
|
+
writeFileSync2(join2(opts.out, "repos.json"), `${JSON.stringify(context.repos, null, 2)}
|
|
4947
5110
|
`);
|
|
4948
5111
|
console.log(
|
|
4949
5112
|
`wrote ${opts.out}/prompt.md, ${context.skills.length} skill${context.skills.length === 1 ? "" : "s"}, repos.json (${context.repos.length} repo${context.repos.length === 1 ? "" : "s"})`
|
|
@@ -4971,27 +5134,29 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
|
4971
5134
|
if (opts.json) return printJson(prompt);
|
|
4972
5135
|
console.log(prompt.text);
|
|
4973
5136
|
});
|
|
4974
|
-
const artifactsCmd = issues.command("artifacts").description(
|
|
4975
|
-
|
|
4976
|
-
async (ref, opts) => {
|
|
4977
|
-
const api = client(opts);
|
|
4978
|
-
const issue = await resolveIssue(api, ref);
|
|
4979
|
-
const res = await api.listArtifacts(issue.id);
|
|
4980
|
-
if (opts.json) return printJson(res);
|
|
4981
|
-
if (res.items.length === 0) return console.log("no artifacts attached");
|
|
4982
|
-
table([
|
|
4983
|
-
["NAME", "TYPE", "VERSION", "FRESH", "SUMMARY", "ATTACHED"],
|
|
4984
|
-
...res.items.map((a) => [
|
|
4985
|
-
a.name,
|
|
4986
|
-
a.artifact_type,
|
|
4987
|
-
`v${a.current_version.version}`,
|
|
4988
|
-
a.fresh ? "yes" : "no",
|
|
4989
|
-
artifactSummary(a),
|
|
4990
|
-
timestamp(a.current_version.created_at)
|
|
4991
|
-
])
|
|
4992
|
-
]);
|
|
4993
|
-
}
|
|
5137
|
+
const artifactsCmd = issues.command("artifacts").description(
|
|
5138
|
+
"Typed, versioned attachments on an issue \u2014 the work products transition requirements gate on"
|
|
4994
5139
|
);
|
|
5140
|
+
withCommon(
|
|
5141
|
+
artifactsCmd.command("list <ref>").description("List the artifacts attached to an issue")
|
|
5142
|
+
).action(async (ref, opts) => {
|
|
5143
|
+
const api = client(opts);
|
|
5144
|
+
const issue = await resolveIssue(api, ref);
|
|
5145
|
+
const res = await api.listArtifacts(issue.id);
|
|
5146
|
+
if (opts.json) return printJson(res);
|
|
5147
|
+
if (res.items.length === 0) return console.log("no artifacts attached");
|
|
5148
|
+
table([
|
|
5149
|
+
["NAME", "TYPE", "VERSION", "FRESH", "SUMMARY", "ATTACHED"],
|
|
5150
|
+
...res.items.map((a) => [
|
|
5151
|
+
a.name,
|
|
5152
|
+
a.artifact_type,
|
|
5153
|
+
`v${a.current_version.version}`,
|
|
5154
|
+
a.fresh ? "yes" : "no",
|
|
5155
|
+
artifactSummary(a),
|
|
5156
|
+
timestamp(a.current_version.created_at)
|
|
5157
|
+
])
|
|
5158
|
+
]);
|
|
5159
|
+
});
|
|
4995
5160
|
withCommon(
|
|
4996
5161
|
artifactsCmd.command("show <ref> <name>").description("Show an artifact with its full version history")
|
|
4997
5162
|
).action(async (ref, name2, opts) => {
|
|
@@ -4999,7 +5164,9 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
|
4999
5164
|
const issue = await resolveIssue(api, ref);
|
|
5000
5165
|
const artifact = await api.getArtifact(issue.id, name2);
|
|
5001
5166
|
if (opts.json) return printJson(artifact);
|
|
5002
|
-
console.log(
|
|
5167
|
+
console.log(
|
|
5168
|
+
`${artifact.artifact_type} artifact "${artifact.name}" on ${issue.project_name}/${issue.number}`
|
|
5169
|
+
);
|
|
5003
5170
|
if (artifact.description) console.log(artifact.description);
|
|
5004
5171
|
console.log(
|
|
5005
5172
|
`current: v${artifact.current_version.version} (${artifact.fresh ? "fresh" : "attached before the current state \u2014 reaffirm or attach a new version to satisfy gates"})`
|
|
@@ -5022,25 +5189,27 @@ files (v${artifact.current_version.version}):`);
|
|
|
5022
5189
|
}
|
|
5023
5190
|
});
|
|
5024
5191
|
withCommon(
|
|
5025
|
-
artifactsCmd.command("attach <ref> <name>").description(
|
|
5192
|
+
artifactsCmd.command("attach <ref> <name>").description(
|
|
5193
|
+
"Attach content to a named artifact slot (creates it, or appends the next version)"
|
|
5194
|
+
).option("-f, --file <path>", "upload a file (MIME sniffed from the extension)").option(
|
|
5026
5195
|
"--folder <dir>",
|
|
5027
5196
|
"snapshot a directory tree as one version (collect locally, attach once; MIME per file sniffed)"
|
|
5028
|
-
).option("-t, --text <md|@file>", "inline text document: inline Markdown or @file").option("--
|
|
5029
|
-
// --url is the link payload here; the API base comes from TINES_API_URL.
|
|
5030
|
-
{ baseUrlFlag: false }
|
|
5197
|
+
).option("-t, --text <md|@file>", "inline text document: inline Markdown or @file").option("--link <url>", "link: the URL to attach").option("--pr <spec>", "PR reference: owner/repo#N or a GitHub PR URL").option("--content-type <mime>", "declared MIME type (with --file or --text)").option("--filename <name>", "display filename (with --text; defaults to <name>.md)").option("--title <title>", "display title (with --link)").option("-d, --description <text>", "artifact description, shown in lists and launch prompts")
|
|
5031
5198
|
).action(
|
|
5032
5199
|
async (ref, name2, opts) => {
|
|
5033
|
-
const api = client(
|
|
5034
|
-
const sources = [opts.file, opts.folder, opts.text, opts.
|
|
5200
|
+
const api = client(opts);
|
|
5201
|
+
const sources = [opts.file, opts.folder, opts.text, opts.link, opts.pr].filter(
|
|
5202
|
+
(v) => v !== void 0
|
|
5203
|
+
);
|
|
5035
5204
|
if (sources.length !== 1) {
|
|
5036
5205
|
die(
|
|
5037
|
-
"pass exactly one content source: --file <path>, --folder <dir>, --text <md|@file>, --
|
|
5206
|
+
"pass exactly one content source: --file <path>, --folder <dir>, --text <md|@file>, --link <url>, or --pr <spec> (a link goes in --link; --url is the API base URL)"
|
|
5038
5207
|
);
|
|
5039
5208
|
}
|
|
5040
5209
|
const issue = await resolveIssue(api, ref);
|
|
5041
5210
|
let artifact;
|
|
5042
5211
|
if (opts.folder !== void 0) {
|
|
5043
|
-
if (!
|
|
5212
|
+
if (!existsSync2(opts.folder) || !statSync(opts.folder).isDirectory()) {
|
|
5044
5213
|
die(`--folder needs a directory, got "${opts.folder}"`);
|
|
5045
5214
|
}
|
|
5046
5215
|
const files = walkFolder(opts.folder);
|
|
@@ -5052,7 +5221,7 @@ files (v${artifact.current_version.version}):`);
|
|
|
5052
5221
|
} else if (opts.file !== void 0) {
|
|
5053
5222
|
let bytes;
|
|
5054
5223
|
try {
|
|
5055
|
-
bytes =
|
|
5224
|
+
bytes = readFileSync4(opts.file);
|
|
5056
5225
|
} catch (err) {
|
|
5057
5226
|
die(`cannot read ${opts.file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5058
5227
|
}
|
|
@@ -5071,10 +5240,10 @@ files (v${artifact.current_version.version}):`);
|
|
|
5071
5240
|
...opts.contentType !== void 0 ? { content_type: opts.contentType } : {},
|
|
5072
5241
|
...opts.description !== void 0 ? { description: opts.description } : {}
|
|
5073
5242
|
});
|
|
5074
|
-
} else if (opts.
|
|
5243
|
+
} else if (opts.link !== void 0) {
|
|
5075
5244
|
artifact = await api.putArtifact(issue.id, name2, {
|
|
5076
5245
|
type: "link",
|
|
5077
|
-
url: opts.
|
|
5246
|
+
url: opts.link,
|
|
5078
5247
|
...opts.title !== void 0 ? { title: opts.title } : {},
|
|
5079
5248
|
...opts.description !== void 0 ? { description: opts.description } : {}
|
|
5080
5249
|
});
|
|
@@ -5097,7 +5266,9 @@ files (v${artifact.current_version.version}):`);
|
|
|
5097
5266
|
}
|
|
5098
5267
|
);
|
|
5099
5268
|
withCommon(
|
|
5100
|
-
artifactsCmd.command("reaffirm <ref> <name>").description(
|
|
5269
|
+
artifactsCmd.command("reaffirm <ref> <name>").description(
|
|
5270
|
+
"Bless the current content as fresh (appends a version reusing the same payload)"
|
|
5271
|
+
)
|
|
5101
5272
|
).action(async (ref, name2, opts) => {
|
|
5102
5273
|
const api = client(opts);
|
|
5103
5274
|
const issue = await resolveIssue(api, ref);
|
|
@@ -5108,7 +5279,14 @@ files (v${artifact.current_version.version}):`);
|
|
|
5108
5279
|
);
|
|
5109
5280
|
});
|
|
5110
5281
|
withCommon(
|
|
5111
|
-
artifactsCmd.command("get <ref> <name>").description("Fetch content (current version by default); a link/pr prints its URL").option(
|
|
5282
|
+
artifactsCmd.command("get <ref> <name>").description("Fetch content (current version by default); a link/pr prints its URL").option(
|
|
5283
|
+
"--version <n>",
|
|
5284
|
+
"fetch a specific version from the history",
|
|
5285
|
+
(v) => Number.parseInt(v, 10)
|
|
5286
|
+
).option(
|
|
5287
|
+
"--out <path>",
|
|
5288
|
+
"write to this file, or into this directory (keeps the stored filename)"
|
|
5289
|
+
)
|
|
5112
5290
|
).action(
|
|
5113
5291
|
async (ref, name2, opts) => {
|
|
5114
5292
|
const api = client(opts);
|
|
@@ -5129,7 +5307,7 @@ files (v${artifact.current_version.version}):`);
|
|
|
5129
5307
|
if (opts.out === void 0) {
|
|
5130
5308
|
die(`artifact "${name2}" is a folder \u2014 pass --out <dir> to write its tree`);
|
|
5131
5309
|
}
|
|
5132
|
-
if (
|
|
5310
|
+
if (existsSync2(opts.out) && !statSync(opts.out).isDirectory()) {
|
|
5133
5311
|
die(`--out for a folder must be a directory, and "${opts.out}" is a file`);
|
|
5134
5312
|
}
|
|
5135
5313
|
const files = version.files ?? [];
|
|
@@ -5139,9 +5317,9 @@ files (v${artifact.current_version.version}):`);
|
|
|
5139
5317
|
version: opts.version,
|
|
5140
5318
|
path: file.path
|
|
5141
5319
|
});
|
|
5142
|
-
const target2 =
|
|
5143
|
-
|
|
5144
|
-
|
|
5320
|
+
const target2 = join2(opts.out, file.path);
|
|
5321
|
+
mkdirSync2(dirname2(target2), { recursive: true });
|
|
5322
|
+
writeFileSync2(target2, Buffer.from(content2.bytes));
|
|
5145
5323
|
total += content2.bytes.byteLength;
|
|
5146
5324
|
}
|
|
5147
5325
|
return console.log(
|
|
@@ -5152,10 +5330,10 @@ files (v${artifact.current_version.version}):`);
|
|
|
5152
5330
|
const bytes = Buffer.from(content.bytes);
|
|
5153
5331
|
if (opts.out !== void 0) {
|
|
5154
5332
|
let target2 = opts.out;
|
|
5155
|
-
if (
|
|
5156
|
-
target2 =
|
|
5333
|
+
if (existsSync2(target2) && statSync(target2).isDirectory()) {
|
|
5334
|
+
target2 = join2(target2, version.filename ?? name2);
|
|
5157
5335
|
}
|
|
5158
|
-
|
|
5336
|
+
writeFileSync2(target2, bytes);
|
|
5159
5337
|
return console.log(`wrote ${target2} (${bytes.byteLength} bytes, ${content.content_type})`);
|
|
5160
5338
|
}
|
|
5161
5339
|
if ((content.content_type ?? "").startsWith("text/")) {
|
|
@@ -5163,12 +5341,14 @@ files (v${artifact.current_version.version}):`);
|
|
|
5163
5341
|
return;
|
|
5164
5342
|
}
|
|
5165
5343
|
const target = version.filename ?? name2;
|
|
5166
|
-
|
|
5344
|
+
writeFileSync2(target, bytes);
|
|
5167
5345
|
console.log(`wrote ${target} (${bytes.byteLength} bytes, ${content.content_type})`);
|
|
5168
5346
|
}
|
|
5169
5347
|
);
|
|
5170
5348
|
withCommon(
|
|
5171
|
-
artifactsCmd.command("delete <ref> <name>").description(
|
|
5349
|
+
artifactsCmd.command("delete <ref> <name>").description(
|
|
5350
|
+
"Delete an artifact \u2014 every version and its stored files (history is not recoverable)"
|
|
5351
|
+
)
|
|
5172
5352
|
).action(async (ref, name2, opts) => {
|
|
5173
5353
|
const api = client(opts);
|
|
5174
5354
|
const issue = await resolveIssue(api, ref);
|
|
@@ -5179,30 +5359,38 @@ files (v${artifact.current_version.version}):`);
|
|
|
5179
5359
|
);
|
|
5180
5360
|
});
|
|
5181
5361
|
withCommon(
|
|
5182
|
-
issues.command("assign <ref> [runner]").description(
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
const
|
|
5189
|
-
if (opts.
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5362
|
+
issues.command("assign <ref> [runner]").description(
|
|
5363
|
+
"Pin an issue to a runner (<runner>[:tier]) \u2014 replaces routing rules for it; --clear unpins"
|
|
5364
|
+
).option("--clear", "remove the pin")
|
|
5365
|
+
).action(
|
|
5366
|
+
async (ref, runnerSpec, opts) => {
|
|
5367
|
+
const api = client(opts);
|
|
5368
|
+
const issue = await resolveIssue(api, ref);
|
|
5369
|
+
if (opts.clear) {
|
|
5370
|
+
if (runnerSpec !== void 0) die("--clear does not take a runner");
|
|
5371
|
+
const updated2 = await api.updateIssue(issue.id, { pinned_runner_id: null });
|
|
5372
|
+
if (opts.json) return printJson(updated2);
|
|
5373
|
+
return console.log(
|
|
5374
|
+
`unpinned ${updated2.project_name}/#${updated2.number} \u2014 routing rules apply again`
|
|
5375
|
+
);
|
|
5376
|
+
}
|
|
5377
|
+
if (runnerSpec === void 0) die("pass <runner>[:tier] to pin, or --clear to unpin");
|
|
5378
|
+
const { name: name2, tier } = parseTargetSpec(runnerSpec);
|
|
5379
|
+
const runner = await resolveRunner(api, name2);
|
|
5380
|
+
const updated = await api.updateIssue(issue.id, {
|
|
5381
|
+
pinned_runner_id: runner.id,
|
|
5382
|
+
pinned_tier: tier ?? null
|
|
5383
|
+
});
|
|
5384
|
+
if (opts.json) return printJson(updated);
|
|
5385
|
+
console.log(
|
|
5386
|
+
`pinned ${updated.project_name}/#${updated.number} to ${runner.name}${tier ? ` (tier ${tier})` : ""} \u2014 only this runner will take it`
|
|
5387
|
+
);
|
|
5388
|
+
}
|
|
5389
|
+
);
|
|
5204
5390
|
withCommon(
|
|
5205
|
-
issues.command("dispatch <ref>").description(
|
|
5391
|
+
issues.command("dispatch <ref>").description(
|
|
5392
|
+
"Explain why an issue is (not) dispatching: eligibility, routing, per-runner verdicts"
|
|
5393
|
+
)
|
|
5206
5394
|
).action(async (ref, opts) => {
|
|
5207
5395
|
const api = client(opts);
|
|
5208
5396
|
const issue = await resolveIssue(api, ref);
|
|
@@ -5270,7 +5458,9 @@ function printNote(note) {
|
|
|
5270
5458
|
if (note) console.error(`note: ${note}`);
|
|
5271
5459
|
}
|
|
5272
5460
|
function register3(program3) {
|
|
5273
|
-
const journal = program3.command("journal").description(
|
|
5461
|
+
const journal = program3.command("journal").description(
|
|
5462
|
+
"An issue's stage journal: shared notes for its project + the stage your run was launched in"
|
|
5463
|
+
);
|
|
5274
5464
|
withCommon(
|
|
5275
5465
|
journal.command("show <ref>").description("Print the journal for the stage your run was launched in").option("--state <workflow>/<state>", STATE_FLAG_HELP)
|
|
5276
5466
|
).action(async (ref, opts) => {
|
|
@@ -5329,29 +5519,96 @@ start one: tines journal append ${issue.project_name}/${issue.number} "- <date>:
|
|
|
5329
5519
|
"the version being replaced (from the prompt or journal show)",
|
|
5330
5520
|
(v) => Number.parseInt(v, 10)
|
|
5331
5521
|
)
|
|
5332
|
-
).action(
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5522
|
+
).action(
|
|
5523
|
+
async (ref, opts) => {
|
|
5524
|
+
const api = client(opts);
|
|
5525
|
+
const { scope, note, item } = await resolveJournal(api, ref, opts.state);
|
|
5526
|
+
printNote(note);
|
|
5527
|
+
if (!item) die(`no journal exists yet for ${scope.label}; nothing to rewrite`);
|
|
5528
|
+
const updated = await api.updateContextItem(item.id, {
|
|
5529
|
+
body: readBodyValue(opts.body),
|
|
5530
|
+
expected_version: opts.expectVersion
|
|
5531
|
+
});
|
|
5532
|
+
if (opts.json) return printJson(updated);
|
|
5533
|
+
console.log(`rewrote the ${scope.label} journal (now v${updated.version})`);
|
|
5534
|
+
}
|
|
5535
|
+
);
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5538
|
+
// src/commands/login.ts
|
|
5539
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
5540
|
+
function maskKey(key) {
|
|
5541
|
+
return `${key.slice(0, 14)}\u2026`;
|
|
5542
|
+
}
|
|
5543
|
+
function readKeyArg(value) {
|
|
5544
|
+
if (value !== "-") return value;
|
|
5545
|
+
const key = readFileSync5(0, "utf8").trim();
|
|
5546
|
+
if (!key) die("no API key on stdin");
|
|
5547
|
+
return key;
|
|
5548
|
+
}
|
|
5549
|
+
function register4(program3) {
|
|
5550
|
+
program3.command("login").description("Store the API URL and key for this machine, so commands need no env vars").option("-u, --url <url>", `base URL of the Tines API to store (default ${DEFAULT_URL})`).option("--api-key <key>", 'API key from Settings \u2192 API keys ("-" reads it from stdin)').option("--no-verify", "store without checking the key against the API").action(async (opts) => {
|
|
5551
|
+
if (!opts.url && !opts.apiKey) die("pass --url and/or --api-key");
|
|
5552
|
+
const dir = defaultConfigDir();
|
|
5553
|
+
const apiKey = opts.apiKey ? readKeyArg(opts.apiKey) : void 0;
|
|
5554
|
+
const url = (opts.url ?? resolveUrlSetting({}).value ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
5555
|
+
const key = apiKey ?? resolveApiKeySetting({}).value;
|
|
5556
|
+
if (opts.verify && key) {
|
|
5557
|
+
try {
|
|
5558
|
+
await createApiClient({ baseUrl: url, apiKey: key }).listProjects({ limit: 1 });
|
|
5559
|
+
} catch (err) {
|
|
5560
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
5561
|
+
die(`${url} rejected the API key (${err.message}); nothing stored`);
|
|
5562
|
+
}
|
|
5563
|
+
if (err instanceof ApiError) throw err;
|
|
5564
|
+
const cause = err.cause?.code;
|
|
5565
|
+
die(
|
|
5566
|
+
`could not reach ${url}${cause ? ` (${cause})` : ""}; nothing stored (pass --no-verify to store anyway)`
|
|
5567
|
+
);
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
const stored = saveCliConfig(dir, { url: opts.url, api_key: apiKey });
|
|
5571
|
+
console.log(`stored in ${configPath(dir)}`);
|
|
5572
|
+
console.log(`url: ${stored.url ?? `${DEFAULT_URL} (default)`}`);
|
|
5573
|
+
console.log(`api key: ${stored.api_key ? maskKey(stored.api_key) : "none"}`);
|
|
5574
|
+
});
|
|
5575
|
+
program3.command("logout").description("Forget the stored API URL and key").action(() => {
|
|
5576
|
+
const dir = defaultConfigDir();
|
|
5577
|
+
console.log(
|
|
5578
|
+
clearCliConfig(dir) ? `removed ${configPath(dir)}` : `nothing stored in ${configPath(dir)}`
|
|
5579
|
+
);
|
|
5580
|
+
});
|
|
5581
|
+
withCommon(
|
|
5582
|
+
program3.command("config").description("Show the API URL and key in effect, and where each comes from")
|
|
5583
|
+
).action((opts) => {
|
|
5584
|
+
const url = resolveUrlSetting(opts);
|
|
5585
|
+
const key = resolveApiKeySetting(opts);
|
|
5586
|
+
const report = {
|
|
5587
|
+
url: { value: url.value, source: url.source },
|
|
5588
|
+
api_key: {
|
|
5589
|
+
value: key.value ? maskKey(key.value) : null,
|
|
5590
|
+
source: key.value ? key.source : null
|
|
5591
|
+
},
|
|
5592
|
+
config_path: configPath(defaultConfigDir())
|
|
5593
|
+
};
|
|
5594
|
+
if (opts.json) return printJson(report);
|
|
5595
|
+
console.log(`url: ${report.url.value} (${report.url.source})`);
|
|
5596
|
+
console.log(
|
|
5597
|
+
report.api_key.value ? `api key: ${report.api_key.value} (${report.api_key.source})` : "api key: none (pass --api-key, set TINES_API_KEY, or run `tines login`)"
|
|
5598
|
+
);
|
|
5599
|
+
console.log(`config file: ${report.config_path}`);
|
|
5343
5600
|
});
|
|
5344
5601
|
}
|
|
5345
5602
|
|
|
5346
5603
|
// src/commands/misc.ts
|
|
5347
5604
|
function registerTime(program3) {
|
|
5348
|
-
withCommon(
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
}
|
|
5354
|
-
);
|
|
5605
|
+
withCommon(
|
|
5606
|
+
program3.command("time").description("Fetch the current time from the Tines API")
|
|
5607
|
+
).action(async (opts) => {
|
|
5608
|
+
const result = await client(opts).getTime();
|
|
5609
|
+
if (opts.json) printJson(result);
|
|
5610
|
+
else console.log(`Server time: ${result.time} (unix ${result.unix})`);
|
|
5611
|
+
});
|
|
5355
5612
|
}
|
|
5356
5613
|
function registerEvents(program3) {
|
|
5357
5614
|
const events = program3.command("events").description("Read the activity log");
|
|
@@ -5380,7 +5637,7 @@ function registerEvents(program3) {
|
|
|
5380
5637
|
}
|
|
5381
5638
|
|
|
5382
5639
|
// src/commands/projects.ts
|
|
5383
|
-
function
|
|
5640
|
+
function register5(program3) {
|
|
5384
5641
|
const projects = program3.command("projects").description("Manage projects");
|
|
5385
5642
|
withList(projects.command("list").description("List projects")).action(async (opts) => {
|
|
5386
5643
|
const api = client(opts);
|
|
@@ -5394,7 +5651,10 @@ function register4(program3) {
|
|
|
5394
5651
|
});
|
|
5395
5652
|
});
|
|
5396
5653
|
withCommon(
|
|
5397
|
-
projects.command("create <name>").description("Create a project (with its initial context prompt)").option("-d, --description <text>", "project description").option("-w, --default-workflow <id-or-name>", "default workflow for new issues").option(
|
|
5654
|
+
projects.command("create <name>").description("Create a project (with its initial context prompt)").option("-d, --description <text>", "project description").option("-w, --default-workflow <id-or-name>", "default workflow for new issues").option(
|
|
5655
|
+
"--prompt <md>",
|
|
5656
|
+
"initial conventions prompt, stitched into every issue's agent prompt: inline Markdown or @file"
|
|
5657
|
+
).option("--no-prompt", "create without an initial prompt")
|
|
5398
5658
|
).action(
|
|
5399
5659
|
async (name2, opts) => {
|
|
5400
5660
|
if (opts.prompt === void 0 || opts.prompt === true) {
|
|
@@ -5471,8 +5731,10 @@ async function resolveRoutingScope(api, opts) {
|
|
|
5471
5731
|
if (opts.state) parts.push(`state ${opts.state}`);
|
|
5472
5732
|
return { projectId, stateId, label: parts.length > 0 ? parts.join(" \xB7 ") : "global" };
|
|
5473
5733
|
}
|
|
5474
|
-
function
|
|
5475
|
-
const routing = program3.command("routing").description(
|
|
5734
|
+
function register6(program3) {
|
|
5735
|
+
const routing = program3.command("routing").description(
|
|
5736
|
+
"Scoped routing rules: which runner takes which issues (most specific scope wins)"
|
|
5737
|
+
);
|
|
5476
5738
|
withCommon(routing.command("list").description("List routing rules, most specific first")).action(
|
|
5477
5739
|
async (opts) => {
|
|
5478
5740
|
const res = await client(opts).listRoutingRules();
|
|
@@ -5485,7 +5747,9 @@ function register5(program3) {
|
|
|
5485
5747
|
}
|
|
5486
5748
|
);
|
|
5487
5749
|
withCommon(
|
|
5488
|
-
routing.command("set <target...>").description(
|
|
5750
|
+
routing.command("set <target...>").description(
|
|
5751
|
+
"Create or replace the rule at a scope: an ordered list of <runner>[:tier] targets"
|
|
5752
|
+
).option("-p, --project <name>", "scope: project name or id").option("-s, --state <workflow/state>", "scope: workflow-qualified state")
|
|
5489
5753
|
).action(async (targetSpecs, opts) => {
|
|
5490
5754
|
const api = client(opts);
|
|
5491
5755
|
const scope = await resolveRoutingScope(api, opts);
|
|
@@ -5499,7 +5763,11 @@ function register5(program3) {
|
|
|
5499
5763
|
const existing = items.find(
|
|
5500
5764
|
(r) => r.scope.project_id === scope.projectId && r.scope.workflow_state_id === scope.stateId
|
|
5501
5765
|
);
|
|
5502
|
-
const rule = existing ? await api.updateRoutingRule(existing.id, { targets }) : await api.createRoutingRule({
|
|
5766
|
+
const rule = existing ? await api.updateRoutingRule(existing.id, { targets }) : await api.createRoutingRule({
|
|
5767
|
+
project_id: scope.projectId,
|
|
5768
|
+
workflow_state_id: scope.stateId,
|
|
5769
|
+
targets
|
|
5770
|
+
});
|
|
5503
5771
|
if (opts.json) return printJson(rule);
|
|
5504
5772
|
console.log(
|
|
5505
5773
|
`${existing ? "updated" : "created"} the ${rule.scope.label} rule: ${ruleTargetsLabel(rule)}`
|
|
@@ -5530,21 +5798,21 @@ import {
|
|
|
5530
5798
|
createWriteStream,
|
|
5531
5799
|
existsSync as existsSync4,
|
|
5532
5800
|
mkdirSync as mkdirSync4,
|
|
5533
|
-
readFileSync as
|
|
5801
|
+
readFileSync as readFileSync9,
|
|
5534
5802
|
rmSync as rmSync2,
|
|
5535
5803
|
statSync as statSync3,
|
|
5536
|
-
unlinkSync,
|
|
5804
|
+
unlinkSync as unlinkSync2,
|
|
5537
5805
|
writeFileSync as writeFileSync3
|
|
5538
5806
|
} from "node:fs";
|
|
5539
5807
|
import { hostname, platform, arch } from "node:os";
|
|
5540
|
-
import { dirname as dirname4, join as
|
|
5808
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
5541
5809
|
|
|
5542
5810
|
// src/version.ts
|
|
5543
|
-
import { readFileSync as
|
|
5811
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
5544
5812
|
function cliVersion() {
|
|
5545
5813
|
try {
|
|
5546
5814
|
const manifest = new URL("../package.json", import.meta.url);
|
|
5547
|
-
return JSON.parse(
|
|
5815
|
+
return JSON.parse(readFileSync6(manifest, "utf8")).version ?? "0.0.0-unknown";
|
|
5548
5816
|
} catch {
|
|
5549
5817
|
return "0.0.0-unknown";
|
|
5550
5818
|
}
|
|
@@ -5592,7 +5860,8 @@ function renderStreamEvent(event) {
|
|
|
5592
5860
|
case "result": {
|
|
5593
5861
|
const parts = [];
|
|
5594
5862
|
if (typeof event.num_turns === "number") parts.push(`${event.num_turns} turns`);
|
|
5595
|
-
if (typeof event.total_cost_usd === "number")
|
|
5863
|
+
if (typeof event.total_cost_usd === "number")
|
|
5864
|
+
parts.push(`$${event.total_cost_usd.toFixed(2)}`);
|
|
5596
5865
|
const detail = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
5597
5866
|
const lines = [`[session] result: ${event.subtype ?? "done"}${detail}`];
|
|
5598
5867
|
if (event.is_error && event.result) lines.push(`[error] ${clip(event.result, 2e3)}`);
|
|
@@ -5647,8 +5916,8 @@ var ClaudeStreamRenderer = class {
|
|
|
5647
5916
|
|
|
5648
5917
|
// src/daemon/cli-refresh.ts
|
|
5649
5918
|
import { spawn } from "node:child_process";
|
|
5650
|
-
import { existsSync as
|
|
5651
|
-
import { dirname as
|
|
5919
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
5920
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
5652
5921
|
|
|
5653
5922
|
// src/daemon/support.ts
|
|
5654
5923
|
import { delimiter } from "node:path";
|
|
@@ -5923,14 +6192,14 @@ function buildSpawnEnv(base, opts) {
|
|
|
5923
6192
|
var PACKAGE = "tines";
|
|
5924
6193
|
var NPM_ARGS = ["--min-release-age=0", "--no-audit", "--no-fund", "--loglevel=error"];
|
|
5925
6194
|
function agentCliPrefix(configDir) {
|
|
5926
|
-
return
|
|
6195
|
+
return join3(configDir, "cli");
|
|
5927
6196
|
}
|
|
5928
6197
|
function binDirOf(prefix) {
|
|
5929
|
-
return
|
|
6198
|
+
return join3(prefix, "node_modules", ".bin");
|
|
5930
6199
|
}
|
|
5931
6200
|
function installedVersion(prefix) {
|
|
5932
6201
|
try {
|
|
5933
|
-
const pkg =
|
|
6202
|
+
const pkg = readFileSync7(join3(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
|
|
5934
6203
|
const version = JSON.parse(pkg).version;
|
|
5935
6204
|
return typeof version === "string" ? version : null;
|
|
5936
6205
|
} catch {
|
|
@@ -5939,7 +6208,7 @@ function installedVersion(prefix) {
|
|
|
5939
6208
|
}
|
|
5940
6209
|
function lastGood(prefix) {
|
|
5941
6210
|
const binDir = binDirOf(prefix);
|
|
5942
|
-
if (!
|
|
6211
|
+
if (!existsSync3(join3(binDir, PACKAGE))) return AMBIENT_CLI;
|
|
5943
6212
|
return { binDir, version: installedVersion(prefix), source: "stale" };
|
|
5944
6213
|
}
|
|
5945
6214
|
function runNpm(file, args, cwd, timeoutMs) {
|
|
@@ -5976,7 +6245,7 @@ async function installAgentCli(opts) {
|
|
|
5976
6245
|
const prefix = agentCliPrefix(opts.configDir);
|
|
5977
6246
|
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
5978
6247
|
try {
|
|
5979
|
-
|
|
6248
|
+
mkdirSync3(prefix, { recursive: true });
|
|
5980
6249
|
} catch (err) {
|
|
5981
6250
|
opts.log(`agent CLI refresh: cannot create ${prefix} (${message(err)})`);
|
|
5982
6251
|
return AMBIENT_CLI;
|
|
@@ -5984,8 +6253,8 @@ async function installAgentCli(opts) {
|
|
|
5984
6253
|
const args = ["install", "--prefix", prefix, `${PACKAGE}@latest`, ...NPM_ARGS];
|
|
5985
6254
|
let result = await runNpm("npm", args, prefix, timeoutMs);
|
|
5986
6255
|
if (result.spawnError?.code === "ENOENT") {
|
|
5987
|
-
const sibling =
|
|
5988
|
-
if (
|
|
6256
|
+
const sibling = join3(dirname3(process.execPath), "npm");
|
|
6257
|
+
if (existsSync3(sibling)) result = await runNpm(sibling, args, prefix, timeoutMs);
|
|
5989
6258
|
}
|
|
5990
6259
|
if (result.code !== 0) {
|
|
5991
6260
|
const fallback = lastGood(prefix);
|
|
@@ -6007,38 +6276,13 @@ function message(err) {
|
|
|
6007
6276
|
}
|
|
6008
6277
|
|
|
6009
6278
|
// src/daemon/store.ts
|
|
6010
|
-
import {
|
|
6011
|
-
|
|
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";
|
|
6019
|
-
import { homedir } from "node:os";
|
|
6020
|
-
import { dirname as dirname3, join as join3 } from "node:path";
|
|
6021
|
-
function defaultConfigDir() {
|
|
6022
|
-
return process.env.TINES_CONFIG_DIR ?? join3(homedir(), ".config", "tines");
|
|
6023
|
-
}
|
|
6024
|
-
function readJsonFile(path2) {
|
|
6025
|
-
if (!existsSync3(path2)) return null;
|
|
6026
|
-
try {
|
|
6027
|
-
return JSON.parse(readFileSync6(path2, "utf8"));
|
|
6028
|
-
} catch {
|
|
6029
|
-
return null;
|
|
6030
|
-
}
|
|
6031
|
-
}
|
|
6032
|
-
function writeJsonFile(path2, value, { secret = false } = {}) {
|
|
6033
|
-
mkdirSync3(dirname3(path2), { recursive: true });
|
|
6034
|
-
writeFileSync2(path2, `${JSON.stringify(value, null, 2)}
|
|
6035
|
-
`, secret ? { mode: 384 } : {});
|
|
6036
|
-
}
|
|
6279
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync, statSync as statSync2 } from "node:fs";
|
|
6280
|
+
import { join as join4 } from "node:path";
|
|
6037
6281
|
function credentialsKey(url, name2) {
|
|
6038
6282
|
return `${url.replace(/\/+$/, "")}#${name2}`;
|
|
6039
6283
|
}
|
|
6040
6284
|
function credentialsPath(dir) {
|
|
6041
|
-
return
|
|
6285
|
+
return join4(dir, "runners.json");
|
|
6042
6286
|
}
|
|
6043
6287
|
function loadRunnerCredentials(dir, url, name2) {
|
|
6044
6288
|
const all = readJsonFile(credentialsPath(dir));
|
|
@@ -6061,7 +6305,7 @@ function clearRunnerCredentials(dir, url, name2) {
|
|
|
6061
6305
|
writeJsonFile(path2, all, { secret: true });
|
|
6062
6306
|
}
|
|
6063
6307
|
function daemonStatePath(dir, runnerId) {
|
|
6064
|
-
return
|
|
6308
|
+
return join4(dir, `daemon-state-${runnerId}.json`);
|
|
6065
6309
|
}
|
|
6066
6310
|
function loadDaemonState(path2) {
|
|
6067
6311
|
const state = readJsonFile(path2);
|
|
@@ -6075,10 +6319,10 @@ function saveDaemonState(path2, runs) {
|
|
|
6075
6319
|
}
|
|
6076
6320
|
function processStartTimeMs(pid) {
|
|
6077
6321
|
try {
|
|
6078
|
-
const stat =
|
|
6322
|
+
const stat = readFileSync8(`/proc/${pid}/stat`, "utf8");
|
|
6079
6323
|
const afterComm = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
6080
6324
|
const startTicks = Number(afterComm[19]);
|
|
6081
|
-
const btimeLine =
|
|
6325
|
+
const btimeLine = readFileSync8("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
|
|
6082
6326
|
const btime = Number(btimeLine?.slice("btime ".length));
|
|
6083
6327
|
if (!Number.isFinite(startTicks) || !Number.isFinite(btime)) return null;
|
|
6084
6328
|
return btime * 1e3 + startTicks / 100 * 1e3;
|
|
@@ -6087,10 +6331,10 @@ function processStartTimeMs(pid) {
|
|
|
6087
6331
|
}
|
|
6088
6332
|
}
|
|
6089
6333
|
function workspacesDir(configDir) {
|
|
6090
|
-
return
|
|
6334
|
+
return join4(configDir, "workspaces");
|
|
6091
6335
|
}
|
|
6092
6336
|
function keptMarkerPath(workspace) {
|
|
6093
|
-
return
|
|
6337
|
+
return join4(workspace, "kept.json");
|
|
6094
6338
|
}
|
|
6095
6339
|
function writeKeptMarker(workspace, marker) {
|
|
6096
6340
|
writeJsonFile(keptMarkerPath(workspace), marker);
|
|
@@ -6112,7 +6356,7 @@ function listKeptWorkspaces(configDir) {
|
|
|
6112
6356
|
}
|
|
6113
6357
|
const kept = [];
|
|
6114
6358
|
for (const name2 of names) {
|
|
6115
|
-
const path2 =
|
|
6359
|
+
const path2 = join4(root, name2);
|
|
6116
6360
|
const marker = readKeptMarker(path2);
|
|
6117
6361
|
if (marker) kept.push({ ...marker, path: path2 });
|
|
6118
6362
|
}
|
|
@@ -6149,7 +6393,7 @@ function directorySizeBytes(path2) {
|
|
|
6149
6393
|
return 0;
|
|
6150
6394
|
}
|
|
6151
6395
|
for (const entry of entries) {
|
|
6152
|
-
const child =
|
|
6396
|
+
const child = join4(path2, entry.name);
|
|
6153
6397
|
if (entry.isDirectory()) total += directorySizeBytes(child);
|
|
6154
6398
|
else if (entry.isFile()) {
|
|
6155
6399
|
try {
|
|
@@ -6172,20 +6416,23 @@ async function uploadRawLog(run) {
|
|
|
6172
6416
|
await new Promise((resolve) => run.rawSpool?.end(resolve) ?? resolve());
|
|
6173
6417
|
const size = statSync3(path2).size;
|
|
6174
6418
|
if (size > 0) {
|
|
6175
|
-
let body =
|
|
6419
|
+
let body = readFileSync9(path2);
|
|
6176
6420
|
if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
|
|
6177
6421
|
const marker = Buffer.from(
|
|
6178
6422
|
`{"type":"tines_truncated","dropped_bytes":${body.byteLength - RUN_LOG_RAW_MAX_BYTES}}
|
|
6179
6423
|
`
|
|
6180
6424
|
);
|
|
6181
|
-
body = Buffer.concat([
|
|
6425
|
+
body = Buffer.concat([
|
|
6426
|
+
marker,
|
|
6427
|
+
body.subarray(body.byteLength - RUN_LOG_RAW_MAX_BYTES + marker.byteLength)
|
|
6428
|
+
]);
|
|
6182
6429
|
}
|
|
6183
6430
|
await run.rawUpload(body);
|
|
6184
6431
|
}
|
|
6185
6432
|
} catch {
|
|
6186
6433
|
} finally {
|
|
6187
6434
|
try {
|
|
6188
|
-
|
|
6435
|
+
unlinkSync2(path2);
|
|
6189
6436
|
} catch {
|
|
6190
6437
|
}
|
|
6191
6438
|
}
|
|
@@ -6242,7 +6489,9 @@ async function runDaemon(opts) {
|
|
|
6242
6489
|
};
|
|
6243
6490
|
let creds = loadRunnerCredentials(opts.configDir, opts.url, opts.name);
|
|
6244
6491
|
if (creds) {
|
|
6245
|
-
log(
|
|
6492
|
+
log(
|
|
6493
|
+
`reconnecting as runner "${opts.name}" (${creds.runner_id}) \u2014 token from ${opts.configDir}`
|
|
6494
|
+
);
|
|
6246
6495
|
} else {
|
|
6247
6496
|
if (!opts.apiKey) {
|
|
6248
6497
|
throw new Error(
|
|
@@ -6271,37 +6520,40 @@ async function runDaemon(opts) {
|
|
|
6271
6520
|
const ensureCli = () => opts.cliRefresh ? refresher.ensure() : Promise.resolve(AMBIENT_CLI);
|
|
6272
6521
|
const cliLabel = (cli) => cli.source === "ambient" ? `ambient PATH (${opts.cliRefresh ? "refresh failed" : "refresh disabled"})` : `tines ${cli.version ?? "unknown"} (daemon-managed${cli.source === "stale" ? ", last-good copy" : ""})`;
|
|
6273
6522
|
log(`agent CLI: ${cliLabel(await ensureCli())}`);
|
|
6274
|
-
const table2 = new RunTable(
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6523
|
+
const table2 = new RunTable(
|
|
6524
|
+
{
|
|
6525
|
+
finish: async (run, status, error) => {
|
|
6526
|
+
await client2.finishRun(run.runId, { status, ...error ? { error } : {} });
|
|
6527
|
+
},
|
|
6528
|
+
release: (run, { keep, outcome }) => {
|
|
6529
|
+
if (run.timeout) clearTimeout(run.timeout);
|
|
6530
|
+
run.renderer?.finish();
|
|
6531
|
+
settleWorkspace(run.workspace, keep, {
|
|
6532
|
+
run_id: run.runId,
|
|
6533
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {},
|
|
6534
|
+
status: outcome,
|
|
6535
|
+
...run.endNote ? { error: run.endNote } : {}
|
|
6536
|
+
});
|
|
6537
|
+
void uploadRawLog(run);
|
|
6538
|
+
sweepKeptWorkspaces();
|
|
6539
|
+
},
|
|
6540
|
+
noteKept: (run) => run.batcher.append(`workspace kept at ${run.workspace}
|
|
6291
6541
|
`),
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6542
|
+
persist: () => {
|
|
6543
|
+
const entries = table2.values().filter((run) => run.child?.pid !== void 0).map((run) => ({
|
|
6544
|
+
run_id: run.runId,
|
|
6545
|
+
pid: run.child.pid,
|
|
6546
|
+
workspace: run.workspace,
|
|
6547
|
+
key_fingerprint: run.keyFingerprint,
|
|
6548
|
+
started_at: run.spawnedAt,
|
|
6549
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {}
|
|
6550
|
+
}));
|
|
6551
|
+
saveDaemonState(statePath, entries);
|
|
6552
|
+
},
|
|
6553
|
+
log
|
|
6302
6554
|
},
|
|
6303
|
-
|
|
6304
|
-
|
|
6555
|
+
{ keep: (outcome) => keepWorkspace(opts.keepWorkspaces, outcome) }
|
|
6556
|
+
);
|
|
6305
6557
|
for (const orphan of loadDaemonState(statePath)) {
|
|
6306
6558
|
if (pidAlive(orphan.pid)) {
|
|
6307
6559
|
const processStart = processStartTimeMs(orphan.pid);
|
|
@@ -6309,7 +6561,9 @@ async function runDaemon(opts) {
|
|
|
6309
6561
|
if (reused) {
|
|
6310
6562
|
log(`state-file pid ${orphan.pid} (run ${orphan.run_id}) was recycled; not killing it`);
|
|
6311
6563
|
} else {
|
|
6312
|
-
log(
|
|
6564
|
+
log(
|
|
6565
|
+
`killing orphaned harness from a previous life: run ${orphan.run_id} (pid ${orphan.pid})`
|
|
6566
|
+
);
|
|
6313
6567
|
killTree(orphan.pid, "SIGKILL");
|
|
6314
6568
|
}
|
|
6315
6569
|
}
|
|
@@ -6342,7 +6596,7 @@ async function runDaemon(opts) {
|
|
|
6342
6596
|
const launch = async (assignment) => {
|
|
6343
6597
|
const runId = assignment.run.id;
|
|
6344
6598
|
if (table2.has(runId)) return;
|
|
6345
|
-
const workspace =
|
|
6599
|
+
const workspace = join5(workspacesDir(opts.configDir), runId);
|
|
6346
6600
|
const issueLabel = assignment.run.issue_ref ? `${assignment.run.issue_ref.project_name}/${assignment.run.issue_ref.number}` : void 0;
|
|
6347
6601
|
const run = {
|
|
6348
6602
|
runId,
|
|
@@ -6360,32 +6614,43 @@ async function runDaemon(opts) {
|
|
|
6360
6614
|
};
|
|
6361
6615
|
run.flush = () => run.batcher.flush();
|
|
6362
6616
|
table2.track(run);
|
|
6363
|
-
log(
|
|
6617
|
+
log(
|
|
6618
|
+
`run ${runId} assigned (issue ${issueLabel ?? assignment.run.issue_id}); materializing workspace`
|
|
6619
|
+
);
|
|
6364
6620
|
try {
|
|
6365
6621
|
rmSync2(workspace, { recursive: true, force: true });
|
|
6366
6622
|
mkdirSync4(workspace, { recursive: true });
|
|
6367
|
-
writeFileSync3(
|
|
6623
|
+
writeFileSync3(join5(workspace, "prompt.md"), `${assignment.prompt}
|
|
6368
6624
|
`);
|
|
6369
6625
|
for (const skill of assignment.bundle.skills) {
|
|
6370
6626
|
for (const file of skill.files) {
|
|
6371
|
-
const target =
|
|
6627
|
+
const target = join5(workspace, "skills", skill.name, file.path);
|
|
6372
6628
|
mkdirSync4(dirname4(target), { recursive: true });
|
|
6373
6629
|
writeFileSync3(target, file.content);
|
|
6374
6630
|
}
|
|
6375
6631
|
}
|
|
6376
6632
|
writeFileSync3(
|
|
6377
|
-
|
|
6633
|
+
join5(workspace, "repos.json"),
|
|
6378
6634
|
`${JSON.stringify(assignment.bundle.repos, null, 2)}
|
|
6379
6635
|
`
|
|
6380
6636
|
);
|
|
6381
6637
|
for (const repo of assignment.bundle.repos) {
|
|
6382
6638
|
if (run.settled) return table2.cleanup(run);
|
|
6383
|
-
const args = [
|
|
6639
|
+
const args = [
|
|
6640
|
+
"clone",
|
|
6641
|
+
...repo.branch ? ["--branch", repo.branch] : [],
|
|
6642
|
+
repo.url,
|
|
6643
|
+
repo.dir
|
|
6644
|
+
];
|
|
6384
6645
|
run.batcher.append(`$ git ${args.join(" ")}
|
|
6385
6646
|
`);
|
|
6386
6647
|
const result = await runGit(args, workspace, run.batcher);
|
|
6387
6648
|
if (result !== 0) {
|
|
6388
|
-
return table2.finishAndCleanup(
|
|
6649
|
+
return table2.finishAndCleanup(
|
|
6650
|
+
run,
|
|
6651
|
+
"failed",
|
|
6652
|
+
`git clone failed for ${repo.url} (exit ${result})`
|
|
6653
|
+
);
|
|
6389
6654
|
}
|
|
6390
6655
|
}
|
|
6391
6656
|
if (run.settled) return table2.cleanup(run);
|
|
@@ -6399,7 +6664,7 @@ async function runDaemon(opts) {
|
|
|
6399
6664
|
);
|
|
6400
6665
|
const harnessInput = {
|
|
6401
6666
|
workspace,
|
|
6402
|
-
promptFile:
|
|
6667
|
+
promptFile: join5(workspace, "prompt.md"),
|
|
6403
6668
|
prompt: assignment.prompt,
|
|
6404
6669
|
model: assignment.run.model
|
|
6405
6670
|
};
|
|
@@ -6432,7 +6697,7 @@ async function runDaemon(opts) {
|
|
|
6432
6697
|
const renderer = new ClaudeStreamRenderer((line) => run.batcher.append(line));
|
|
6433
6698
|
run.renderer = renderer;
|
|
6434
6699
|
run.drain = () => renderer.finish();
|
|
6435
|
-
const spoolPath =
|
|
6700
|
+
const spoolPath = join5(opts.configDir, "rawlogs", `${runId}.ndjson`);
|
|
6436
6701
|
mkdirSync4(dirname4(spoolPath), { recursive: true });
|
|
6437
6702
|
run.rawSpoolPath = spoolPath;
|
|
6438
6703
|
run.rawSpool = createWriteStream(spoolPath);
|
|
@@ -6445,16 +6710,13 @@ async function runDaemon(opts) {
|
|
|
6445
6710
|
child.stdout?.on("data", (data) => run.batcher.append(data.toString("utf8")));
|
|
6446
6711
|
}
|
|
6447
6712
|
child.stderr?.on("data", (data) => run.batcher.append(data.toString("utf8")));
|
|
6448
|
-
run.timeout = setTimeout(
|
|
6449
|
-
()
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
},
|
|
6456
|
-
assignment.timeout_minutes * 6e4
|
|
6457
|
-
);
|
|
6713
|
+
run.timeout = setTimeout(() => {
|
|
6714
|
+
if (run.settled) return;
|
|
6715
|
+
log(`run ${runId} hit its ${assignment.timeout_minutes}m timeout; killing`);
|
|
6716
|
+
run.timedOut = true;
|
|
6717
|
+
if (child.pid) killTree(child.pid, "SIGTERM");
|
|
6718
|
+
if (child.pid) setTimeout(() => killTree(child.pid, "SIGKILL"), 5e3).unref?.();
|
|
6719
|
+
}, assignment.timeout_minutes * 6e4);
|
|
6458
6720
|
child.on("error", (err) => {
|
|
6459
6721
|
void table2.finishAndCleanup(run, "failed", `failed to launch harness: ${message2(err)}`);
|
|
6460
6722
|
});
|
|
@@ -6578,27 +6840,31 @@ function printTierTable(runner) {
|
|
|
6578
6840
|
override?.effort ? `effort ${override.effort}` : null,
|
|
6579
6841
|
stale ? `stale \u2014 built-in is now ${builtin}` : null
|
|
6580
6842
|
].filter(Boolean);
|
|
6581
|
-
console.log(
|
|
6843
|
+
console.log(
|
|
6844
|
+
` ${tier}: ${model} [${source}]${marks.length > 0 ? ` (${marks.join(", ")})` : ""}`
|
|
6845
|
+
);
|
|
6582
6846
|
}
|
|
6583
6847
|
}
|
|
6584
|
-
function
|
|
6848
|
+
function register7(program3) {
|
|
6585
6849
|
const runners = program3.command("runners").description("Manage the runner registry");
|
|
6586
|
-
withCommon(runners.command("list").description("List runners")).action(
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
[
|
|
6592
|
-
|
|
6593
|
-
r
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6850
|
+
withCommon(runners.command("list").description("List runners")).action(
|
|
6851
|
+
async (opts) => {
|
|
6852
|
+
const res = await client(opts).listRunners();
|
|
6853
|
+
if (opts.json) return printJson(res);
|
|
6854
|
+
if (res.items.length === 0) return console.log("no runners");
|
|
6855
|
+
table([
|
|
6856
|
+
["NAME", "TYPE", "STATUS", "RUNS", "TIER", "LAST SEEN"],
|
|
6857
|
+
...res.items.map((r) => [
|
|
6858
|
+
r.name,
|
|
6859
|
+
r.type,
|
|
6860
|
+
runnerStatusLabel(r),
|
|
6861
|
+
`${r.active_runs}/${r.max_concurrent}`,
|
|
6862
|
+
r.default_tier,
|
|
6863
|
+
r.last_seen_at ? timestamp(r.last_seen_at) : "\u2014"
|
|
6864
|
+
])
|
|
6865
|
+
]);
|
|
6866
|
+
}
|
|
6867
|
+
);
|
|
6602
6868
|
withCommon(runners.command("show <name>").description("Show a runner")).action(
|
|
6603
6869
|
async (ref, opts) => {
|
|
6604
6870
|
const runner = await resolveRunner(client(opts), ref);
|
|
@@ -6622,7 +6888,8 @@ function register6(program3) {
|
|
|
6622
6888
|
const b = runner.budget;
|
|
6623
6889
|
const parts = [];
|
|
6624
6890
|
if (b.max_run_cost_usd !== void 0) parts.push(`$${b.max_run_cost_usd}/run`);
|
|
6625
|
-
if (b.max_run_tokens !== void 0)
|
|
6891
|
+
if (b.max_run_tokens !== void 0)
|
|
6892
|
+
parts.push(`${b.max_run_tokens.toLocaleString()} tok/run`);
|
|
6626
6893
|
if (b.daily_usd !== void 0) parts.push(`$${b.daily_usd}/day`);
|
|
6627
6894
|
if (b.daily_tokens !== void 0) parts.push(`${b.daily_tokens.toLocaleString()} tok/day`);
|
|
6628
6895
|
if (parts.length > 0) console.log(`budget: ${parts.join(" ")}`);
|
|
@@ -6676,7 +6943,12 @@ function register6(program3) {
|
|
|
6676
6943
|
}
|
|
6677
6944
|
);
|
|
6678
6945
|
withCommon(
|
|
6679
|
-
runners.command("budget <name>").description(
|
|
6946
|
+
runners.command("budget <name>").description(
|
|
6947
|
+
"Set or clear a runner's money limits (per-run caps enforce now; daily limits arrive with the budgets milestone)"
|
|
6948
|
+
).option("--max-run-usd <n>", "hard per-run cost cap (platform-enforced on Claude runners)").option("--max-run-tokens <n>", "hard per-run token cap (input + output)").option("--daily-usd <n>", "daily USD limit (stored now, enforced by the budgets milestone)").option(
|
|
6949
|
+
"--daily-tokens <n>",
|
|
6950
|
+
"daily token limit (stored now, enforced by the budgets milestone)"
|
|
6951
|
+
).option("--clear", "remove all limits")
|
|
6680
6952
|
).action(
|
|
6681
6953
|
async (ref, opts) => {
|
|
6682
6954
|
const api = client(opts);
|
|
@@ -6691,7 +6963,8 @@ function register6(program3) {
|
|
|
6691
6963
|
} else if (flags) {
|
|
6692
6964
|
const num = (value, flag) => {
|
|
6693
6965
|
const n = Number(value);
|
|
6694
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
6966
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
6967
|
+
die(`${flag} must be a positive number, got "${value}"`);
|
|
6695
6968
|
return n;
|
|
6696
6969
|
};
|
|
6697
6970
|
updated = await api.updateRunner(runner.id, {
|
|
@@ -6709,10 +6982,14 @@ function register6(program3) {
|
|
|
6709
6982
|
if (!b) return console.log(`no limits on "${updated.name}"`);
|
|
6710
6983
|
console.log(`limits on "${updated.name}":`);
|
|
6711
6984
|
if (b.max_run_cost_usd !== void 0) console.log(` $${b.max_run_cost_usd} per run`);
|
|
6712
|
-
if (b.max_run_tokens !== void 0)
|
|
6713
|
-
|
|
6985
|
+
if (b.max_run_tokens !== void 0)
|
|
6986
|
+
console.log(` ${b.max_run_tokens.toLocaleString()} tokens per run`);
|
|
6987
|
+
if (b.daily_usd !== void 0)
|
|
6988
|
+
console.log(` $${b.daily_usd} per day (enforced by the budgets milestone)`);
|
|
6714
6989
|
if (b.daily_tokens !== void 0) {
|
|
6715
|
-
console.log(
|
|
6990
|
+
console.log(
|
|
6991
|
+
` ${b.daily_tokens.toLocaleString()} tokens per day (enforced by the budgets milestone)`
|
|
6992
|
+
);
|
|
6716
6993
|
}
|
|
6717
6994
|
}
|
|
6718
6995
|
);
|
|
@@ -6735,7 +7012,12 @@ function register6(program3) {
|
|
|
6735
7012
|
}
|
|
6736
7013
|
);
|
|
6737
7014
|
withCommon(
|
|
6738
|
-
runners.command("remove <name>").description(
|
|
7015
|
+
runners.command("remove <name>").description(
|
|
7016
|
+
"Remove a runner (refused while routing rules or pins reference it, unless --force)"
|
|
7017
|
+
).option(
|
|
7018
|
+
"--force",
|
|
7019
|
+
"strip the runner from routing rules and clear issue pins (emptied rules are kept, flagged)"
|
|
7020
|
+
)
|
|
6739
7021
|
).action(async (ref, opts) => {
|
|
6740
7022
|
const api = client(opts);
|
|
6741
7023
|
const runner = await resolveRunner(api, ref);
|
|
@@ -6757,14 +7039,28 @@ function register6(program3) {
|
|
|
6757
7039
|
runner_id: rotated.runner.id,
|
|
6758
7040
|
token: rotated.runner_token
|
|
6759
7041
|
});
|
|
6760
|
-
console.log(
|
|
7042
|
+
console.log(
|
|
7043
|
+
`stored it for the daemon on this machine (${defaultConfigDir()}); restart the daemon to adopt it.`
|
|
7044
|
+
);
|
|
6761
7045
|
} else {
|
|
6762
|
-
console.log(
|
|
7046
|
+
console.log(
|
|
7047
|
+
"drop it into the daemon machine's config \u2014 its next poll gets a 401 until it adopts the new token."
|
|
7048
|
+
);
|
|
6763
7049
|
}
|
|
6764
7050
|
});
|
|
6765
7051
|
const runnerCmd = program3.command("runner").description("The local runner daemon");
|
|
6766
7052
|
withCommon(
|
|
6767
|
-
runnerCmd.command("daemon").description(
|
|
7053
|
+
runnerCmd.command("daemon").description(
|
|
7054
|
+
"Run the local runner daemon: register/reconnect, poll for assigned runs, execute them"
|
|
7055
|
+
).option("--name <name>", "runner name, unique per user (default: this hostname)").option("--harness <harness>", "claude-code | codex | custom", "claude-code").option(
|
|
7056
|
+
"--command <template>",
|
|
7057
|
+
"custom harness command template ({prompt_file}, {workspace}, {model})"
|
|
7058
|
+
).option("--max-concurrent <n>", "maximum simultaneous runs", (v) => Number.parseInt(v, 10), 1).option(
|
|
7059
|
+
"--poll-interval <seconds>",
|
|
7060
|
+
"seconds between polls",
|
|
7061
|
+
(v) => Number.parseInt(v, 10),
|
|
7062
|
+
15
|
|
7063
|
+
).option(
|
|
6768
7064
|
"--no-cli-refresh",
|
|
6769
7065
|
"do not install/refresh the agent-facing tines CLI from npm (harnesses use the ambient PATH)"
|
|
6770
7066
|
).option(
|
|
@@ -6789,7 +7085,9 @@ function register6(program3) {
|
|
|
6789
7085
|
die(`--harness must be claude-code, codex, or custom, got "${opts.harness}"`);
|
|
6790
7086
|
}
|
|
6791
7087
|
if (harness === "custom" && !opts.command) {
|
|
6792
|
-
die(
|
|
7088
|
+
die(
|
|
7089
|
+
'the custom harness needs --command "<template>" ({prompt_file}, {workspace}, {model})'
|
|
7090
|
+
);
|
|
6793
7091
|
}
|
|
6794
7092
|
if (harness !== "custom" && opts.command) die("--command only applies to --harness custom");
|
|
6795
7093
|
if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent < 1 || opts.maxConcurrent > 100) {
|
|
@@ -6800,7 +7098,9 @@ function register6(program3) {
|
|
|
6800
7098
|
}
|
|
6801
7099
|
const keepWorkspaces = opts.keepWorkspaces;
|
|
6802
7100
|
if (!KEEP_WORKSPACES_MODES.includes(keepWorkspaces)) {
|
|
6803
|
-
die(
|
|
7101
|
+
die(
|
|
7102
|
+
`--keep-workspaces must be ${KEEP_WORKSPACES_MODES.join(", ")}, got "${opts.keepWorkspaces}"`
|
|
7103
|
+
);
|
|
6804
7104
|
}
|
|
6805
7105
|
if (!Number.isFinite(opts.keepWorkspacesFor) || opts.keepWorkspacesFor <= 0) {
|
|
6806
7106
|
die("--keep-workspaces-for must be a positive number of hours");
|
|
@@ -6831,9 +7131,7 @@ function register6(program3) {
|
|
|
6831
7131
|
if (opts.json) return printJson({ items: sized });
|
|
6832
7132
|
if (sized.length === 0) {
|
|
6833
7133
|
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
|
-
);
|
|
7134
|
+
return console.log("the daemon keeps them only with --keep-workspaces failed (or always).");
|
|
6837
7135
|
}
|
|
6838
7136
|
table([
|
|
6839
7137
|
["RUN", "ISSUE", "STATUS", "AGE", "SIZE", "PATH"],
|
|
@@ -6844,7 +7142,8 @@ function register6(program3) {
|
|
|
6844
7142
|
if (opts.all === void 0 && opts.olderThan === void 0) {
|
|
6845
7143
|
die("pass --all or --older-than <hours>");
|
|
6846
7144
|
}
|
|
6847
|
-
if (opts.all && opts.olderThan !== void 0)
|
|
7145
|
+
if (opts.all && opts.olderThan !== void 0)
|
|
7146
|
+
die("--all cannot be combined with --older-than");
|
|
6848
7147
|
if (opts.olderThan !== void 0 && (!Number.isFinite(opts.olderThan) || opts.olderThan < 0)) {
|
|
6849
7148
|
die("--older-than must be a non-negative number of hours");
|
|
6850
7149
|
}
|
|
@@ -6874,61 +7173,69 @@ function register6(program3) {
|
|
|
6874
7173
|
);
|
|
6875
7174
|
printList(res, opts, (items) => {
|
|
6876
7175
|
if (items.length === 0) return console.log(opts.active ? "no active runs" : "no runs");
|
|
6877
|
-
table([
|
|
7176
|
+
table([
|
|
7177
|
+
["ID", "ISSUE", "RUNNER", "TIER", "STATUS", "DURATION", "COST", "CREATED"],
|
|
7178
|
+
...items.map(runRow)
|
|
7179
|
+
]);
|
|
6878
7180
|
});
|
|
6879
7181
|
});
|
|
6880
7182
|
withCommon(
|
|
6881
7183
|
runsCmd.command("show <id>").description("Show a run; --logs prints the captured log tail, --logs --full the whole log").option("--logs", "print the log tail").option("--full", "with --logs: print the complete log, not the 256 KB tail").option("--raw", "with --logs --full: print the unrendered harness stream instead")
|
|
6882
|
-
).action(
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
if (parts.length > 0) console.log(`usage: ${parts.join(" ")}`);
|
|
6904
|
-
}
|
|
6905
|
-
if (run.provider_session_id) console.log(`provider session: ${run.provider_session_id}`);
|
|
6906
|
-
if (run.provider_url) console.log(`provider console: ${run.provider_url}`);
|
|
6907
|
-
if (run.error) console.log(`error: ${run.error}`);
|
|
6908
|
-
if (opts.logs) {
|
|
6909
|
-
console.log("");
|
|
6910
|
-
if (opts.full || opts.raw) {
|
|
6911
|
-
const res = await api.getRunLogFull(id, { raw: opts.raw });
|
|
6912
|
-
const body = res.body;
|
|
6913
|
-
if (!body) return;
|
|
6914
|
-
const reader = body.getReader();
|
|
6915
|
-
const decoder = new TextDecoder();
|
|
6916
|
-
for (; ; ) {
|
|
6917
|
-
const { done, value } = await reader.read();
|
|
6918
|
-
if (done) break;
|
|
6919
|
-
if (value) process.stdout.write(decoder.decode(value, { stream: true }));
|
|
7184
|
+
).action(
|
|
7185
|
+
async (id, opts) => {
|
|
7186
|
+
const api = client(opts);
|
|
7187
|
+
const run = await api.getRun(id);
|
|
7188
|
+
if (opts.json) return printJson(run);
|
|
7189
|
+
console.log(`${run.id} ${run.status} on ${run.runner_name}`);
|
|
7190
|
+
if (run.issue_ref) console.log(`issue: ${issueRef(run.issue_ref)} \u2014 ${run.issue_ref.title}`);
|
|
7191
|
+
console.log(`tier: ${run.tier} model: ${run.model ?? "(n/a)"}`);
|
|
7192
|
+
console.log(
|
|
7193
|
+
`states: ${run.state_at_start_name ?? run.state_id_at_start} \u2192 ${run.state_at_end_name ?? run.state_id_at_end ?? "\u2026"}`
|
|
7194
|
+
);
|
|
7195
|
+
console.log(
|
|
7196
|
+
`created: ${timestamp(run.created_at)} started: ${run.started_at ? timestamp(run.started_at) : "\u2014"} ended: ${run.ended_at ? timestamp(run.ended_at) : "\u2014"} duration: ${runDurationLabel(run)}`
|
|
7197
|
+
);
|
|
7198
|
+
if (run.usage) {
|
|
7199
|
+
const u = run.usage;
|
|
7200
|
+
const parts = [];
|
|
7201
|
+
if (u.input_tokens !== void 0 || u.output_tokens !== void 0) {
|
|
7202
|
+
parts.push(
|
|
7203
|
+
`${(u.input_tokens ?? 0).toLocaleString()} in / ${(u.output_tokens ?? 0).toLocaleString()} out tokens`
|
|
7204
|
+
);
|
|
6920
7205
|
}
|
|
6921
|
-
|
|
6922
|
-
|
|
7206
|
+
if (u.cost_usd !== void 0) parts.push(`$${u.cost_usd.toFixed(2)}`);
|
|
7207
|
+
if (u.cost_source)
|
|
7208
|
+
parts.push(`(${u.cost_source === "provider" ? "provider-reported" : u.cost_source})`);
|
|
7209
|
+
if (parts.length > 0) console.log(`usage: ${parts.join(" ")}`);
|
|
6923
7210
|
}
|
|
6924
|
-
if (run.
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
7211
|
+
if (run.provider_session_id) console.log(`provider session: ${run.provider_session_id}`);
|
|
7212
|
+
if (run.provider_url) console.log(`provider console: ${run.provider_url}`);
|
|
7213
|
+
if (run.error) console.log(`error: ${run.error}`);
|
|
7214
|
+
if (opts.logs) {
|
|
7215
|
+
console.log("");
|
|
7216
|
+
if (opts.full || opts.raw) {
|
|
7217
|
+
const res = await api.getRunLogFull(id, { raw: opts.raw });
|
|
7218
|
+
const body = res.body;
|
|
7219
|
+
if (!body) return;
|
|
7220
|
+
const reader = body.getReader();
|
|
7221
|
+
const decoder = new TextDecoder();
|
|
7222
|
+
for (; ; ) {
|
|
7223
|
+
const { done, value } = await reader.read();
|
|
7224
|
+
if (done) break;
|
|
7225
|
+
if (value) process.stdout.write(decoder.decode(value, { stream: true }));
|
|
7226
|
+
}
|
|
7227
|
+
process.stdout.write(decoder.decode());
|
|
7228
|
+
return;
|
|
7229
|
+
}
|
|
7230
|
+
if (run.log_bytes_dropped > 0) {
|
|
7231
|
+
console.log(
|
|
7232
|
+
`[${Math.round(run.log_bytes_dropped / 1024)} KB truncated from the head \u2014 ` + (run.log_expired ? "past its retention window; only this tail remains]" : `run \`tines runs show ${run.id} --logs --full\` for the complete ${Math.round(run.log_full_bytes / 1024)} KB log]`)
|
|
7233
|
+
);
|
|
7234
|
+
}
|
|
7235
|
+
console.log(run.log || "(no log output captured)");
|
|
6928
7236
|
}
|
|
6929
|
-
console.log(run.log || "(no log output captured)");
|
|
6930
7237
|
}
|
|
6931
|
-
|
|
7238
|
+
);
|
|
6932
7239
|
withCommon(
|
|
6933
7240
|
runsCmd.command("cancel <id>").description("Cancel a run (judged like any other end: usually a strike)")
|
|
6934
7241
|
).action(async (id, opts) => {
|
|
@@ -6971,7 +7278,7 @@ title template: ${s.title_template}`);
|
|
|
6971
7278
|
for (const line of s.description_template.split("\n")) console.log(` ${line}`);
|
|
6972
7279
|
}
|
|
6973
7280
|
}
|
|
6974
|
-
function
|
|
7281
|
+
function register8(program3) {
|
|
6975
7282
|
const schedules = program3.command("schedules").description("Manage scheduled tasks (addressed as <project>/<name>)");
|
|
6976
7283
|
withList(
|
|
6977
7284
|
schedules.command("list").description("List scheduled tasks (hides paused schedules unless --all)").option("-p, --project <name>", "filter by project name or id").option("-a, --all", "include paused schedules")
|
|
@@ -7022,7 +7329,9 @@ recent instances:`);
|
|
|
7022
7329
|
}
|
|
7023
7330
|
});
|
|
7024
7331
|
withCommon(
|
|
7025
|
-
schedules.command("edit <ref>").description(
|
|
7332
|
+
schedules.command("edit <ref>").description(
|
|
7333
|
+
"Edit a schedule: templates, workflow, start state, recurrence, timezone, gate, or name"
|
|
7334
|
+
).option("-t, --title <template>", "set the title template").option(
|
|
7026
7335
|
"-d, --description <markdown>",
|
|
7027
7336
|
`set the description template (Markdown) \u2014 ${BODY_VALUE_HELP}`
|
|
7028
7337
|
).option(
|
|
@@ -7031,7 +7340,13 @@ recent instances:`);
|
|
|
7031
7340
|
).option(
|
|
7032
7341
|
"-s, --state <id-or-name>",
|
|
7033
7342
|
"start state for future instances (the workflow's initial state = the default)"
|
|
7034
|
-
).option(
|
|
7343
|
+
).option(
|
|
7344
|
+
"--every <preset>",
|
|
7345
|
+
'repeat hourly (or every N hours: "6h"), daily, weekly, or monthly'
|
|
7346
|
+
).option(
|
|
7347
|
+
"--at <when>",
|
|
7348
|
+
"preset time of day HH:MM (default 09:00); for hourly, the minute past the hour :MM (default :00)"
|
|
7349
|
+
).option("--on <when>", "weekday (weekly) or day of month (monthly)").option("--cron <expr>", "5-field cron expression (alternative to --every/--at/--on)").option("--tz <iana>", "set the schedule timezone").option("--if-closed", "only create a new instance when all previous instances are closed").option("--no-if-closed", "clear the only-when-closed gate").option("--name <new-name>", "rename the schedule")
|
|
7035
7350
|
).action(
|
|
7036
7351
|
async (ref, opts) => {
|
|
7037
7352
|
const descriptionTemplate = opts.description !== void 0 ? readBodyValue(opts.description) : void 0;
|
|
@@ -7040,7 +7355,8 @@ recent instances:`);
|
|
|
7040
7355
|
const body = {};
|
|
7041
7356
|
if (opts.title !== void 0) body.title_template = opts.title;
|
|
7042
7357
|
if (descriptionTemplate !== void 0) body.description_template = descriptionTemplate;
|
|
7043
|
-
if (opts.workflow !== void 0)
|
|
7358
|
+
if (opts.workflow !== void 0)
|
|
7359
|
+
body.workflow_id = (await resolveWorkflow(api, opts.workflow)).id;
|
|
7044
7360
|
if (opts.state !== void 0) body.state = opts.state;
|
|
7045
7361
|
const recurrence = buildRecurrence(opts);
|
|
7046
7362
|
if (recurrence?.preset) body.preset = recurrence.preset;
|
|
@@ -7060,15 +7376,15 @@ recent instances:`);
|
|
|
7060
7376
|
printScheduleDetail(updated);
|
|
7061
7377
|
}
|
|
7062
7378
|
);
|
|
7063
|
-
withCommon(
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
}
|
|
7071
|
-
);
|
|
7379
|
+
withCommon(
|
|
7380
|
+
schedules.command("pause <ref>").description("Pause a schedule (keeps config and history)")
|
|
7381
|
+
).action(async (ref, opts) => {
|
|
7382
|
+
const api = client(opts);
|
|
7383
|
+
const schedule = await resolveSchedule(api, ref);
|
|
7384
|
+
const updated = await api.updateSchedule(schedule.id, { enabled: false });
|
|
7385
|
+
if (opts.json) return printJson(updated);
|
|
7386
|
+
console.log(`paused schedule "${scheduleRef(updated)}"`);
|
|
7387
|
+
});
|
|
7072
7388
|
withCommon(
|
|
7073
7389
|
schedules.command("resume <ref>").description("Resume a paused schedule (recomputes the next occurrence from now)")
|
|
7074
7390
|
).action(async (ref, opts) => {
|
|
@@ -7076,7 +7392,9 @@ recent instances:`);
|
|
|
7076
7392
|
const schedule = await resolveSchedule(api, ref);
|
|
7077
7393
|
const updated = await api.updateSchedule(schedule.id, { enabled: true });
|
|
7078
7394
|
if (opts.json) return printJson(updated);
|
|
7079
|
-
console.log(
|
|
7395
|
+
console.log(
|
|
7396
|
+
`resumed schedule "${scheduleRef(updated)}" \u2014 next run ${timestamp(updated.next_run_at)}`
|
|
7397
|
+
);
|
|
7080
7398
|
});
|
|
7081
7399
|
withCommon(
|
|
7082
7400
|
schedules.command("run <ref>").description("Create an instance now (respects the only-when-closed gate)")
|
|
@@ -7108,57 +7426,61 @@ recent instances:`);
|
|
|
7108
7426
|
}
|
|
7109
7427
|
|
|
7110
7428
|
// src/commands/supervisor.ts
|
|
7111
|
-
function
|
|
7429
|
+
function register9(program3) {
|
|
7112
7430
|
const supervisor = program3.command("supervisor").description("The automation kill switch, quota policy, and attempt limit");
|
|
7113
|
-
withCommon(
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
}
|
|
7125
|
-
const stateNames = /* @__PURE__ */ new Map();
|
|
7126
|
-
for (const wf of workflows.items) {
|
|
7127
|
-
for (const s of wf.states) stateNames.set(s.id, `${wf.name}/${s.name}`);
|
|
7128
|
-
}
|
|
7129
|
-
console.log(`automation: ${settings.enabled ? "ON" : "OFF (kill switch \u2014 nothing dispatches)"}`);
|
|
7130
|
-
console.log(quotaLabel(settings.quota, (id) => stateNames.get(id) ?? id));
|
|
7131
|
-
console.log(`utilization: ${utilizationLabel(settings.quota, activeRunItems, (id) => stateNames.get(id) ?? id)}`);
|
|
7132
|
-
console.log(`attempt limit: ${settings.attempt_limit} strikes, then the issue parks`);
|
|
7133
|
-
if (runnersRes.items.length === 0) {
|
|
7134
|
-
console.log("runners: none");
|
|
7135
|
-
} else {
|
|
7136
|
-
console.log("runners:");
|
|
7137
|
-
table(
|
|
7138
|
-
runnersRes.items.map((r) => [
|
|
7139
|
-
` ${r.name}`,
|
|
7140
|
-
r.type,
|
|
7141
|
-
runnerStatusLabel(r),
|
|
7142
|
-
`${r.active_runs}/${r.max_concurrent}`
|
|
7143
|
-
])
|
|
7144
|
-
);
|
|
7145
|
-
}
|
|
7431
|
+
withCommon(
|
|
7432
|
+
supervisor.command("status").description("One-screen overview: kill switch, quota, utilization, runners")
|
|
7433
|
+
).action(async (opts) => {
|
|
7434
|
+
const api = client(opts);
|
|
7435
|
+
const [settings, runnersRes, workflows, activeRunItems] = await Promise.all([
|
|
7436
|
+
api.getSupervisorSettings(),
|
|
7437
|
+
api.listRunners(),
|
|
7438
|
+
api.listWorkflows(),
|
|
7439
|
+
listAll((page) => api.listRuns({ active: true, ...page }))
|
|
7440
|
+
]);
|
|
7441
|
+
if (opts.json) {
|
|
7442
|
+
return printJson({ settings, runners: runnersRes.items, active_runs: activeRunItems });
|
|
7146
7443
|
}
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
const settings = await client(opts).updateSupervisorSettings({ enabled: true });
|
|
7151
|
-
if (opts.json) return printJson(settings);
|
|
7152
|
-
console.log("automation is ON \u2014 eligible issues with a matching rule will dispatch");
|
|
7444
|
+
const stateNames = /* @__PURE__ */ new Map();
|
|
7445
|
+
for (const wf of workflows.items) {
|
|
7446
|
+
for (const s of wf.states) stateNames.set(s.id, `${wf.name}/${s.name}`);
|
|
7153
7447
|
}
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7448
|
+
console.log(
|
|
7449
|
+
`automation: ${settings.enabled ? "ON" : "OFF (kill switch \u2014 nothing dispatches)"}`
|
|
7450
|
+
);
|
|
7451
|
+
console.log(quotaLabel(settings.quota, (id) => stateNames.get(id) ?? id));
|
|
7452
|
+
console.log(
|
|
7453
|
+
`utilization: ${utilizationLabel(settings.quota, activeRunItems, (id) => stateNames.get(id) ?? id)}`
|
|
7454
|
+
);
|
|
7455
|
+
console.log(`attempt limit: ${settings.attempt_limit} strikes, then the issue parks`);
|
|
7456
|
+
if (runnersRes.items.length === 0) {
|
|
7457
|
+
console.log("runners: none");
|
|
7458
|
+
} else {
|
|
7459
|
+
console.log("runners:");
|
|
7460
|
+
table(
|
|
7461
|
+
runnersRes.items.map((r) => [
|
|
7462
|
+
` ${r.name}`,
|
|
7463
|
+
r.type,
|
|
7464
|
+
runnerStatusLabel(r),
|
|
7465
|
+
`${r.active_runs}/${r.max_concurrent}`
|
|
7466
|
+
])
|
|
7467
|
+
);
|
|
7160
7468
|
}
|
|
7161
|
-
);
|
|
7469
|
+
});
|
|
7470
|
+
withCommon(
|
|
7471
|
+
supervisor.command("enable").description("Arm automation (the kill switch on)")
|
|
7472
|
+
).action(async (opts) => {
|
|
7473
|
+
const settings = await client(opts).updateSupervisorSettings({ enabled: true });
|
|
7474
|
+
if (opts.json) return printJson(settings);
|
|
7475
|
+
console.log("automation is ON \u2014 eligible issues with a matching rule will dispatch");
|
|
7476
|
+
});
|
|
7477
|
+
withCommon(
|
|
7478
|
+
supervisor.command("disable").description("Pause all automation at once (the kill switch off)")
|
|
7479
|
+
).action(async (opts) => {
|
|
7480
|
+
const settings = await client(opts).updateSupervisorSettings({ enabled: false });
|
|
7481
|
+
if (opts.json) return printJson(settings);
|
|
7482
|
+
console.log("automation is OFF \u2014 nothing new dispatches until re-enabled");
|
|
7483
|
+
});
|
|
7162
7484
|
const quota = supervisor.command("quota").description("Pick and configure the quota policy");
|
|
7163
7485
|
withCommon(
|
|
7164
7486
|
quota.command("global <n>").description("Use the global cap: at most <n> concurrent runs in total")
|
|
@@ -7171,7 +7493,11 @@ function register8(program3) {
|
|
|
7171
7493
|
console.log(quotaLabel(settings.quota));
|
|
7172
7494
|
});
|
|
7173
7495
|
withCommon(
|
|
7174
|
-
quota.command("roster").description("Use the per-state roster: at most N concurrent runs per workflow state").requiredOption(
|
|
7496
|
+
quota.command("roster").description("Use the per-state roster: at most N concurrent runs per workflow state").requiredOption(
|
|
7497
|
+
"--default <n>",
|
|
7498
|
+
"limit for states without an override",
|
|
7499
|
+
(v) => Number.parseInt(v, 10)
|
|
7500
|
+
).option(
|
|
7175
7501
|
"--state <workflow/state=n>",
|
|
7176
7502
|
"per-state override (repeatable), counted by the state a run started in",
|
|
7177
7503
|
collect,
|
|
@@ -7203,7 +7529,7 @@ function register8(program3) {
|
|
|
7203
7529
|
}
|
|
7204
7530
|
|
|
7205
7531
|
// src/commands/workflows.ts
|
|
7206
|
-
import { readFileSync as
|
|
7532
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7207
7533
|
function readJsonBody(inline, file) {
|
|
7208
7534
|
if (inline !== void 0 && file !== void 0) {
|
|
7209
7535
|
die("pass the JSON inline or with --file, not both");
|
|
@@ -7212,14 +7538,14 @@ function readJsonBody(inline, file) {
|
|
|
7212
7538
|
if (file !== void 0 && file !== "-") {
|
|
7213
7539
|
let raw;
|
|
7214
7540
|
try {
|
|
7215
|
-
raw =
|
|
7541
|
+
raw = readFileSync10(file, "utf8");
|
|
7216
7542
|
} catch (err) {
|
|
7217
7543
|
die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7218
7544
|
}
|
|
7219
7545
|
return parseJsonObject(raw, file);
|
|
7220
7546
|
}
|
|
7221
7547
|
if (file === "-" || !process.stdin.isTTY) {
|
|
7222
|
-
const raw =
|
|
7548
|
+
const raw = readFileSync10(0, "utf8");
|
|
7223
7549
|
if (raw.trim() === "") {
|
|
7224
7550
|
if (file === "-") die("no JSON on stdin");
|
|
7225
7551
|
return void 0;
|
|
@@ -7295,7 +7621,7 @@ function printWorkflowDetail(wf) {
|
|
|
7295
7621
|
for (const w of wf.warnings ?? []) console.log(`
|
|
7296
7622
|
warning: ${w}`);
|
|
7297
7623
|
}
|
|
7298
|
-
function
|
|
7624
|
+
function register10(program3) {
|
|
7299
7625
|
const workflows = program3.command("workflows").description("Manage the workflow library");
|
|
7300
7626
|
withList(workflows.command("list").description("List the workflow library")).action(
|
|
7301
7627
|
async (opts) => {
|
|
@@ -7325,22 +7651,26 @@ function register9(program3) {
|
|
|
7325
7651
|
printWorkflowDetail(wf);
|
|
7326
7652
|
});
|
|
7327
7653
|
withCommon(
|
|
7328
|
-
workflows.command("create [json]").description(
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7654
|
+
workflows.command("create [json]").description(
|
|
7655
|
+
'Create a workflow from a JSON definition (states carry initial "prompt" instructions)'
|
|
7656
|
+
).option("-f, --file <path>", 'read the JSON definition from a file ("-" for stdin)').option("--no-prompts", 'allow states without initial "prompt" instructions').addHelpText("after", WORKFLOW_JSON_HELP)
|
|
7657
|
+
).action(
|
|
7658
|
+
async (inline, opts) => {
|
|
7659
|
+
const body = readJsonBody(inline, opts.file);
|
|
7660
|
+
if (!body) {
|
|
7661
|
+
die(
|
|
7662
|
+
`missing workflow JSON: pass it inline, with --file <path>, or pipe it on stdin
|
|
7334
7663
|
see \`tines workflows create --help\` for the expected shape`
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7664
|
+
);
|
|
7665
|
+
}
|
|
7666
|
+
assertNewStatesHavePrompts(body.states, opts.prompts);
|
|
7667
|
+
const wf = await client(opts).createWorkflow(body);
|
|
7668
|
+
if (opts.json) return printJson(wf);
|
|
7669
|
+
console.log(`created workflow "${wf.name}" (${wf.id})
|
|
7341
7670
|
`);
|
|
7342
|
-
|
|
7343
|
-
|
|
7671
|
+
printWorkflowDetail(wf);
|
|
7672
|
+
}
|
|
7673
|
+
);
|
|
7344
7674
|
withCommon(
|
|
7345
7675
|
workflows.command("edit <id-or-name> [json]").description("Update a workflow from a JSON definition and/or flags").option("-f, --file <path>", 'read the JSON definition from a file ("-" for stdin)').option("-n, --name <name>", "rename the workflow").option("-d, --description <text>", "set the description").option("--initial-state <id-or-name>", "set the initial state").option("--no-prompts", 'allow new states without initial "prompt" instructions').addHelpText("after", WORKFLOW_JSON_HELP)
|
|
7346
7676
|
).action(
|
|
@@ -7377,14 +7707,15 @@ var program2 = new Command();
|
|
|
7377
7707
|
program2.name("tines").description("CLI for Tines").version(cliVersion()).enablePositionalOptions();
|
|
7378
7708
|
registerTime(program2);
|
|
7379
7709
|
register4(program2);
|
|
7380
|
-
|
|
7710
|
+
register5(program2);
|
|
7711
|
+
register10(program2);
|
|
7381
7712
|
register2(program2);
|
|
7382
7713
|
register(program2);
|
|
7383
7714
|
register3(program2);
|
|
7715
|
+
register8(program2);
|
|
7384
7716
|
register7(program2);
|
|
7385
7717
|
register6(program2);
|
|
7386
|
-
|
|
7387
|
-
register8(program2);
|
|
7718
|
+
register9(program2);
|
|
7388
7719
|
registerEvents(program2);
|
|
7389
7720
|
|
|
7390
7721
|
// src/index.ts
|