tines 0.0.142 → 0.0.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +238 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3506,6 +3506,17 @@ function isStaleTierOverride(builtinModel, overrideModel) {
3506
3506
  if (!builtinModel || !overrideModel || builtinModel === overrideModel) return false;
3507
3507
  return (MODEL_PREDECESSORS[builtinModel] ?? []).includes(overrideModel);
3508
3508
  }
3509
+ function ageLabel(at, now = Date.now()) {
3510
+ if (!Number.isFinite(at)) return "\u2014";
3511
+ const seconds = Math.max(0, Math.round((now - at) / 1e3));
3512
+ if (seconds < 60) return `${seconds}s`;
3513
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
3514
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
3515
+ return `${Math.floor(seconds / 86400)}d`;
3516
+ }
3517
+ function prUrlOf(v) {
3518
+ return v.pr_repo_url && v.pr_number !== null ? `${v.pr_repo_url}/pull/${v.pr_number}` : null;
3519
+ }
3509
3520
  function runDurationLabel(run, now = Date.now()) {
3510
3521
  if (!run.started_at) return "\u2014";
3511
3522
  const seconds = Math.max(0, Math.round(((run.ended_at ?? now) - run.started_at) / 1e3));
@@ -4029,6 +4040,7 @@ function createApiClient(options) {
4029
4040
  deleteRoutingRule: (id) => request("DELETE", `/api/v1/routing-rules/${id}`),
4030
4041
  // Supervisor settings
4031
4042
  getSupervisorSettings: () => get("/api/v1/supervisor/settings"),
4043
+ getSupervisorQueue: () => get("/api/v1/supervisor/queue"),
4032
4044
  updateSupervisorSettings: (body) => request("PUT", "/api/v1/supervisor/settings", body),
4033
4045
  // API keys (create/revoke require a browser session, not a key)
4034
4046
  listApiKeys: (filters = {}) => get(`/api/v1/api-keys${query(filters)}`),
@@ -4184,7 +4196,7 @@ function artifactSummary(a) {
4184
4196
  case "link":
4185
4197
  return cv.title ? `${cv.title} \u2014 ${cv.url}` : cv.url ?? "";
4186
4198
  case "pr":
4187
- return `${prRefLabel(cv)} \u2014 ${cv.pr_repo_url}/pull/${cv.pr_number}`;
4199
+ return `${prRefLabel(cv)} \u2014 ${prUrlOf(cv)}`;
4188
4200
  }
4189
4201
  }
4190
4202
  function artifactTypeLabel(a) {
@@ -4234,6 +4246,84 @@ function commentLines(comment) {
4234
4246
  const header = ` [${timestamp(comment.created_at)}] ${actorLabel(comment.actor)} (${comment.id})${comment.updated_at ? " (edited)" : ""}:`;
4235
4247
  return ["", header, ...comment.body.split("\n").map((line) => ` ${line}`)];
4236
4248
  }
4249
+ function sinceLastRunLines(since, now = Date.now()) {
4250
+ const prev = since.previous_run;
4251
+ const ended = prev.ended_at === null ? "not ended" : `ended ${ageLabel(prev.ended_at, now)} ago`;
4252
+ const lines = [
4253
+ `since the last run (${prev.state_at_start_name ?? "unknown state"}, ${prev.run_id} ${ended}):`
4254
+ ];
4255
+ const t = since.transition;
4256
+ if (t) {
4257
+ const via = t.action ? `via "${t.action}"` : "moved directly";
4258
+ const stale = since.stale_artifacts.length > 0 ? ` \u2014 now stale: ${since.stale_artifacts.join(", ")}` : "";
4259
+ lines.push(
4260
+ ` moved from ${t.from_state.name} ${via} by ${actorLabel(t.actor)} ${ageLabel(t.at, now)} ago${stale}`
4261
+ );
4262
+ }
4263
+ for (const c of since.comments) lines.push(...commentLines(c));
4264
+ if (since.comment_count > since.comments.length) {
4265
+ const hidden = since.comment_count - since.comments.length;
4266
+ lines.push(` \u2026 and ${hidden} earlier comment${hidden === 1 ? "" : "s"}`);
4267
+ }
4268
+ return lines;
4269
+ }
4270
+ function roundLines(round, now = Date.now()) {
4271
+ const b = round.boundary;
4272
+ const from = b ? `since ${actorLabel(b.actor)} ${b.action ? `moved "${b.action}"` : "moved it directly"} ${timestamp(b.at)}` : `since created ${timestamp(round.boundary_at)}`;
4273
+ const lines = [`round (${round.run_count} run${round.run_count === 1 ? "" : "s"} ${from}):`];
4274
+ for (const stage of round.stages) {
4275
+ const [latest, ...earlier] = stage.runs;
4276
+ if (!latest) continue;
4277
+ lines.push(` ${stage.state.name ?? stage.state.id} \u2014 ${runHeadline(latest, now)}`);
4278
+ if (latest.artifacts.length > 0) {
4279
+ lines.push(` ${latest.artifacts.map(artifactChangeLabel).join(" \xB7 ")}`);
4280
+ }
4281
+ for (const run of earlier) lines.push(` ${earlierAttemptLine(run, now)}`);
4282
+ if (latest.summary_comment) {
4283
+ lines.push(` summary (${latest.summary_comment.id}):`);
4284
+ for (const line of latest.summary_comment.body.split("\n")) lines.push(` ${line}`);
4285
+ }
4286
+ const earlierComments = latest.earlier_comment_ids;
4287
+ if (earlierComments.length > 0) {
4288
+ lines.push(
4289
+ ` ${earlierComments.length} earlier comment${earlierComments.length === 1 ? "" : "s"}: ${earlierComments.join(", ")}`
4290
+ );
4291
+ }
4292
+ }
4293
+ return lines;
4294
+ }
4295
+ function runHeadline(run, now) {
4296
+ const cost = runCostLabel(run);
4297
+ return [
4298
+ `${run.run_id} on ${run.runner_name}`,
4299
+ runDurationLabel(run, now),
4300
+ run.outcome ?? run.status,
4301
+ ...cost ? [cost] : [],
4302
+ run.transition ? `"${run.transition.action ?? "moved directly"}" \u2192 ${run.transition.to_state.name}` : "no transition"
4303
+ ].join(" \xB7 ");
4304
+ }
4305
+ function earlierAttemptLine(run, now) {
4306
+ const back = run.returned_via;
4307
+ const how = back ? back.action ? `sent back by ${back.from_state.name} ("${back.action}")` : "moved directly back" : "never came back";
4308
+ return `${run.run_id} \xB7 ${runDurationLabel(run, now)} \xB7 ${run.outcome ?? run.status} \xB7 ${how}`;
4309
+ }
4310
+ function artifactChangeLabel(a) {
4311
+ const versions = a.from_version === null ? `v${a.to_version}` : `v${a.from_version} \u2192 v${a.to_version}`;
4312
+ const extra = a.pr_url ? ` (${a.pr_url})` : a.files ? ` (${a.files.length} file${a.files.length === 1 ? "" : "s"})` : "";
4313
+ return `${a.name} ${versions}${extra}`;
4314
+ }
4315
+ function arrivedViaLabel(via) {
4316
+ if (!via) return "\u2014";
4317
+ return via.action ?? "moved directly";
4318
+ }
4319
+ function roundSummaryLabel(summary) {
4320
+ if (!summary) return "\u2014";
4321
+ const parts = [
4322
+ ...summary.pr_url ? [`PR ${summary.pr_url.split("/").pop()}`] : [],
4323
+ ...summary.artifacts.map((a) => `${a.name} v${a.version}`)
4324
+ ];
4325
+ return parts.length > 0 ? parts.join(" \xB7 ") : "\u2014";
4326
+ }
4237
4327
  function runRow(run) {
4238
4328
  return [
4239
4329
  run.id,
@@ -4257,21 +4347,20 @@ function byteSize(bytes) {
4257
4347
  const rounded = unit <= 1 ? Math.round(value) : Math.round(value * 10) / 10;
4258
4348
  return `${unit <= 1 ? rounded : rounded.toFixed(1)} ${units[unit]}`;
4259
4349
  }
4260
- function ageLabel(isoTimestamp, now = Date.now()) {
4261
- const then = Date.parse(isoTimestamp);
4262
- if (!Number.isFinite(then)) return "\u2014";
4263
- const seconds = Math.max(0, Math.round((now - then) / 1e3));
4264
- if (seconds < 60) return `${seconds}s`;
4265
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
4266
- if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
4267
- return `${Math.floor(seconds / 86400)}d`;
4350
+ function ageLabel2(isoTimestamp, now = Date.now()) {
4351
+ return ageLabel(Date.parse(isoTimestamp), now);
4352
+ }
4353
+ function hoursLabel(ms, now = Date.now()) {
4354
+ const hours = Math.max(0, now - ms) / 36e5;
4355
+ if (hours < 48) return `${hours < 10 ? hours.toFixed(1) : Math.round(hours)}h`;
4356
+ return `${Math.round(hours / 24)}d`;
4268
4357
  }
4269
4358
  function keptWorkspaceRow(kept, sizeBytes, now = Date.now()) {
4270
4359
  return [
4271
4360
  kept.run_id,
4272
4361
  kept.issue_ref ?? "\u2014",
4273
4362
  kept.status,
4274
- ageLabel(kept.kept_at, now),
4363
+ ageLabel2(kept.kept_at, now),
4275
4364
  byteSize(sizeBytes),
4276
4365
  kept.path
4277
4366
  ];
@@ -5270,6 +5359,14 @@ allowed actions: ${allowed.length ? allowed.join(", ") : "none (terminal state)"
5270
5359
  for (const line of rest) console.log(` ${line}`);
5271
5360
  }
5272
5361
  }
5362
+ if (issue.since_last_run && issue.effective_state.category === "active") {
5363
+ console.log("");
5364
+ for (const line of sinceLastRunLines(issue.since_last_run)) console.log(line);
5365
+ }
5366
+ if (issue.round && issue.effective_state.category === "awaiting_human") {
5367
+ console.log("");
5368
+ for (const line of roundLines(issue.round)) console.log(line);
5369
+ }
5273
5370
  if (issue.comments.length > 0) {
5274
5371
  console.log(`
5275
5372
  comments (${issue.comments.length}):`);
@@ -5390,6 +5487,25 @@ function register2(program3) {
5390
5487
  );
5391
5488
  printList(res, opts, (items) => {
5392
5489
  if (items.length === 0) return console.log(opts.ready ? "no ready issues" : "no issues");
5490
+ if (opts.category === "awaiting_human") {
5491
+ table([
5492
+ ["REF", "TITLE", "STATE", "WAITING", "VIA", "ROUND", ""],
5493
+ ...items.map((i) => [
5494
+ `${i.project_name}/${i.number}`,
5495
+ i.title,
5496
+ i.effective_state.name,
5497
+ ageLabel2(new Date(i.state_entered_at).toISOString()),
5498
+ arrivedViaLabel(i.arrived_via),
5499
+ roundSummaryLabel(i.round_summary ?? null),
5500
+ [
5501
+ i.open_blockers.length > 0 ? "blocked" : "",
5502
+ i.duplicate_of ? "dup" : "",
5503
+ ...i.labels.map((l) => `[${l.name}]`)
5504
+ ].filter(Boolean).join(" ")
5505
+ ])
5506
+ ]);
5507
+ return;
5508
+ }
5393
5509
  table([
5394
5510
  ["REF", "TITLE", "STATE", "CATEGORY", "LAST ACTIVITY", ""],
5395
5511
  ...items.map((i) => [
@@ -8258,20 +8374,108 @@ recent instances:`);
8258
8374
  }
8259
8375
 
8260
8376
  // src/commands/supervisor.ts
8377
+ function queueBlocks(groups) {
8378
+ const byKey = /* @__PURE__ */ new Map();
8379
+ for (const g of groups) {
8380
+ const key = `${g.verdict}|${g.runner_id ?? ""}`;
8381
+ const seen = byKey.get(key);
8382
+ if (seen) {
8383
+ seen.count += g.count;
8384
+ seen.groups.push(g);
8385
+ seen.binding ??= g.binding;
8386
+ } else {
8387
+ byKey.set(key, {
8388
+ verdict: g.verdict,
8389
+ runnerName: g.runner_name,
8390
+ binding: g.binding,
8391
+ count: g.count,
8392
+ groups: [g]
8393
+ });
8394
+ }
8395
+ }
8396
+ return [...byKey.values()].sort((a, b) => b.count - a.count);
8397
+ }
8398
+ function queueHeadline(block) {
8399
+ const runner = block.runnerName ?? "the routed runner";
8400
+ switch (block.verdict) {
8401
+ case "ok":
8402
+ return "dispatching next pass";
8403
+ case "at_capacity":
8404
+ return block.binding?.kind === "max_concurrent" ? `at capacity on ${runner} (${block.binding.current}/${block.binding.limit})` : `at capacity on ${runner}`;
8405
+ case "quota_exhausted":
8406
+ if (block.binding?.kind === "global_cap")
8407
+ return `global cap reached (${block.binding.current}/${block.binding.limit})`;
8408
+ if (block.binding?.kind === "state_roster")
8409
+ return `roster limit reached (${block.binding.current}/${block.binding.limit})`;
8410
+ return "quota exhausted";
8411
+ case "offline":
8412
+ return `${runner} offline`;
8413
+ case "paused":
8414
+ return `${runner} paused`;
8415
+ case "draining":
8416
+ return `${runner} draining`;
8417
+ case "backing_off":
8418
+ return `${runner} backing off`;
8419
+ case "no_rule":
8420
+ return "no matching routing rule";
8421
+ case "no_targets":
8422
+ return "the matching rule has no targets";
8423
+ case "ambiguous_rule":
8424
+ return "two routing rules tie";
8425
+ case "pin_missing":
8426
+ return "pinned to a runner that is gone";
8427
+ case "automation_off":
8428
+ return "automation is off";
8429
+ case "parked":
8430
+ return "parked";
8431
+ }
8432
+ }
8433
+ function queueFix(block) {
8434
+ const runner = block.runnerName ?? "the routed runner";
8435
+ switch (block.verdict) {
8436
+ case "at_capacity":
8437
+ return `raise the cap on ${runner} \u2014 restart its daemon with tines runner daemon --max-concurrent N, or edit the runner on the Agents page`;
8438
+ case "quota_exhausted":
8439
+ return block.binding?.kind === "state_roster" ? "raise the roster limit \u2014 tines supervisor quota roster --default <n> --state <workflow>/<state>=<n> (this replaces the whole roster, so restate every override you keep)" : "raise the cap \u2014 tines supervisor quota global <n> \u2014 or switch to a per-state roster with tines supervisor quota roster";
8440
+ case "offline":
8441
+ return `start the daemon on that machine \u2014 tines runner daemon`;
8442
+ case "paused":
8443
+ return `tines runners resume ${runner}`;
8444
+ case "no_rule":
8445
+ return "add a routing rule for that state \u2014 tines routing set <runner> --state <workflow>/<state>";
8446
+ case "no_targets":
8447
+ case "ambiguous_rule":
8448
+ return "tines routing list, then edit the rule";
8449
+ case "pin_missing":
8450
+ return "clear the pin on those issues";
8451
+ case "automation_off":
8452
+ return "tines supervisor enable";
8453
+ case "backing_off":
8454
+ return "retries automatically; check the daemon log if it keeps failing";
8455
+ default:
8456
+ return null;
8457
+ }
8458
+ }
8261
8459
  function register10(program3) {
8262
8460
  const supervisor = program3.command("supervisor").description("The automation kill switch, quota policy, and attempt limit");
8263
8461
  withCommon(
8264
8462
  supervisor.command("status").description("One-screen overview: kill switch, quota, utilization, runners")
8265
8463
  ).action(async (opts) => {
8266
8464
  const api = client(opts);
8267
- const [settings, runnersRes, workflows, activeRunItems] = await Promise.all([
8465
+ const [settings, runnersRes, workflows, activeRunItems, queue] = await Promise.all([
8268
8466
  api.getSupervisorSettings(),
8269
8467
  api.listRunners(),
8270
8468
  api.listWorkflows(),
8271
- listAll((page) => api.listRuns({ active: true, ...page }))
8469
+ listAll((page) => api.listRuns({ active: true, ...page })),
8470
+ api.getSupervisorQueue()
8272
8471
  ]);
8273
8472
  if (opts.json) {
8274
- return printJson({ settings, runners: runnersRes.items, active_runs: activeRunItems });
8473
+ return printJson({
8474
+ settings,
8475
+ runners: runnersRes.items,
8476
+ active_runs: activeRunItems,
8477
+ queue
8478
+ });
8275
8479
  }
8276
8480
  const stateNames = /* @__PURE__ */ new Map();
8277
8481
  for (const wf of workflows.items) {
@@ -8285,6 +8489,27 @@ function register10(program3) {
8285
8489
  `utilization: ${utilizationLabel(settings.quota, activeRunItems, (id) => stateNames.get(id) ?? id)}`
8286
8490
  );
8287
8491
  console.log(`attempt limit: ${settings.attempt_limit} strikes, then the issue parks`);
8492
+ if (queue.waiting === 0) {
8493
+ console.log("waiting: nothing");
8494
+ } else {
8495
+ console.log(`waiting: ${queue.waiting} ${queue.waiting === 1 ? "issue" : "issues"}`);
8496
+ for (const block of queueBlocks(queue.groups)) {
8497
+ const states = block.groups.map(
8498
+ (g) => `${stateNames.get(g.state_id) ?? g.state_name} ${g.count} (oldest ${hoursLabel(g.oldest_entered_at)})`
8499
+ ).join(" \xB7 ");
8500
+ console.log(` ${queueHeadline(block)}: ${states}`);
8501
+ const fix = queueFix(block);
8502
+ if (fix) console.log(` fix: ${fix}`);
8503
+ }
8504
+ }
8505
+ console.log(
8506
+ queue.parked.count === 0 ? "parked: none" : `parked: ${queue.parked.count}, oldest ${hoursLabel(queue.parked.oldest_entered_at ?? Date.now())}`
8507
+ );
8508
+ if (queue.awaiting_human.count > 0) {
8509
+ console.log(
8510
+ `awaiting you: ${queue.awaiting_human.count} issues, oldest ${hoursLabel(queue.awaiting_human.oldest_entered_at ?? Date.now())}`
8511
+ );
8512
+ }
8288
8513
  if (runnersRes.items.length === 0) {
8289
8514
  console.log("runners: none");
8290
8515
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.142",
3
+ "version": "0.0.144",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",