@tablation/crew 0.68.0 → 0.68.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 +51 -1
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3887,6 +3887,9 @@ function ticketMentions(text) {
3887
3887
  var TicketAttributionTracker = class {
3888
3888
  #tokensByTicket = /* @__PURE__ */ new Map();
3889
3889
  #current;
3890
+ /** Tracker record uuid → ticket key, learned when a write names both (a claim resolves the key from the last mention). */
3891
+ #keyByRecord = /* @__PURE__ */ new Map();
3892
+ #lastBy = {};
3890
3893
  /** The ticket key most recently named in the transcript, or undefined before any. */
3891
3894
  get current() {
3892
3895
  return this.#current;
@@ -3903,6 +3906,37 @@ var TicketAttributionTracker = class {
3903
3906
  if (this.#current === void 0 || tokens <= 0) return;
3904
3907
  this.#tokensByTicket.set(this.#current, (this.#tokensByTicket.get(this.#current) ?? 0) + tokens);
3905
3908
  }
3909
+ /**
3910
+ * Record a tool call if it is a ticket-targeted tracker write (claim,
3911
+ * hand-off or comment). The ticket key comes from a record uuid seen on an
3912
+ * earlier write when known, else from the key last named in the transcript
3913
+ * (a claim or hand-off announces its ticket). A comment on a record never
3914
+ * seen before is ignored: its uuid cannot be resolved to a key.
3915
+ */
3916
+ noteWrite(tool, input) {
3917
+ const w = classifyWrite(tool, input);
3918
+ if (!w) return;
3919
+ let key = w.recordId ? this.#keyByRecord.get(w.recordId) : void 0;
3920
+ if (w.kind !== "comment") key ??= this.#current;
3921
+ if (!key) return;
3922
+ if (w.kind !== "comment" && w.recordId) this.#keyByRecord.set(w.recordId, key);
3923
+ this.#lastBy[w.kind] = key;
3924
+ }
3925
+ /**
3926
+ * How the attribution is decided: a hand-off write beats a claim, a claim
3927
+ * beats a comment (so a follow-up ticket filed or commented on while
3928
+ * working never displaces the claimed one), and any write beats the
3929
+ * mention-token heuristic, which is only the fallback for runs that made
3930
+ * no ticket-targeted write. The pre-run hint is last.
3931
+ */
3932
+ decide(hint) {
3933
+ for (const by of ["handoff", "claim", "comment"]) {
3934
+ const ticket = this.#lastBy[by];
3935
+ if (ticket) return { ticket, by };
3936
+ }
3937
+ if (this.#tokensByTicket.size > 0) return { ticket: this.winner(hint), by: "mentions" };
3938
+ return { ticket: hint, by: "hint" };
3939
+ }
3906
3940
  /**
3907
3941
  * The ticket key the run spent the most tokens on, or `fallback` (the
3908
3942
  * pre-run poll hint) when the transcript never named one — an
@@ -3926,6 +3960,20 @@ function isClaimWrite(tool, input) {
3926
3960
  const body = input?.body;
3927
3961
  return typeof body === "object" && body !== null && body.status === "in_progress";
3928
3962
  }
3963
+ var HANDOFF_STATUSES = /* @__PURE__ */ new Set(["fixed", "verified", "needs_info", "accepted"]);
3964
+ function classifyWrite(tool, input) {
3965
+ if (!tool) return void 0;
3966
+ const body = input?.body;
3967
+ if (typeof body !== "object" || body === null) return void 0;
3968
+ const b = body;
3969
+ if (/records_controller_update$/.test(tool)) {
3970
+ const recordId = typeof input?.recordId === "string" ? input.recordId : void 0;
3971
+ if (b.status === "in_progress" || b.status === "qa") return { kind: "claim", recordId };
3972
+ if (typeof b.status === "string" && HANDOFF_STATUSES.has(b.status)) return { kind: "handoff", recordId };
3973
+ }
3974
+ if (/records_controller_create$/.test(tool) && typeof b.ticket_id === "string") return { kind: "comment", recordId: b.ticket_id };
3975
+ return void 0;
3976
+ }
3929
3977
 
3930
3978
  // src/agent.ts
3931
3979
  var DIGEST_MAX_AGE_SECONDS = 600;
@@ -4216,6 +4264,7 @@ var RunObserver = class {
4216
4264
  let turnText = "";
4217
4265
  for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
4218
4266
  for (const ev of events) {
4267
+ if (ev.kind === "tool") this.attribution.noteWrite(ev.tool, ev.input);
4219
4268
  if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
4220
4269
  const claimed = this.attribution.current;
4221
4270
  if (claimed && claimed !== this.liveTicket) {
@@ -4300,12 +4349,13 @@ function writeRunMeta(id, startedAt, liveTicket, extra) {
4300
4349
  }
4301
4350
  }
4302
4351
  async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
4303
- const attributedTicket = obs.attribution.winner(obs.liveTicket);
4352
+ const { ticket: attributedTicket, by: attributedBy } = obs.attribution.decide(obs.liveTicket);
4304
4353
  const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
4305
4354
  const { result } = obs;
4306
4355
  try {
4307
4356
  writeRunMeta(id, startedAt, obs.liveTicket, {
4308
4357
  attributedTicket: attributedTicket ?? null,
4358
+ attributedBy,
4309
4359
  running: void 0,
4310
4360
  exitCode: end.code,
4311
4361
  ...end.interrupted ? { interrupted: end.interrupted } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.68.0",
3
+ "version": "0.68.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",