cruo-agent 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -45,64 +45,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
45
45
  ));
46
46
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
47
47
 
48
- // src/options.ts
49
- function stripTokenFlag(argv2) {
50
- const at = argv2.indexOf("--token");
51
- if (at < 0) return [...argv2];
52
- return [...argv2.slice(0, at), ...argv2.slice(at + 2)];
53
- }
54
- function numericIn(argv2, name, spec = {}) {
55
- const i = argv2.indexOf(`--${name}`);
56
- if (i < 0) return spec.fallback;
57
- const raw = argv2[i + 1];
58
- if (raw === void 0 || raw.startsWith("--")) {
59
- throw new OptionError(
60
- `--${name} expects a ${spec.integer ? "whole " : ""}number${spec.unit ? ` of ${spec.unit}` : ""}, but no value followed it.`
61
- );
62
- }
63
- const value = raw.trim() === "" ? NaN : Number(raw);
64
- const what = `--${name} ${JSON.stringify(raw)}`;
65
- if (!Number.isFinite(value)) {
66
- throw new OptionError(
67
- `${what} is not a number.` + // Reached only by something like `-abc`. A plain `-5` is a finite
68
- // negative and falls to the range check below, which is the accurate
69
- // refusal for it — this hint is for the case where the leading dash is
70
- // the reason it did not parse at all.
71
- (raw.startsWith("-") ? " A leading `-` reads as a flag, not a sign." : "")
72
- );
73
- }
74
- if (value <= 0) {
75
- throw new OptionError(`${what} must be greater than zero${spec.unit ? ` ${spec.unit}` : ""}.`);
76
- }
77
- if (spec.integer && !Number.isInteger(value)) {
78
- throw new OptionError(`${what} must be a whole number.`);
79
- }
80
- return value;
81
- }
82
- function requiredNumericIn(argv2, name, fallback, spec = {}) {
83
- return numericIn(argv2, name, { ...spec, fallback });
84
- }
85
- function secondsIn(argv2, name, fallbackSeconds) {
86
- return requiredNumericIn(argv2, name, fallbackSeconds, { unit: "seconds" }) * 1e3;
87
- }
88
- var OptionError, flagIn, optIn;
89
- var init_options = __esm({
90
- "src/options.ts"() {
91
- "use strict";
92
- OptionError = class extends Error {
93
- constructor(message) {
94
- super(message);
95
- this.name = "OptionError";
96
- }
97
- };
98
- flagIn = (argv2, name) => argv2.includes(`--${name}`);
99
- optIn = (argv2, name, fallback) => {
100
- const i = argv2.indexOf(`--${name}`);
101
- return i >= 0 && argv2[i + 1] && !argv2[i + 1].startsWith("--") ? argv2[i + 1] : fallback;
102
- };
103
- }
104
- });
105
-
106
48
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
107
49
  // @__NO_SIDE_EFFECTS__
108
50
  function $constructor(name, initializer3, params) {
@@ -14032,17 +13974,17 @@ var init_schemas2 = __esm({
14032
13974
  inst._zod.processJSONSchema = (ctx, json2, params) => arrayProcessor(inst, ctx, json2, params);
14033
13975
  inst.element = def.element;
14034
13976
  _installLazyMethods(inst, "ZodArray", {
14035
- min(n, params) {
14036
- return this.check(_minLength(n, params));
13977
+ min(n2, params) {
13978
+ return this.check(_minLength(n2, params));
14037
13979
  },
14038
13980
  nonempty(params) {
14039
13981
  return this.check(_minLength(1, params));
14040
13982
  },
14041
- max(n, params) {
14042
- return this.check(_maxLength(n, params));
13983
+ max(n2, params) {
13984
+ return this.check(_maxLength(n2, params));
14043
13985
  },
14044
- length(n, params) {
14045
- return this.check(_length(n, params));
13986
+ length(n2, params) {
13987
+ return this.check(_length(n2, params));
14046
13988
  },
14047
13989
  unwrap() {
14048
13990
  return this.element;
@@ -15185,7 +15127,7 @@ var init_zod = __esm({
15185
15127
  });
15186
15128
 
15187
15129
  // ../../packages/core/dist/schemas.js
15188
- var stateCategory, issuePriority, memberRole, projectKey, createProjectInput, createIssueInput, updateIssueInput, moveIssueInput, workflowStateInput, listIssuesFilter;
15130
+ var stateCategory, issuePriority, memberRole, projectKey, createProjectInput, estimateHours, createIssueInput, updateIssueInput, moveIssueInput, workflowStateInput, listIssuesFilter;
15189
15131
  var init_schemas3 = __esm({
15190
15132
  "../../packages/core/dist/schemas.js"() {
15191
15133
  "use strict";
@@ -15213,6 +15155,7 @@ var init_schemas3 = __esm({
15213
15155
  description: external_exports.string().trim().max(2e3).optional(),
15214
15156
  leadId: external_exports.string().uuid().optional()
15215
15157
  });
15158
+ estimateHours = external_exports.number().positive().max(9999).multipleOf(0.01).nullish();
15216
15159
  createIssueInput = external_exports.object({
15217
15160
  projectId: external_exports.string().uuid(),
15218
15161
  title: external_exports.string().trim().min(1).max(300),
@@ -15223,6 +15166,7 @@ var init_schemas3 = __esm({
15223
15166
  parentId: external_exports.string().uuid().nullish(),
15224
15167
  dueDate: external_exports.string().date().nullish(),
15225
15168
  // YYYY-MM-DD
15169
+ estimateHours,
15226
15170
  labelIds: external_exports.array(external_exports.string().uuid()).optional()
15227
15171
  });
15228
15172
  updateIssueInput = external_exports.object({
@@ -15230,7 +15174,8 @@ var init_schemas3 = __esm({
15230
15174
  description: external_exports.string().trim().max(2e4).nullish(),
15231
15175
  priority: issuePriority.optional(),
15232
15176
  assigneeId: external_exports.string().uuid().nullish(),
15233
- dueDate: external_exports.string().date().nullish()
15177
+ dueDate: external_exports.string().date().nullish(),
15178
+ estimateHours
15234
15179
  });
15235
15180
  moveIssueInput = external_exports.object({
15236
15181
  issueId: external_exports.string().uuid(),
@@ -15288,6 +15233,13 @@ var init_identity = __esm({
15288
15233
  }
15289
15234
  });
15290
15235
 
15236
+ // ../../packages/core/dist/effort.js
15237
+ var init_effort = __esm({
15238
+ "../../packages/core/dist/effort.js"() {
15239
+ "use strict";
15240
+ }
15241
+ });
15242
+
15291
15243
  // ../../packages/core/dist/plans.js
15292
15244
  var init_plans = __esm({
15293
15245
  "../../packages/core/dist/plans.js"() {
@@ -15339,6 +15291,7 @@ var init_dist = __esm({
15339
15291
  init_schemas3();
15340
15292
  init_rank();
15341
15293
  init_identity();
15294
+ init_effort();
15342
15295
  init_plans();
15343
15296
  init_mcp_connect();
15344
15297
  init_tree();
@@ -15349,8 +15302,8 @@ var init_dist = __esm({
15349
15302
 
15350
15303
  // ../../packages/mcp-kit/dist/env.js
15351
15304
  function pick2(...names) {
15352
- for (const n of names) {
15353
- const v = process.env[n];
15305
+ for (const n2 of names) {
15306
+ const v = process.env[n2];
15354
15307
  if (v && v.trim())
15355
15308
  return v.trim();
15356
15309
  }
@@ -15570,9 +15523,9 @@ function __generator(thisArg, body) {
15570
15523
  return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() {
15571
15524
  return this;
15572
15525
  }), g;
15573
- function verb(n) {
15526
+ function verb(n2) {
15574
15527
  return function(v) {
15575
- return step([n, v]);
15528
+ return step([n2, v]);
15576
15529
  };
15577
15530
  }
15578
15531
  function step(op) {
@@ -15645,12 +15598,12 @@ function __values(o) {
15645
15598
  };
15646
15599
  throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
15647
15600
  }
15648
- function __read(o, n) {
15601
+ function __read(o, n2) {
15649
15602
  var m = typeof Symbol === "function" && o[Symbol.iterator];
15650
15603
  if (!m) return o;
15651
15604
  var i = m.call(o), r, ar = [], e;
15652
15605
  try {
15653
- while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
15606
+ while ((n2 === void 0 || n2-- > 0) && !(r = i.next()).done) ar.push(r.value);
15654
15607
  } catch (error51) {
15655
15608
  e = { error: error51 };
15656
15609
  } finally {
@@ -15697,19 +15650,19 @@ function __asyncGenerator(thisArg, _arguments, generator) {
15697
15650
  return Promise.resolve(v).then(f, reject);
15698
15651
  };
15699
15652
  }
15700
- function verb(n, f) {
15701
- if (g[n]) {
15702
- i[n] = function(v) {
15653
+ function verb(n2, f) {
15654
+ if (g[n2]) {
15655
+ i[n2] = function(v) {
15703
15656
  return new Promise(function(a, b) {
15704
- q.push([n, v, a, b]) > 1 || resume(n, v);
15657
+ q.push([n2, v, a, b]) > 1 || resume(n2, v);
15705
15658
  });
15706
15659
  };
15707
- if (f) i[n] = f(i[n]);
15660
+ if (f) i[n2] = f(i[n2]);
15708
15661
  }
15709
15662
  }
15710
- function resume(n, v) {
15663
+ function resume(n2, v) {
15711
15664
  try {
15712
- step(g[n](v));
15665
+ step(g[n2](v));
15713
15666
  } catch (e) {
15714
15667
  settle(q[0][3], e);
15715
15668
  }
@@ -15734,9 +15687,9 @@ function __asyncDelegator(o) {
15734
15687
  }), verb("return"), i[Symbol.iterator] = function() {
15735
15688
  return this;
15736
15689
  }, i;
15737
- function verb(n, f) {
15738
- i[n] = o[n] ? function(v) {
15739
- return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v;
15690
+ function verb(n2, f) {
15691
+ i[n2] = o[n2] ? function(v) {
15692
+ return (p = !p) ? { value: __await(o[n2](v)), done: false } : f ? f(v) : v;
15740
15693
  } : f;
15741
15694
  }
15742
15695
  }
@@ -15746,10 +15699,10 @@ function __asyncValues(o) {
15746
15699
  return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function() {
15747
15700
  return this;
15748
15701
  }, i);
15749
- function verb(n) {
15750
- i[n] = o[n] && function(v) {
15702
+ function verb(n2) {
15703
+ i[n2] = o[n2] && function(v) {
15751
15704
  return new Promise(function(resolve2, reject) {
15752
- v = o[n](v), settle(resolve2, reject, v.done, v.value);
15705
+ v = o[n2](v), settle(resolve2, reject, v.done, v.value);
15753
15706
  });
15754
15707
  };
15755
15708
  }
@@ -15868,7 +15821,7 @@ var init_tslib_es6 = __esm({
15868
15821
  };
15869
15822
  __assign = function() {
15870
15823
  __assign = Object.assign || function __assign2(t) {
15871
- for (var s, i = 1, n = arguments.length; i < n; i++) {
15824
+ for (var s, i = 1, n2 = arguments.length; i < n2; i++) {
15872
15825
  s = arguments[i];
15873
15826
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
15874
15827
  }
@@ -31053,8 +31006,8 @@ var require_GoTrueClient = __commonJS({
31053
31006
  const result = await this.initializePromise;
31054
31007
  const queue = (_a3 = this._pendingInitNotifications) !== null && _a3 !== void 0 ? _a3 : [];
31055
31008
  this._pendingInitNotifications = null;
31056
- for (const n of queue) {
31057
- await this._notifyAllSubscribers(n.event, n.session, n.broadcast);
31009
+ for (const n2 of queue) {
31010
+ await this._notifyAllSubscribers(n2.event, n2.session, n2.broadcast);
31058
31011
  }
31059
31012
  return result;
31060
31013
  }
@@ -39404,17 +39357,17 @@ var require_codegen = __commonJS({
39404
39357
  this.nodes = nodes;
39405
39358
  }
39406
39359
  render(opts) {
39407
- return this.nodes.reduce((code, n) => code + n.render(opts), "");
39360
+ return this.nodes.reduce((code, n2) => code + n2.render(opts), "");
39408
39361
  }
39409
39362
  optimizeNodes() {
39410
39363
  const { nodes } = this;
39411
39364
  let i = nodes.length;
39412
39365
  while (i--) {
39413
- const n = nodes[i].optimizeNodes();
39414
- if (Array.isArray(n))
39415
- nodes.splice(i, 1, ...n);
39416
- else if (n)
39417
- nodes[i] = n;
39366
+ const n2 = nodes[i].optimizeNodes();
39367
+ if (Array.isArray(n2))
39368
+ nodes.splice(i, 1, ...n2);
39369
+ else if (n2)
39370
+ nodes[i] = n2;
39418
39371
  else
39419
39372
  nodes.splice(i, 1);
39420
39373
  }
@@ -39424,16 +39377,16 @@ var require_codegen = __commonJS({
39424
39377
  const { nodes } = this;
39425
39378
  let i = nodes.length;
39426
39379
  while (i--) {
39427
- const n = nodes[i];
39428
- if (n.optimizeNames(names, constants))
39380
+ const n2 = nodes[i];
39381
+ if (n2.optimizeNames(names, constants))
39429
39382
  continue;
39430
- subtractNames(names, n.names);
39383
+ subtractNames(names, n2.names);
39431
39384
  nodes.splice(i, 1);
39432
39385
  }
39433
39386
  return nodes.length > 0 ? this : void 0;
39434
39387
  }
39435
39388
  get names() {
39436
- return this.nodes.reduce((names, n) => addNames(names, n.names), {});
39389
+ return this.nodes.reduce((names, n2) => addNames(names, n2.names), {});
39437
39390
  }
39438
39391
  };
39439
39392
  var BlockNode = class extends ParentNode {
@@ -39844,8 +39797,8 @@ var require_codegen = __commonJS({
39844
39797
  endFunc() {
39845
39798
  return this._endBlockNode(Func);
39846
39799
  }
39847
- optimize(n = 1) {
39848
- while (n-- > 0) {
39800
+ optimize(n2 = 1) {
39801
+ while (n2-- > 0) {
39849
39802
  this._root.optimizeNodes();
39850
39803
  this._root.optimizeNames(this._root.names, this._constants);
39851
39804
  }
@@ -39859,19 +39812,19 @@ var require_codegen = __commonJS({
39859
39812
  this._nodes.push(node);
39860
39813
  }
39861
39814
  _endBlockNode(N1, N2) {
39862
- const n = this._currNode;
39863
- if (n instanceof N1 || N2 && n instanceof N2) {
39815
+ const n2 = this._currNode;
39816
+ if (n2 instanceof N1 || N2 && n2 instanceof N2) {
39864
39817
  this._nodes.pop();
39865
39818
  return this;
39866
39819
  }
39867
39820
  throw new Error(`CodeGen: not in block "${N2 ? `${N1.kind}/${N2.kind}` : N1.kind}"`);
39868
39821
  }
39869
39822
  _elseNode(node) {
39870
- const n = this._currNode;
39871
- if (!(n instanceof If)) {
39823
+ const n2 = this._currNode;
39824
+ if (!(n2 instanceof If)) {
39872
39825
  throw new Error('CodeGen: "else" without "if"');
39873
39826
  }
39874
- this._currNode = n.else = node;
39827
+ this._currNode = n2.else = node;
39875
39828
  return this;
39876
39829
  }
39877
39830
  get _root() {
@@ -39888,8 +39841,8 @@ var require_codegen = __commonJS({
39888
39841
  };
39889
39842
  exports.CodeGen = CodeGen;
39890
39843
  function addNames(names, from) {
39891
- for (const n in from)
39892
- names[n] = (names[n] || 0) + (from[n] || 0);
39844
+ for (const n2 in from)
39845
+ names[n2] = (names[n2] || 0) + (from[n2] || 0);
39893
39846
  return names;
39894
39847
  }
39895
39848
  function addExprNames(names, from) {
@@ -39909,11 +39862,11 @@ var require_codegen = __commonJS({
39909
39862
  items.push(c);
39910
39863
  return items;
39911
39864
  }, []));
39912
- function replaceName(n) {
39913
- const c = constants[n.str];
39914
- if (c === void 0 || names[n.str] !== 1)
39915
- return n;
39916
- delete names[n.str];
39865
+ function replaceName(n2) {
39866
+ const c = constants[n2.str];
39867
+ if (c === void 0 || names[n2.str] !== 1)
39868
+ return n2;
39869
+ delete names[n2.str];
39917
39870
  return c;
39918
39871
  }
39919
39872
  function canOptimize(e) {
@@ -39921,8 +39874,8 @@ var require_codegen = __commonJS({
39921
39874
  }
39922
39875
  }
39923
39876
  function subtractNames(names, from) {
39924
- for (const n in from)
39925
- names[n] = (names[n] || 0) - (from[n] || 0);
39877
+ for (const n2 in from)
39878
+ names[n2] = (names[n2] || 0) - (from[n2] || 0);
39926
39879
  }
39927
39880
  function not(x) {
39928
39881
  return typeof x == "boolean" || typeof x == "number" || x === null ? !x : (0, code_1._)`!${par(x)}`;
@@ -45938,6 +45891,64 @@ var init_dist7 = __esm({
45938
45891
  }
45939
45892
  });
45940
45893
 
45894
+ // src/options.ts
45895
+ function stripTokenFlag(argv2) {
45896
+ const at = argv2.indexOf("--token");
45897
+ if (at < 0) return [...argv2];
45898
+ return [...argv2.slice(0, at), ...argv2.slice(at + 2)];
45899
+ }
45900
+ function numericIn(argv2, name, spec = {}) {
45901
+ const i = argv2.indexOf(`--${name}`);
45902
+ if (i < 0) return spec.fallback;
45903
+ const raw = argv2[i + 1];
45904
+ if (raw === void 0 || raw.startsWith("--")) {
45905
+ throw new OptionError(
45906
+ `--${name} expects a ${spec.integer ? "whole " : ""}number${spec.unit ? ` of ${spec.unit}` : ""}, but no value followed it.`
45907
+ );
45908
+ }
45909
+ const value = raw.trim() === "" ? NaN : Number(raw);
45910
+ const what = `--${name} ${JSON.stringify(raw)}`;
45911
+ if (!Number.isFinite(value)) {
45912
+ throw new OptionError(
45913
+ `${what} is not a number.` + // Reached only by something like `-abc`. A plain `-5` is a finite
45914
+ // negative and falls to the range check below, which is the accurate
45915
+ // refusal for it — this hint is for the case where the leading dash is
45916
+ // the reason it did not parse at all.
45917
+ (raw.startsWith("-") ? " A leading `-` reads as a flag, not a sign." : "")
45918
+ );
45919
+ }
45920
+ if (value <= 0) {
45921
+ throw new OptionError(`${what} must be greater than zero${spec.unit ? ` ${spec.unit}` : ""}.`);
45922
+ }
45923
+ if (spec.integer && !Number.isInteger(value)) {
45924
+ throw new OptionError(`${what} must be a whole number.`);
45925
+ }
45926
+ return value;
45927
+ }
45928
+ function requiredNumericIn(argv2, name, fallback, spec = {}) {
45929
+ return numericIn(argv2, name, { ...spec, fallback });
45930
+ }
45931
+ function secondsIn(argv2, name, fallbackSeconds) {
45932
+ return requiredNumericIn(argv2, name, fallbackSeconds, { unit: "seconds" }) * 1e3;
45933
+ }
45934
+ var OptionError, flagIn, optIn;
45935
+ var init_options = __esm({
45936
+ "src/options.ts"() {
45937
+ "use strict";
45938
+ OptionError = class extends Error {
45939
+ constructor(message) {
45940
+ super(message);
45941
+ this.name = "OptionError";
45942
+ }
45943
+ };
45944
+ flagIn = (argv2, name) => argv2.includes(`--${name}`);
45945
+ optIn = (argv2, name, fallback) => {
45946
+ const i = argv2.indexOf(`--${name}`);
45947
+ return i >= 0 && argv2[i + 1] && !argv2[i + 1].startsWith("--") ? argv2[i + 1] : fallback;
45948
+ };
45949
+ }
45950
+ });
45951
+
45941
45952
  // src/env.ts
45942
45953
  function applyCloudDefaults() {
45943
45954
  const url3 = process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
@@ -45990,6 +46001,21 @@ function childSession() {
45990
46001
  const endpoint = sessionEndpoint();
45991
46002
  return endpoint ? fetchDelegatedSession(endpoint, token) : mintDelegatedSession({ config: config2, token, product: "projects", mintedAt: MINTED_AT });
45992
46003
  }
46004
+ async function contextOrExplain() {
46005
+ try {
46006
+ return await getContext();
46007
+ } catch (error51) {
46008
+ const label = process.env.CRUO_IDENTITY?.trim();
46009
+ if (!label) throw error51;
46010
+ throw new Error(
46011
+ `${error51 instanceof Error ? error51.message : String(error51)}
46012
+
46013
+ That is the token stored for \`${label}\`.
46014
+ Replace it: Cruo \u2192 Settings \u2192 Members \u2192 ${label} \u2192 new token,
46015
+ then: cruo login <token> --as ${label}`
46016
+ );
46017
+ }
46018
+ }
45993
46019
  var stdioResolver;
45994
46020
  var init_auth = __esm({
45995
46021
  "src/auth.ts"() {
@@ -46001,6 +46027,176 @@ var init_auth = __esm({
46001
46027
  }
46002
46028
  });
46003
46029
 
46030
+ // src/usage.ts
46031
+ var usage_exports = {};
46032
+ __export(usage_exports, {
46033
+ reportUsage: () => reportUsage
46034
+ });
46035
+ function compact(value) {
46036
+ if (value === 0) return "\u2014";
46037
+ if (value >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
46038
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
46039
+ return String(value);
46040
+ }
46041
+ async function reportUsage(opts) {
46042
+ const ctx = await contextOrExplain();
46043
+ const since = new Date(Date.now() - opts.days * 864e5).toISOString();
46044
+ let query = ctx.client.from("harness_runs").select("*").gte("started_at", since).order("started_at", { ascending: false });
46045
+ let issueTitle = null;
46046
+ if (opts.issue) {
46047
+ const [key, num3] = opts.issue.split("-");
46048
+ const { data: project } = await ctx.client.from("projects").select("id").eq("key", (key ?? "").toUpperCase()).maybeSingle();
46049
+ const { data: issue2 } = project ? await ctx.client.from("issues").select("id, title").eq("project_id", project.id).eq("number", Number(num3)).maybeSingle() : { data: null };
46050
+ if (!issue2) {
46051
+ console.error(`
46052
+ No issue ${opts.issue} on this board.
46053
+ `);
46054
+ process.exit(2);
46055
+ }
46056
+ issueTitle = issue2.title;
46057
+ query = query.eq("issue_id", issue2.id);
46058
+ }
46059
+ const { data, error: error51 } = await query;
46060
+ if (error51) {
46061
+ console.error(`
46062
+ Could not read run costs: ${error51.message}
46063
+ `);
46064
+ process.exit(1);
46065
+ }
46066
+ const rows = data ?? [];
46067
+ console.log(
46068
+ `
46069
+ ${opts.issue ? `${opts.issue} \u2014 ${issueTitle}` : ctx.workspaceName} \xB7 last ${opts.days} day${opts.days === 1 ? "" : "s"}`
46070
+ );
46071
+ if (rows.length === 0) {
46072
+ console.log(
46073
+ `
46074
+ No runs recorded. Either no agent has run in that window, or the runs
46075
+ predate cost recording (\`CRA-89\`, 2026-08-29).
46076
+ `
46077
+ );
46078
+ return;
46079
+ }
46080
+ const ids = [...new Set(rows.map((r) => r.agent_id))];
46081
+ const { data: people } = await ctx.client.schema("public").from("profiles").select("id, display_name, email").in("id", ids);
46082
+ const nameOf = (id) => {
46083
+ const p = (people ?? []).find((x) => x.id === id);
46084
+ return p?.display_name || p?.email || id.slice(0, 8);
46085
+ };
46086
+ const blank = () => ({
46087
+ runs: 0,
46088
+ productive: 0,
46089
+ neverRan: 0,
46090
+ ms: 0,
46091
+ bytes: 0,
46092
+ input: 0,
46093
+ output: 0,
46094
+ cacheRead: 0,
46095
+ cacheWrite: 0,
46096
+ cost: 0,
46097
+ measured: 0
46098
+ });
46099
+ const fold = (a, r) => {
46100
+ a.runs += 1;
46101
+ if (r.outcome === "productive") a.productive += 1;
46102
+ if (r.outcome === "never-ran") a.neverRan += 1;
46103
+ a.ms += r.duration_ms;
46104
+ a.bytes += n(r.stdout_bytes);
46105
+ a.input += n(r.input_tokens);
46106
+ a.output += n(r.output_tokens);
46107
+ a.cacheRead += n(r.cache_read_tokens);
46108
+ a.cacheWrite += n(r.cache_write_tokens);
46109
+ a.cost += n(r.cost_usd);
46110
+ if (r.input_tokens !== null || r.cost_usd !== null) a.measured += 1;
46111
+ };
46112
+ const byAgent = /* @__PURE__ */ new Map();
46113
+ const total = blank();
46114
+ for (const r of rows) {
46115
+ if (!byAgent.has(r.agent_id)) byAgent.set(r.agent_id, blank());
46116
+ fold(byAgent.get(r.agent_id), r);
46117
+ fold(total, r);
46118
+ }
46119
+ const w = Math.max(12, ...[...byAgent.keys()].map((id) => nameOf(id).length));
46120
+ console.log(
46121
+ `
46122
+ ${pad("agent", w)} ${rpad("runs", 5)} ${rpad("useful", 7)} ${rpad("dead", 5)} ${rpad("time", 7)} ${rpad("in", 8)} ${rpad("out", 8)} ${rpad("cost", 9)}`
46123
+ );
46124
+ for (const [id, a] of [...byAgent].sort((x, y) => y[1].runs - x[1].runs)) {
46125
+ console.log(
46126
+ ` ${pad(nameOf(id), w)} ${rpad(String(a.runs), 5)} ${rpad(String(a.productive), 7)} ${rpad(String(a.neverRan), 5)} ${rpad(duration3(a.ms), 7)} ${rpad(compact(a.input + a.cacheRead), 8)} ${rpad(compact(a.output), 8)} ${rpad(a.cost > 0 ? `$${a.cost.toFixed(2)}` : "\u2014", 9)}`
46127
+ );
46128
+ }
46129
+ if (!opts.issue) {
46130
+ const byIssue = /* @__PURE__ */ new Map();
46131
+ for (const r of rows) {
46132
+ if (!r.issue_id) continue;
46133
+ if (!byIssue.has(r.issue_id)) byIssue.set(r.issue_id, blank());
46134
+ fold(byIssue.get(r.issue_id), r);
46135
+ }
46136
+ const worst = [...byIssue].sort((x, y) => y[1].runs - x[1].runs).slice(0, 8);
46137
+ if (worst.length > 0) {
46138
+ const { data: issues } = await ctx.client.from("issues").select("id, number, title, project_id").in("id", worst.map(([id]) => id));
46139
+ const { data: projects } = await ctx.client.from("projects").select("id, key");
46140
+ const refOf = (id) => {
46141
+ const i = (issues ?? []).find((x) => x.id === id);
46142
+ if (!i) return id.slice(0, 8);
46143
+ const key = (projects ?? []).find((p) => p.id === i.project_id)?.key ?? "?";
46144
+ return `${key}-${i.number}`;
46145
+ };
46146
+ const titleOf = (id) => (issues ?? []).find((x) => x.id === id)?.title ?? "";
46147
+ console.log(`
46148
+ most-worked cards`);
46149
+ for (const [id, a] of worst) {
46150
+ console.log(
46151
+ ` ${rpad(String(a.runs), 3)} run${a.runs === 1 ? " " : "s"} ${pad(refOf(id), 8)} ${rpad(duration3(a.ms), 6)} ${titleOf(id).slice(0, 52)}`
46152
+ );
46153
+ }
46154
+ }
46155
+ } else {
46156
+ console.log(`
46157
+ run by run`);
46158
+ for (const r of rows) {
46159
+ console.log(
46160
+ ` ${r.started_at.slice(0, 16).replace("T", " ")} ${pad(r.outcome, 13)} ${rpad(duration3(r.duration_ms), 6)} ${rpad(compact(n(r.output_tokens)), 7)} out ${nameOf(r.agent_id)}`
46161
+ );
46162
+ }
46163
+ }
46164
+ console.log(
46165
+ `
46166
+ ${total.runs} run(s), ${total.productive} productive, ${total.neverRan} never reached a model.
46167
+ ${duration3(total.ms)} of wall clock, ${compact(total.bytes)} bytes of output.`
46168
+ );
46169
+ if (total.measured === 0) {
46170
+ console.log(
46171
+ `
46172
+ No token or cost figures: this harness does not report them. Runs, time and
46173
+ output are measured for any harness; tokens need one that says what it spent \u2014
46174
+ e.g. --harness "claude -p --output-format json".
46175
+ `
46176
+ );
46177
+ } else if (total.measured < total.runs) {
46178
+ console.log(
46179
+ `
46180
+ ${total.measured} of ${total.runs} runs reported tokens; the rest did not, so the
46181
+ figures above are a floor rather than a total.
46182
+ `
46183
+ );
46184
+ } else {
46185
+ console.log("");
46186
+ }
46187
+ }
46188
+ var n, pad, rpad, duration3;
46189
+ var init_usage = __esm({
46190
+ "src/usage.ts"() {
46191
+ "use strict";
46192
+ init_auth();
46193
+ n = (v) => v === null || v === void 0 ? 0 : Number(v);
46194
+ pad = (s, w) => s.padEnd(w);
46195
+ rpad = (s, w) => s.padStart(w);
46196
+ duration3 = (ms2) => ms2 >= 36e5 ? `${(ms2 / 36e5).toFixed(1)}h` : ms2 >= 6e4 ? `${(ms2 / 6e4).toFixed(1)}m` : `${Math.round(ms2 / 1e3)}s`;
46197
+ }
46198
+ });
46199
+
46004
46200
  // src/harness-signal.ts
46005
46201
  function refusalIn(stdout) {
46006
46202
  if (!stdout.trim()) return null;
@@ -46012,7 +46208,36 @@ function refusalIn(stdout) {
46012
46208
  function neverRan(run) {
46013
46209
  return run.refusal !== null || run.stdoutBytes === 0;
46014
46210
  }
46015
- var REFUSALS;
46211
+ function usageIn(text) {
46212
+ const trimmed = text.trimEnd();
46213
+ if (!trimmed.endsWith("}")) return null;
46214
+ let parsed = null;
46215
+ for (let i = trimmed.length - 1; i >= 0; i -= 1) {
46216
+ if (trimmed[i] !== "{") continue;
46217
+ try {
46218
+ parsed = JSON.parse(trimmed.slice(i));
46219
+ break;
46220
+ } catch {
46221
+ }
46222
+ }
46223
+ if (!parsed || typeof parsed !== "object") return null;
46224
+ const root = parsed;
46225
+ const usage = root.usage ?? root;
46226
+ const out = {
46227
+ model: typeof root.model === "string" ? root.model : void 0,
46228
+ // Both spellings. The snake_case ones are the API's; a harness that passes
46229
+ // its response through unchanged emits those, and one that tidies them
46230
+ // emits camelCase.
46231
+ inputTokens: num(usage.input_tokens) ?? num(usage.inputTokens),
46232
+ outputTokens: num(usage.output_tokens) ?? num(usage.outputTokens),
46233
+ cacheReadTokens: num(usage.cache_read_input_tokens) ?? num(usage.cacheReadInputTokens),
46234
+ cacheWriteTokens: num(usage.cache_creation_input_tokens) ?? num(usage.cacheCreationInputTokens),
46235
+ costUsd: num(root.total_cost_usd) ?? num(root.totalCostUsd) ?? num(root.cost_usd)
46236
+ };
46237
+ const measured = Object.values(out).some((v) => v !== void 0);
46238
+ return measured ? out : null;
46239
+ }
46240
+ var REFUSALS, num;
46016
46241
  var init_harness_signal = __esm({
46017
46242
  "src/harness-signal.ts"() {
46018
46243
  "use strict";
@@ -46023,6 +46248,47 @@ var init_harness_signal = __esm({
46023
46248
  [/invalid api key|authentication[_ ]error|please run \/login/i, "the harness is not signed in"],
46024
46249
  [/rate limit(ed| exceeded|s? reached)\b.*\btry again/i, "the account is rate limited"]
46025
46250
  ];
46251
+ num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
46252
+ }
46253
+ });
46254
+
46255
+ // src/pacing.ts
46256
+ function afterAttempt(p, attempt, limits, now) {
46257
+ const allDead = attempt.invoked > 0 && attempt.failedToRun === attempt.invoked;
46258
+ const deadTicks = allDead ? p.deadTicks + 1 : 0;
46259
+ if (deadTicks < 2) {
46260
+ return {
46261
+ next: { deadTicks, invokeAfter: 0, backedOff: false },
46262
+ event: p.backedOff ? "recovered" : null,
46263
+ cooldownMs: 0
46264
+ };
46265
+ }
46266
+ const cooldownMs = Math.min(
46267
+ limits.intervalMs * 2 ** (deadTicks - 1),
46268
+ limits.maxIntervalMs
46269
+ );
46270
+ return {
46271
+ next: { deadTicks, invokeAfter: now + cooldownMs, backedOff: true },
46272
+ // Said once per descent, not once per tick. The ceiling is reached in a few
46273
+ // ticks and held for hours; repeating the line there would bury the
46274
+ // `recovered` that matters.
46275
+ event: p.backedOff ? null : "backing-off",
46276
+ cooldownMs
46277
+ };
46278
+ }
46279
+ var freshPacing, allowanceAt;
46280
+ var init_pacing = __esm({
46281
+ "src/pacing.ts"() {
46282
+ "use strict";
46283
+ freshPacing = () => ({
46284
+ deadTicks: 0,
46285
+ invokeAfter: 0,
46286
+ backedOff: false
46287
+ });
46288
+ allowanceAt = (p, now) => {
46289
+ if (!p.backedOff) return "free";
46290
+ return now >= p.invokeAfter ? "probe" : "held";
46291
+ };
46026
46292
  }
46027
46293
  });
46028
46294
 
@@ -46253,10 +46519,10 @@ async function pollMentions(ctx, identity) {
46253
46519
  if (error51) throw new Error(`Mention poll failed: ${error51.message}`);
46254
46520
  if (!notes || notes.length === 0) return [];
46255
46521
  const byIssue = /* @__PURE__ */ new Map();
46256
- for (const n of notes) {
46257
- const list = byIssue.get(n.issue_id) ?? [];
46258
- list.push(n.id);
46259
- byIssue.set(n.issue_id, list);
46522
+ for (const n2 of notes) {
46523
+ const list = byIssue.get(n2.issue_id) ?? [];
46524
+ list.push(n2.id);
46525
+ byIssue.set(n2.issue_id, list);
46260
46526
  }
46261
46527
  const { data: issues, error: issueErr } = await ctx.client.from("issues").select("*").in("id", [...byIssue.keys()]);
46262
46528
  if (issueErr) throw new Error(`Mention issue lookup failed: ${issueErr.message}`);
@@ -46450,8 +46716,18 @@ function systemPrompt(ctx, identity, worktree) {
46450
46716
  `survives. A run that ends with uncommitted changes has produced nothing,`,
46451
46717
  `however good the changes were.`,
46452
46718
  ``,
46453
- `Do not push, and do not merge into any other branch. Publishing is done`,
46454
- `for you after you exit, by the process that started you.`
46719
+ `Do not push, and do not merge into any other branch. That is not a`,
46720
+ `matter of trust \u2014 you have no push tool, and nothing in this loop`,
46721
+ `merges anything (\`CRA-88\`). Releasing is a person's job.`,
46722
+ ...options.push ? [
46723
+ `Your branch is published for you after you exit, by the process`,
46724
+ `that started you. It is pushed, not merged: somebody still has`,
46725
+ `to review and land it.`
46726
+ ] : [
46727
+ `Your branch stays on this machine. Nothing publishes it, so say`,
46728
+ `on the card which branch holds the work \u2014 that name is the only`,
46729
+ `way anyone finds it.`
46730
+ ]
46455
46731
  ] : [],
46456
46732
  ``,
46457
46733
  `You act on the board through the Cruo MCP tools. Start with get_issue to read the`,
@@ -46530,11 +46806,14 @@ async function invokeHarness(ctx, identity, hit, worktree) {
46530
46806
  cwd,
46531
46807
  env: harnessEnv()
46532
46808
  });
46809
+ const startedAtMs = Date.now();
46533
46810
  let stdoutBytes = 0;
46811
+ let tail = "";
46534
46812
  let head2 = "";
46535
46813
  child.stdout?.on("data", (c) => {
46536
46814
  stdoutBytes += c.length;
46537
46815
  if (head2.length < 2048) head2 += c.toString("utf8").slice(0, 2048 - head2.length);
46816
+ tail = (tail + c.toString("utf8")).slice(-4096);
46538
46817
  process.stdout.write(c);
46539
46818
  });
46540
46819
  child.stderr?.on("data", (c) => process.stderr.write(c));
@@ -46546,11 +46825,23 @@ async function invokeHarness(ctx, identity, hit, worktree) {
46546
46825
  child.on("error", (e) => {
46547
46826
  clearTimeout(timer);
46548
46827
  log(` harness failed to start: ${e.message}`);
46549
- resolve2({ code: 127, stdoutBytes, refusal: refusalIn(head2) });
46828
+ resolve2({
46829
+ code: 127,
46830
+ stdoutBytes,
46831
+ refusal: refusalIn(head2),
46832
+ durationMs: Date.now() - startedAtMs,
46833
+ usage: null
46834
+ });
46550
46835
  });
46551
46836
  child.on("close", (code) => {
46552
46837
  clearTimeout(timer);
46553
- resolve2({ code: code ?? 1, stdoutBytes, refusal: refusalIn(head2) });
46838
+ resolve2({
46839
+ code: code ?? 1,
46840
+ stdoutBytes,
46841
+ refusal: refusalIn(head2),
46842
+ durationMs: Date.now() - startedAtMs,
46843
+ usage: usageIn(tail)
46844
+ });
46554
46845
  });
46555
46846
  });
46556
46847
  } finally {
@@ -46633,7 +46924,29 @@ async function beat(ctx, fields) {
46633
46924
  );
46634
46925
  if (error51) log(` could not record a heartbeat: ${error51.message}`);
46635
46926
  }
46636
- async function tick(ctx, identity, deadTicks) {
46927
+ async function recordRun(ctx, hit, run, outcome) {
46928
+ const { error: error51 } = await ctx.client.from("harness_runs").insert({
46929
+ agent_id: ctx.userId,
46930
+ workspace_id: ctx.workspaceId,
46931
+ issue_id: hit.issue.id,
46932
+ reason: hit.reason,
46933
+ duration_ms: run.durationMs,
46934
+ exit_code: run.code,
46935
+ stdout_bytes: run.stdoutBytes,
46936
+ outcome,
46937
+ model: run.usage?.model ?? null,
46938
+ input_tokens: run.usage?.inputTokens ?? null,
46939
+ output_tokens: run.usage?.outputTokens ?? null,
46940
+ cache_read_tokens: run.usage?.cacheReadTokens ?? null,
46941
+ cache_write_tokens: run.usage?.cacheWriteTokens ?? null,
46942
+ cost_usd: run.usage?.costUsd ?? null
46943
+ });
46944
+ if (error51 && !warnedAboutSpendTable) {
46945
+ warnedAboutSpendTable = true;
46946
+ log(` (not recording run costs: ${error51.message})`);
46947
+ }
46948
+ }
46949
+ async function tick(ctx, identity, deadTicks, allowance) {
46637
46950
  const mentions = await pollMentions(ctx, identity);
46638
46951
  const [assigned, stateWork] = await Promise.all([pollAssigned(ctx), poll(ctx, identity)]);
46639
46952
  const seen = /* @__PURE__ */ new Set();
@@ -46654,7 +46967,20 @@ async function tick(ctx, identity, deadTicks) {
46654
46967
  if (free.length > 0) log(`${free.length} issue(s) waiting, all set aside`);
46655
46968
  else if (heldByOthers.size > 0) log(`nothing free \u2014 everything waiting is claimed`);
46656
46969
  await beat(ctx, { holding: 0, deadTicks, intervalMs: options.intervalMs });
46657
- return { invoked: 0, failedToRun: 0 };
46970
+ return { invoked: 0, failedToRun: 0, suppressed: false, waiting: 0 };
46971
+ }
46972
+ if (allowance === "held") {
46973
+ await beat(ctx, {
46974
+ holding: actionable.length,
46975
+ deadTicks,
46976
+ intervalMs: options.intervalMs
46977
+ });
46978
+ return {
46979
+ invoked: 0,
46980
+ failedToRun: 0,
46981
+ suppressed: true,
46982
+ waiting: actionable.length
46983
+ };
46658
46984
  }
46659
46985
  await beat(ctx, {
46660
46986
  holding: actionable.length,
@@ -46667,9 +46993,12 @@ async function tick(ctx, identity, deadTicks) {
46667
46993
  log(
46668
46994
  `${actionable.length} issue(s) for "${identity.jobFunction}"` + (mentionCount > 0 ? ` \u2014 ${mentionCount} from unread mentions` : "")
46669
46995
  );
46670
- const taking = options.limit ? actionable.slice(0, options.limit) : actionable;
46996
+ const cap = allowance === "probe" ? 1 : options.limit;
46997
+ const taking = cap ? actionable.slice(0, cap) : actionable;
46671
46998
  if (taking.length < actionable.length) {
46672
- log(` taking ${taking.length} this tick (--limit ${options.limit})`);
46999
+ log(
47000
+ allowance === "probe" ? ` probing with ${taking.length} of ${actionable.length} \u2014 one card is enough to learn whether the harness works again` : ` taking ${taking.length} this tick (--limit ${options.limit})`
47001
+ );
46673
47002
  }
46674
47003
  for (const hit of taking) {
46675
47004
  const why = hit.reason === "mention" ? "mentioned" : hit.reason === "assigned" ? `assigned \xB7 ${hit.state.name}` : hit.state.name;
@@ -46709,6 +47038,7 @@ async function tick(ctx, identity, deadTicks) {
46709
47038
  const code = run.code;
46710
47039
  if (neverRan(run)) {
46711
47040
  failedToRun += 1;
47041
+ await recordRun(ctx, hit, run, "never-ran");
46712
47042
  if (run.refusal) {
46713
47043
  if (run.refusal !== lastRefusal) {
46714
47044
  log(`! the harness is refusing to work: ${run.refusal}`);
@@ -46725,6 +47055,7 @@ async function tick(ctx, identity, deadTicks) {
46725
47055
  const { data: after } = await ctx.client.from("issues").select("status_id, assignee_id").eq("id", hit.issue.id).maybeSingle();
46726
47056
  const moved = !after || after.status_id !== before.status || after.assignee_id !== before.assignee;
46727
47057
  const productive = hit.reason === "mention" ? await repliedSince(ctx, hit, startedAt) || moved : moved || committed > 0;
47058
+ await recordRun(ctx, hit, run, productive ? "productive" : "unproductive");
46728
47059
  if (productive) {
46729
47060
  await clearAttempts(ctx, hit);
46730
47061
  if (hit.reason === "mention") {
@@ -46740,9 +47071,9 @@ async function tick(ctx, identity, deadTicks) {
46740
47071
  }
46741
47072
  } else {
46742
47073
  const what = hit.reason === "mention" ? "no reply written" : "unchanged";
46743
- const n = await recordAttempt(ctx, hit, `${what} (exit ${code})`, attemptsFor(hit));
47074
+ const n2 = await recordAttempt(ctx, hit, `${what} (exit ${code})`, attemptsFor(hit));
46744
47075
  log(
46745
- ` ${hit.ref} ${what} after the run (exit ${code}) \u2014 attempt ${n}/${options.maxAttempts}` + (n >= options.maxAttempts ? "; setting it aside, a human should look" : "")
47076
+ ` ${hit.ref} ${what} after the run (exit ${code}) \u2014 attempt ${n2}/${options.maxAttempts}` + (n2 >= options.maxAttempts ? "; setting it aside, a human should look" : "")
46746
47077
  );
46747
47078
  }
46748
47079
  if (worktree) {
@@ -46767,7 +47098,7 @@ async function tick(ctx, identity, deadTicks) {
46767
47098
  deadTicks: allDead ? deadTicks + 1 : 0,
46768
47099
  intervalMs: options.intervalMs
46769
47100
  });
46770
- return { invoked, failedToRun };
47101
+ return { invoked, failedToRun, suppressed: false, waiting: actionable.length };
46771
47102
  }
46772
47103
  async function main() {
46773
47104
  log(
@@ -46780,7 +47111,7 @@ async function main() {
46780
47111
  if (staleConfigs > 0) {
46781
47112
  log(` cleared ${staleConfigs} temp config(s) left by a killed run`);
46782
47113
  }
46783
- const ctx = await getContext();
47114
+ const ctx = await contextOrExplain();
46784
47115
  const identity = await identify(ctx);
46785
47116
  try {
46786
47117
  await preflight();
@@ -46811,6 +47142,7 @@ async function main() {
46811
47142
  const swept = await sweep(worktreeConfig, options.harnessTimeoutMs + options.prepareTimeout);
46812
47143
  if (swept.length) log(` swept ${swept.length} worktree(s) left by an earlier run: ${swept.join(", ")}`);
46813
47144
  if (options.push) log(` publishing to ${options.pushRemote} after any run that commits`);
47145
+ else log(` --push is off: branches stay on this machine and reach no remote`);
46814
47146
  if (options.allow === "mcp__cruo") {
46815
47147
  log(`! --worktree with the default --allow gives the harness a checkout it cannot read.`);
46816
47148
  log(` Widen it, e.g. --allow "mcp__cruo,Read,Glob,Grep,Edit,Write,Bash(git:*),Bash(pnpm:*)"`);
@@ -46826,24 +47158,45 @@ async function main() {
46826
47158
  log(
46827
47159
  options.once ? "single pass" : `polling every ${options.intervalMs / 1e3}s \xB7 ctrl-c to stop`
46828
47160
  );
46829
- let deadTicks = 0;
47161
+ let pacing = freshPacing();
47162
+ let lastHeldLog = 0;
46830
47163
  for (; ; ) {
46831
47164
  try {
46832
- const result = await tick(await getContext(), identity, deadTicks);
46833
- const allDead = result.invoked > 0 && result.failedToRun === result.invoked;
46834
- deadTicks = allDead ? deadTicks + 1 : 0;
47165
+ const result = await tick(
47166
+ await getContext(),
47167
+ identity,
47168
+ pacing.deadTicks,
47169
+ allowanceAt(pacing, Date.now())
47170
+ );
47171
+ if (result.suppressed) {
47172
+ const now = Date.now();
47173
+ if (now - lastHeldLog >= 6e4) {
47174
+ lastHeldLog = now;
47175
+ const left = Math.max(0, Math.round((pacing.invokeAfter - now) / 1e3));
47176
+ log(
47177
+ `! ${result.waiting} issue(s) waiting \u2014 not invoking after ${pacing.deadTicks} dead ticks; next attempt in ${left}s`
47178
+ );
47179
+ }
47180
+ } else {
47181
+ const step = afterAttempt(pacing, result, options, Date.now());
47182
+ pacing = step.next;
47183
+ if (step.event === "backing-off") {
47184
+ lastHeldLog = Date.now();
47185
+ log(
47186
+ ` backing off \u2014 ${pacing.deadTicks} ticks with nothing reaching a model; still polling every ${options.intervalMs / 1e3}s, next invocation in ${step.cooldownMs / 1e3}s`
47187
+ );
47188
+ } else if (step.event === "recovered") {
47189
+ log(` recovered \u2014 a run reached a model; invoking normally again`);
47190
+ }
47191
+ }
46835
47192
  } catch (e) {
46836
47193
  log(`poll error: ${e instanceof Error ? e.message : String(e)}`);
46837
47194
  }
46838
47195
  if (options.once) return;
46839
- const wait = deadTicks < 2 ? options.intervalMs : Math.min(options.intervalMs * 2 ** (deadTicks - 1), options.maxIntervalMs);
46840
- if (wait !== options.intervalMs) {
46841
- log(` backing off \u2014 ${deadTicks} ticks with nothing reaching a model; next in ${wait / 1e3}s`);
46842
- }
46843
- await new Promise((r) => setTimeout(r, wait));
47196
+ await new Promise((r) => setTimeout(r, options.intervalMs));
46844
47197
  }
46845
47198
  }
46846
- var argv, flag, opt, num, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV;
47199
+ var argv, flag, opt, num2, ms, readOptions, options, log, worktreeConfig, lastRefusal, PRIORITY_RANK, CONFIG_DIR_PREFIX, CRUO_OWNED_ENV, warnedAboutSpendTable;
46847
47200
  var init_supervisor = __esm({
46848
47201
  "src/supervisor.ts"() {
46849
47202
  "use strict";
@@ -46853,11 +47206,12 @@ var init_supervisor = __esm({
46853
47206
  init_auth();
46854
47207
  init_options();
46855
47208
  init_harness_signal();
47209
+ init_pacing();
46856
47210
  init_worktree();
46857
47211
  argv = process.argv.slice(2);
46858
47212
  flag = (name) => flagIn(argv, name);
46859
47213
  opt = (name, fallback) => optIn(argv, name, fallback);
46860
- num = (name, spec) => numericIn(argv, name, spec);
47214
+ num2 = (name, spec) => numericIn(argv, name, spec);
46861
47215
  ms = (name, fallbackSeconds) => secondsIn(argv, name, fallbackSeconds);
46862
47216
  readOptions = () => ({
46863
47217
  /** Override the function to poll for. Defaults to this agent's own. */
@@ -46873,7 +47227,7 @@ var init_supervisor = __esm({
46873
47227
  * moment they have the least reason to trust three. Unset means all of them,
46874
47228
  * which is the right steady-state behaviour.
46875
47229
  */
46876
- limit: num("limit", { integer: true }),
47230
+ limit: num2("limit", { integer: true }),
46877
47231
  intervalMs: ms("interval", 20),
46878
47232
  once: flag("once"),
46879
47233
  dryRun: flag("dry-run"),
@@ -46903,11 +47257,15 @@ var init_supervisor = __esm({
46903
47257
  * `pm.issue_attempts` since CRA-7 — it was in-memory, which meant a restart
46904
47258
  * forgot every set-aside and nothing outside the process could see one.
46905
47259
  */
46906
- maxAttempts: num("max-attempts", { fallback: 3, integer: true }),
47260
+ maxAttempts: num2("max-attempts", { fallback: 3, integer: true }),
46907
47261
  /**
46908
- * Ceiling for the backoff below. Ten minutes: long enough that a broken
46909
- * supervisor stops being a wall of identical lines, short enough that fixing
46910
- * the cause is noticed without restarting anything.
47262
+ * Ceiling on how long invocation is held back after a run of dead ticks.
47263
+ * Ten minutes: long enough that a broken harness stops being spawned over and
47264
+ * over, short enough that fixing the cause is noticed without restarting
47265
+ * anything.
47266
+ *
47267
+ * It is not a poll ceiling. Polling stays at `--interval` throughout, so this
47268
+ * bounds spend, not how quickly a recovery is seen (`CRA-87`).
46911
47269
  */
46912
47270
  maxIntervalMs: ms("max-interval", 600),
46913
47271
  /**
@@ -46992,6 +47350,7 @@ cruo-supervisor: ${error51.message}
46992
47350
  // stands in for. Cheap to list, and wrong to discover the hard way.
46993
47351
  "CRUO_SESSION"
46994
47352
  ];
47353
+ warnedAboutSpendTable = false;
46995
47354
  main().catch((e) => {
46996
47355
  console.error(e instanceof Error ? e.message : e);
46997
47356
  process.exit(1);
@@ -47000,17 +47359,26 @@ cruo-supervisor: ${error51.message}
47000
47359
  });
47001
47360
 
47002
47361
  // src/cli.ts
47362
+ init_dist();
47363
+ init_dist7();
47003
47364
  init_options();
47004
47365
  import { chmod, mkdir as mkdir2, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
47005
47366
  import { homedir as homedir2 } from "node:os";
47006
47367
  import { join as join3 } from "node:path";
47007
47368
  var CONFIG_DIR = join3(homedir2(), ".cruo");
47008
47369
  var CONFIG_PATH = join3(CONFIG_DIR, "config.json");
47370
+ function nameFromEmail(email3) {
47371
+ const local = email3.split("@")[0]?.toLowerCase().replace(/[^a-z0-9._-]/g, "");
47372
+ return local && local.length > 0 ? local : null;
47373
+ }
47009
47374
  var USAGE = `cruo \u2014 run a Cruo agent
47010
47375
 
47011
47376
  cruo login <token> remember an agent's token on this machine
47012
- cruo logout forget it
47377
+ cruo agents list what is remembered (never the tokens)
47378
+ cruo usage [--days 7] what the agents have been spending
47379
+ cruo logout [name] forget one, or --all
47013
47380
  cruo [options] run the agent's supervisor
47381
+ cruo --as <name> \u2026 run a particular one
47014
47382
 
47015
47383
  Getting a token
47016
47384
  Cruo \u2192 Settings \u2192 Members \u2192 Add an agent. It is shown once.
@@ -47026,52 +47394,188 @@ Common options
47026
47394
 
47027
47395
  Full list and what each one is for: https://cruo.space/docs#agents
47028
47396
 
47397
+ More than one agent on one machine: \`login\` files each token under the name the
47398
+ board shows, so the chain is \`cruo --as jarvis\` and \`cruo --as luna\` rather than
47399
+ a CRUO_TOKEN export per terminal.
47400
+
47029
47401
  The token comes from CRUO_TOKEN, then --token, then whatever \`login\` stored.
47030
47402
  `;
47031
- async function storedToken() {
47403
+ var EMPTY = { version: 2, default: null, agents: {} };
47404
+ async function readConfig2() {
47405
+ let parsed;
47406
+ try {
47407
+ parsed = JSON.parse(await readFile2(CONFIG_PATH, "utf8"));
47408
+ } catch {
47409
+ return { ...EMPTY };
47410
+ }
47411
+ const raw = parsed;
47412
+ if (raw.agents && typeof raw.agents === "object") {
47413
+ return {
47414
+ version: 2,
47415
+ default: typeof raw.default === "string" ? raw.default : null,
47416
+ agents: raw.agents
47417
+ };
47418
+ }
47419
+ const legacy = raw.token?.trim();
47420
+ if (legacy) {
47421
+ return { version: 2, default: "default", agents: { default: { token: legacy } } };
47422
+ }
47423
+ return { ...EMPTY };
47424
+ }
47425
+ async function writeConfig(config3) {
47426
+ await mkdir2(CONFIG_DIR, { recursive: true, mode: 448 });
47427
+ await writeFile2(CONFIG_PATH, JSON.stringify(config3, null, 2) + "\n", { mode: 384 });
47428
+ await chmod(CONFIG_PATH, 384);
47429
+ }
47430
+ async function identify2(token) {
47431
+ const endpoint = process.env.CRUO_SESSION_URL?.trim() || CRUO_CLOUD.sessionUrl;
47032
47432
  try {
47033
- const parsed = JSON.parse(await readFile2(CONFIG_PATH, "utf8"));
47034
- return parsed.token?.trim() || null;
47433
+ const session = await fetchDelegatedSession(endpoint, token);
47434
+ return { email: session.email, workspace: session.workspaceName };
47035
47435
  } catch {
47036
47436
  return null;
47037
47437
  }
47038
47438
  }
47039
- async function login(token) {
47439
+ async function login(token, as) {
47040
47440
  if (!token.startsWith("cruo_pat_")) {
47041
47441
  console.error(`
47042
47442
  That does not look like a Cruo token \u2014 they begin "cruo_pat_".
47043
47443
  `);
47044
47444
  process.exit(2);
47045
47445
  }
47046
- await mkdir2(CONFIG_DIR, { recursive: true, mode: 448 });
47047
- await writeFile2(CONFIG_PATH, JSON.stringify({ token }, null, 2) + "\n", { mode: 384 });
47048
- await chmod(CONFIG_PATH, 384);
47049
- console.log(`
47050
- Saved to ${CONFIG_PATH} (readable only by you).
47446
+ const who = as ? null : await identify2(token);
47447
+ const name = as ?? (who ? nameFromEmail(who.email) : null);
47448
+ if (!name) {
47449
+ console.error(
47450
+ `
47451
+ Could not work out whose token that is, so it needs a name:
47051
47452
 
47052
- Run \`cruo --dry-run\` to see what it would pick up.
47453
+ cruo login <token> --as jarvis
47454
+
47455
+ That happens when the board cannot be reached \u2014 a local stack, or no
47456
+ network. The token is fine; only the lookup failed.
47457
+ `
47458
+ );
47459
+ process.exit(2);
47460
+ }
47461
+ const config3 = await readConfig2();
47462
+ const replacing = config3.agents[name];
47463
+ config3.agents[name] = {
47464
+ token,
47465
+ email: who?.email,
47466
+ workspace: who?.workspace,
47467
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
47468
+ };
47469
+ config3.default ??= name;
47470
+ await writeConfig(config3);
47471
+ const where = who ? ` (${who.email} in ${who.workspace})` : "";
47472
+ console.log(
47473
+ `
47474
+ ${replacing ? "Replaced" : "Saved"} the token for \`${name}\`${where}.
47475
+ ${CONFIG_PATH} is readable only by you.
47476
+
47477
+ ` + (config3.default === name ? `Run \`cruo --dry-run\` to see what it would pick up.
47478
+ ` : `Run \`cruo --as ${name} --dry-run\` to see what it would pick up.
47479
+ A bare \`cruo\` still runs \`${config3.default}\`.
47480
+ `)
47481
+ );
47482
+ }
47483
+ async function logout(which, all) {
47484
+ const config3 = await readConfig2();
47485
+ const names = Object.keys(config3.agents);
47486
+ if (all) {
47487
+ await rm3(CONFIG_PATH, { force: true });
47488
+ console.log(
47489
+ names.length === 0 ? `
47490
+ Nothing was stored.
47491
+ ` : `
47492
+ Forgot ${names.length} token(s): ${names.join(", ")}. ${CONFIG_PATH} is gone.
47493
+ `
47494
+ );
47495
+ return;
47496
+ }
47497
+ if (!which) {
47498
+ if (names.length <= 1) {
47499
+ await rm3(CONFIG_PATH, { force: true });
47500
+ console.log(
47501
+ names.length === 0 ? `
47502
+ Nothing was stored.
47503
+ ` : `
47504
+ Forgot the token for \`${names[0]}\`. ${CONFIG_PATH} is gone.
47505
+ `
47506
+ );
47507
+ return;
47508
+ }
47509
+ console.error(
47510
+ `
47511
+ ${names.length} tokens are stored: ${names.join(", ")}
47512
+
47513
+ Say which: cruo logout <name>
47514
+ Or all: cruo logout --all
47515
+ `
47516
+ );
47517
+ process.exit(2);
47518
+ }
47519
+ if (!config3.agents[which]) {
47520
+ console.error(`
47521
+ Nothing stored for \`${which}\`. Stored: ${names.join(", ") || "(none)"}
47053
47522
  `);
47523
+ process.exit(2);
47524
+ }
47525
+ delete config3.agents[which];
47526
+ if (config3.default === which) config3.default = Object.keys(config3.agents)[0] ?? null;
47527
+ await writeConfig(config3);
47528
+ console.log(
47529
+ `
47530
+ Forgot the token for \`${which}\`.` + (config3.default ? ` A bare \`cruo\` now runs \`${config3.default}\`.
47531
+ ` : ` Nothing is stored now.
47532
+ `)
47533
+ );
47054
47534
  }
47055
- async function logout() {
47056
- await rm3(CONFIG_PATH, { force: true });
47535
+ async function listAgents() {
47536
+ const config3 = await readConfig2();
47537
+ const names = Object.keys(config3.agents);
47538
+ if (names.length === 0) {
47539
+ console.log(`
47540
+ No tokens stored. \`cruo login <token>\` to add one.
47541
+ `);
47542
+ return;
47543
+ }
47544
+ console.log("");
47545
+ for (const name of names) {
47546
+ const a = config3.agents[name];
47547
+ const mark = config3.default === name ? "*" : " ";
47548
+ const who = a.email ? ` \u2014 ${a.email}${a.workspace ? ` in ${a.workspace}` : ""}` : "";
47549
+ console.log(` ${mark} ${name}${who}`);
47550
+ }
47057
47551
  console.log(`
47058
- Forgotten. ${CONFIG_PATH} is gone.
47552
+ * is what a bare \`cruo\` runs. Use \`--as <name>\` for another.
47059
47553
  `);
47060
47554
  }
47061
47555
  async function main2() {
47062
47556
  const argv2 = process.argv.slice(2);
47063
47557
  const [first] = argv2;
47558
+ const flagValue = (name) => {
47559
+ const i = argv2.indexOf(name);
47560
+ return i >= 0 ? argv2[i + 1] ?? null : null;
47561
+ };
47064
47562
  if (first === "login") {
47065
47563
  const token2 = argv2[1];
47066
- if (!token2) {
47564
+ if (!token2 || token2.startsWith("--")) {
47067
47565
  console.error(`
47068
- Usage: cruo login <token>
47566
+ Usage: cruo login <token> [--as <name>]
47069
47567
  `);
47070
47568
  process.exit(2);
47071
47569
  }
47072
- return login(token2);
47570
+ return login(token2, flagValue("--as"));
47571
+ }
47572
+ if (first === "logout") {
47573
+ const named = argv2[1] && !argv2[1].startsWith("--") ? argv2[1] : null;
47574
+ return logout(named, argv2.includes("--all"));
47575
+ }
47576
+ if (first === "agents" || first === "whoami") return listAgents();
47577
+ if (first === "usage") {
47073
47578
  }
47074
- if (first === "logout") return logout();
47075
47579
  if (first === "help" || argv2.includes("--help") || argv2.includes("-h")) {
47076
47580
  console.log(USAGE);
47077
47581
  return;
@@ -47084,7 +47588,22 @@ Usage: cruo login <token>
47084
47588
  Prefer CRUO_TOKEN in the environment, or \`cruo login\` once.`
47085
47589
  );
47086
47590
  }
47087
- const token = process.env.CRUO_TOKEN?.trim() || fromFlag || await storedToken();
47591
+ const asName = flagValue("--as");
47592
+ const config3 = await readConfig2();
47593
+ const stored = asName ? config3.agents[asName] : config3.default ? config3.agents[config3.default] : void 0;
47594
+ if (asName && !stored) {
47595
+ const names = Object.keys(config3.agents);
47596
+ console.error(
47597
+ `
47598
+ Nothing stored for \`${asName}\`.
47599
+
47600
+ ` + (names.length > 0 ? `Stored: ${names.join(", ")}
47601
+ ` : `Nothing is stored at all \u2014 \`cruo login <token>\` first.
47602
+ `)
47603
+ );
47604
+ process.exit(2);
47605
+ }
47606
+ const token = process.env.CRUO_TOKEN?.trim() || fromFlag || stored?.token;
47088
47607
  if (!token) {
47089
47608
  console.error(
47090
47609
  `
@@ -47095,9 +47614,24 @@ Get one from Cruo \u2192 Settings \u2192 Members \u2192 Add an agent.
47095
47614
  );
47096
47615
  process.exit(2);
47097
47616
  }
47617
+ const usingStored = !process.env.CRUO_TOKEN?.trim() && !fromFlag && stored;
47618
+ if (usingStored) {
47619
+ process.env.CRUO_IDENTITY = asName ?? config3.default ?? "";
47620
+ }
47098
47621
  process.env.CRUO_TOKEN = token;
47099
- const passthrough = stripTokenFlag(argv2);
47622
+ const passthrough = stripTokenFlag(argv2).filter((a, i, all) => a !== "--as" && all[i - 1] !== "--as");
47100
47623
  process.argv = [process.argv[0], process.argv[1], ...passthrough];
47624
+ if (first === "usage") {
47625
+ const { reportUsage: reportUsage2 } = await Promise.resolve().then(() => (init_usage(), usage_exports));
47626
+ const days = Number(flagValue("--days") ?? 7);
47627
+ if (!Number.isFinite(days) || days <= 0) {
47628
+ console.error(`
47629
+ --days must be a positive number.
47630
+ `);
47631
+ process.exit(2);
47632
+ }
47633
+ return reportUsage2({ days, issue: flagValue("--issue") });
47634
+ }
47101
47635
  await Promise.resolve().then(() => (init_supervisor(), supervisor_exports));
47102
47636
  }
47103
47637
  main2().catch((error51) => {