@tablation/crew 0.78.0 → 0.78.1

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/cli.js +77 -15
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -230,6 +230,13 @@ function extractTurnTokens(line) {
230
230
  if (!usage2) return void 0;
231
231
  return (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
232
232
  }
233
+ function extractPrefixTokens(line) {
234
+ const obj = line;
235
+ if (!obj || obj.type !== "assistant") return void 0;
236
+ const usage2 = obj.message?.usage;
237
+ if (!usage2) return void 0;
238
+ return (usage2.input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
239
+ }
233
240
  function extractResult(line) {
234
241
  const obj = line;
235
242
  if (!obj || obj.type !== "result") return void 0;
@@ -580,6 +587,7 @@ var claudeAdapter = {
580
587
  mapStreamLine,
581
588
  extractResult,
582
589
  extractTurnTokens,
590
+ extractPrefixTokens,
583
591
  inspectMcp(o) {
584
592
  const s = inspectClaudeMcp(o);
585
593
  return s.state === "no-claude" ? { state: "no-binary" } : s;
@@ -5114,6 +5122,8 @@ async function reportAgentRun(target, run2) {
5114
5122
  cacheReadTokens: run2.cacheReadTokens,
5115
5123
  cacheWriteTokens: run2.cacheWriteTokens,
5116
5124
  costUsd: run2.costUsd,
5125
+ numTurns: run2.numTurns,
5126
+ prefixTokens: run2.prefixTokens,
5117
5127
  promptVersion: run2.promptVersion,
5118
5128
  promptSha: run2.promptSha
5119
5129
  };
@@ -5271,24 +5281,31 @@ function ticketMentions(text) {
5271
5281
  }
5272
5282
  var TicketAttributionTracker = class {
5273
5283
  #tokensByTicket = /* @__PURE__ */ new Map();
5284
+ #turns = 0;
5274
5285
  #current;
5275
5286
  /** Tracker record uuid → ticket key, learned when a write names both (a claim resolves the key from the last mention). */
5276
5287
  #keyByRecord = /* @__PURE__ */ new Map();
5277
5288
  #lastBy = {};
5278
- /** The ticket key most recently named in the transcript, or undefined before any. */
5289
+ /**
5290
+ * The ticket key most recently named in the transcript, or undefined before any.
5291
+ * Only the FIRST key in a turn counts: a closing summary names its own
5292
+ * ticket first and then lists others ("Other tickets", "Blocked by", "See
5293
+ * also"), and those trailing keys are not what the run worked (CREW-2598).
5294
+ */
5279
5295
  get current() {
5280
5296
  return this.#current;
5281
5297
  }
5282
5298
  /** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
5283
5299
  note(text) {
5284
5300
  const mentions = ticketMentions(text);
5285
- if (mentions.length > 0) this.#current = mentions[mentions.length - 1];
5301
+ if (mentions.length > 0) this.#current = mentions[0];
5286
5302
  }
5287
5303
  /** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
5288
5304
  add(text, tokens) {
5289
5305
  const mentions = ticketMentions(text);
5290
- if (mentions.length > 0) this.#current = mentions[mentions.length - 1];
5306
+ if (mentions.length > 0) this.#current = mentions[0];
5291
5307
  if (this.#current === void 0 || tokens <= 0) return;
5308
+ this.#turns++;
5292
5309
  this.#tokensByTicket.set(this.#current, (this.#tokensByTicket.get(this.#current) ?? 0) + tokens);
5293
5310
  }
5294
5311
  /**
@@ -5310,16 +5327,31 @@ var TicketAttributionTracker = class {
5310
5327
  /**
5311
5328
  * How the attribution is decided: a hand-off write beats a claim, a claim
5312
5329
  * beats a comment (so a follow-up ticket filed or commented on while
5313
- * working never displaces the claimed one), and any write beats the
5314
- * mention-token heuristic, which is only the fallback for runs that made
5315
- * no ticket-targeted write. The pre-run hint is last.
5330
+ * working never displaces the claimed one), and any write beats everything
5331
+ * below. Next, `hintMoved` — the post-run read-back showed the hinted
5332
+ * ticket changed status or assignee during the run — is a hand-off signal
5333
+ * for a run that wrote through MCP tools crew cannot see. Then the
5334
+ * mention-token heuristic, which with a hint may override it only when a
5335
+ * different ticket holds a strict majority of the credited tokens (a single
5336
+ * credited turn, like a lone end-of-run summary naming another ticket,
5337
+ * must not). A run with no hint
5338
+ * keeps the plain winner. The pre-run hint is last.
5316
5339
  */
5317
- decide(hint) {
5340
+ decide(hint, opts = {}) {
5318
5341
  for (const by of ["handoff", "claim", "comment"]) {
5319
5342
  const ticket = this.#lastBy[by];
5320
5343
  if (ticket) return { ticket, by };
5321
5344
  }
5322
- if (this.#tokensByTicket.size > 0) return { ticket: this.winner(hint), by: "mentions" };
5345
+ if (hint && opts.hintMoved) return { ticket: hint, by: "readback" };
5346
+ if (this.#tokensByTicket.size > 0) {
5347
+ if (!hint) return { ticket: this.winner(), by: "mentions" };
5348
+ const winner = this.winner(hint);
5349
+ if (winner === hint) return { ticket: hint, by: "mentions" };
5350
+ let total = 0;
5351
+ for (const n of this.#tokensByTicket.values()) total += n;
5352
+ const share = (this.#tokensByTicket.get(winner) ?? 0) / total;
5353
+ return share > 0.5 && this.#turns > 1 ? { ticket: winner, by: "mentions" } : { ticket: hint, by: "hint" };
5354
+ }
5323
5355
  return { ticket: hint, by: "hint" };
5324
5356
  }
5325
5357
  /**
@@ -8467,6 +8499,8 @@ var RunObserver = class {
8467
8499
  /** Hand-off writes and their outcomes, to tell a run whose hand-off never landed (CREW-2363). */
8468
8500
  handoff = new HandoffTranscriptTracker();
8469
8501
  result;
8502
+ /** Prefix tokens of the first assistant call that carried usage; kept once. */
8503
+ prefixTokens;
8470
8504
  /** Set once the stream carries the harness's final result (turn count or cost). */
8471
8505
  sawResult = false;
8472
8506
  muted = false;
@@ -8593,6 +8627,7 @@ var RunObserver = class {
8593
8627
  }
8594
8628
  }
8595
8629
  }
8630
+ if (this.prefixTokens === void 0 && parsed !== void 0) this.prefixTokens = this.adapter.extractPrefixTokens?.(parsed);
8596
8631
  const turnTokens = parsed === void 0 ? void 0 : this.adapter.extractTurnTokens(parsed);
8597
8632
  if (turnTokens !== void 0) this.attribution.add(turnText, turnTokens);
8598
8633
  }
@@ -8616,8 +8651,15 @@ function writeRunMeta(id, startedAt, liveTicket, extra) {
8616
8651
  } catch {
8617
8652
  }
8618
8653
  }
8619
- async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8620
- const { ticket: attributedTicket, by: attributedBy } = obs.attribution.decide(obs.liveTicket);
8654
+ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved) {
8655
+ let moved = false;
8656
+ if (hintMoved) {
8657
+ try {
8658
+ moved = await hintMoved();
8659
+ } catch {
8660
+ }
8661
+ }
8662
+ const { ticket: attributedTicket, by: attributedBy } = obs.attribution.decide(obs.liveTicket, { hintMoved: moved });
8621
8663
  const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
8622
8664
  const { result } = obs;
8623
8665
  const pending = end.interrupted ? null : obs.handoff.pending();
@@ -8649,6 +8691,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8649
8691
  ...pending ? { handoff: "pending" } : {},
8650
8692
  sessionId: result?.sessionId ?? null,
8651
8693
  numTurns: result?.numTurns ?? null,
8694
+ prefixTokens: obs.prefixTokens ?? null,
8652
8695
  totalCostUsd: result?.costUsd ?? null
8653
8696
  });
8654
8697
  } catch {
@@ -8661,6 +8704,8 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8661
8704
  ...pending ? { handoff: "pending" } : {},
8662
8705
  ...end.interrupted ? { interrupted: end.interrupted } : {},
8663
8706
  ...identity,
8707
+ attributedTicket: attributedTicket ?? null,
8708
+ attributedBy,
8664
8709
  ...result?.sessionId ? { sessionId: result.sessionId } : {},
8665
8710
  ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
8666
8711
  ...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
@@ -8679,6 +8724,9 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8679
8724
  cacheReadTokens: result?.cacheReadTokens,
8680
8725
  cacheWriteTokens: result?.cacheWriteTokens,
8681
8726
  costUsd: result?.costUsd,
8727
+ // Both come from a finished run: without the result line (interrupted) neither is trustworthy.
8728
+ ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
8729
+ ...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
8682
8730
  harness: identity.harness,
8683
8731
  provider: identity.provider,
8684
8732
  promptVersion: id.promptVersion,
@@ -8723,7 +8771,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8723
8771
  }
8724
8772
  return { ms: ms2, ticket: attributedTicket, handoffPending: !!pending };
8725
8773
  }
8726
- function spawnAgent(plan, emit) {
8774
+ function spawnAgent(plan, emit, hintMoved) {
8727
8775
  const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
8728
8776
  const started = Date.now();
8729
8777
  const startedAt = new Date(started).toISOString();
@@ -8788,7 +8836,7 @@ function spawnAgent(plan, emit) {
8788
8836
  obs.end();
8789
8837
  await endSink(eventsSink);
8790
8838
  if (spawnFailed) return;
8791
- const { ms: ms2, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey);
8839
+ const { ms: ms2, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey, hintMoved);
8792
8840
  resolve6({ code: code ?? 1, ms: ms2, ...handoffPending ? { handoffPending } : {} });
8793
8841
  });
8794
8842
  child.stdin?.on("error", (err) => {
@@ -16914,7 +16962,19 @@ async function runRoleAgent(role, decision, ctx) {
16914
16962
  }
16915
16963
  try {
16916
16964
  const runStart = (/* @__PURE__ */ new Date()).toISOString();
16917
- await spawnAgent(plan, emit);
16965
+ let hintMoved;
16966
+ if (workingId) {
16967
+ try {
16968
+ const before = await tracker.ticket(workingId);
16969
+ const col = tracker.contract.columns;
16970
+ hintMoved = async () => {
16971
+ const after = await tracker.ticket(workingId);
16972
+ return after[col.status] !== before[col.status] || after[col.assignee] !== before[col.assignee];
16973
+ };
16974
+ } catch {
16975
+ }
16976
+ }
16977
+ await spawnAgent(plan, emit, hintMoved);
16918
16978
  await guardRunHandoff(route, ship, tracker, role, workingId, emit, runStart);
16919
16979
  } finally {
16920
16980
  if (memberId) {
@@ -19383,7 +19443,7 @@ ${changed2.map((c) => ` - ${c}
19383
19443
  }
19384
19444
  for (const e of entries) {
19385
19445
  process.stdout.write(
19386
- `${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}
19446
+ `${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}${e.num_turns != null ? ` turns ${e.num_turns}` : ""}${e.prefix_tokens != null ? ` prefix ${e.prefix_tokens}` : ""}
19387
19447
  `
19388
19448
  );
19389
19449
  }
@@ -19401,7 +19461,9 @@ ticket: ${entry.ticket_reference ?? "(none)"}
19401
19461
  model: ${entry.model ?? "?"}
19402
19462
  harness: ${entry.harness ?? "?"}
19403
19463
  provider: ${entry.provider ?? "?"}
19404
- `
19464
+ ` + (entry.num_turns != null ? `turns: ${entry.num_turns}
19465
+ ` : "") + (entry.prefix_tokens != null ? `prefix: ${entry.prefix_tokens} tokens
19466
+ ` : "")
19405
19467
  );
19406
19468
  if (flag("prompt")) {
19407
19469
  process.stdout.write(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.78.0",
3
+ "version": "0.78.1",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",