@tablation/crew 0.50.0 → 0.50.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 +95 -25
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -240,7 +240,7 @@ function mapStreamLine(line) {
240
240
  if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
241
241
  else if (block.type === "text") out.push({ kind: "text", text: block.text });
242
242
  else if (block.type === "tool_use") {
243
- out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
243
+ out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input), input: block.input });
244
244
  }
245
245
  }
246
246
  return out;
@@ -8161,6 +8161,15 @@ function ticketMentions(text) {
8161
8161
  var TicketAttributionTracker = class {
8162
8162
  #tokensByTicket = /* @__PURE__ */ new Map();
8163
8163
  #current;
8164
+ /** The ticket key most recently named in the transcript, or undefined before any. */
8165
+ get current() {
8166
+ return this.#current;
8167
+ }
8168
+ /** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
8169
+ note(text) {
8170
+ const mentions = ticketMentions(text);
8171
+ if (mentions.length > 0) this.#current = mentions[mentions.length - 1];
8172
+ }
8164
8173
  /** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
8165
8174
  add(text, tokens) {
8166
8175
  const mentions = ticketMentions(text);
@@ -8186,6 +8195,11 @@ var TicketAttributionTracker = class {
8186
8195
  return best;
8187
8196
  }
8188
8197
  };
8198
+ function isClaimWrite(tool, input) {
8199
+ if (!tool || !/records_controller_update$/.test(tool)) return false;
8200
+ const body = input?.body;
8201
+ return typeof body === "object" && body !== null && body.status === "in_progress";
8202
+ }
8189
8203
 
8190
8204
  // src/agent.ts
8191
8205
  var DIGEST_MAX_AGE_SECONDS = 600;
@@ -8415,6 +8429,13 @@ var RunObserver = class {
8415
8429
  /** Set once the stream carries the harness's final result (turn count or cost). */
8416
8430
  sawResult = false;
8417
8431
  muted = false;
8432
+ /**
8433
+ * The label stamped on events. Starts as the poll hint and switches to the
8434
+ * ticket the run actually claims (CREW-1776); `id.ticket` stays the hint.
8435
+ */
8436
+ liveTicket;
8437
+ /** Called after `liveTicket` changes, so the sidecar can be rewritten. */
8438
+ onClaim;
8418
8439
  buf = "";
8419
8440
  mapper;
8420
8441
  id;
@@ -8426,6 +8447,7 @@ var RunObserver = class {
8426
8447
  this.adapter = adapter;
8427
8448
  this.eventsSink = eventsSink;
8428
8449
  this.emitCycle = emitCycle;
8450
+ this.liveTicket = id.ticket ?? void 0;
8429
8451
  this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
8430
8452
  }
8431
8453
  feed(text) {
@@ -8466,7 +8488,29 @@ var RunObserver = class {
8466
8488
  consume(parsed, events) {
8467
8489
  const plan = this.id;
8468
8490
  let turnText = "";
8491
+ for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
8469
8492
  for (const ev of events) {
8493
+ if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
8494
+ const claimed = this.attribution.current;
8495
+ if (claimed && claimed !== this.liveTicket) {
8496
+ if (!this.muted) {
8497
+ this.eventsSink?.write(`${JSON.stringify({
8498
+ at: (/* @__PURE__ */ new Date()).toISOString(),
8499
+ cycle: this.emitCycle,
8500
+ route: plan.route,
8501
+ step: "agent",
8502
+ level: "info",
8503
+ message: `ticket: ${this.liveTicket ?? "none"} \u2192 ${claimed}`,
8504
+ role: plan.role,
8505
+ ticket: claimed,
8506
+ data: { kind: "ticket", from: this.liveTicket ?? null, to: claimed }
8507
+ })}
8508
+ `);
8509
+ }
8510
+ this.liveTicket = claimed;
8511
+ if (!this.muted) this.onClaim?.();
8512
+ }
8513
+ }
8470
8514
  const e = {
8471
8515
  at: (/* @__PURE__ */ new Date()).toISOString(),
8472
8516
  cycle: this.emitCycle,
@@ -8475,7 +8519,7 @@ var RunObserver = class {
8475
8519
  level: "info",
8476
8520
  message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
8477
8521
  role: plan.role,
8478
- ...plan.ticket ? { ticket: plan.ticket } : {},
8522
+ ...this.liveTicket ? { ticket: this.liveTicket } : {},
8479
8523
  data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
8480
8524
  };
8481
8525
  if (!this.muted) this.eventsSink?.write(`${JSON.stringify(e)}
@@ -8510,25 +8554,39 @@ var RunObserver = class {
8510
8554
  if (turnTokens !== void 0) this.attribution.add(turnText, turnTokens);
8511
8555
  }
8512
8556
  };
8513
- async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8514
- const attributedTicket = obs.attribution.winner(id.ticket ?? void 0);
8515
- const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
8516
- const { result } = obs;
8557
+ function writeRunMeta(id, startedAt, liveTicket, extra) {
8517
8558
  try {
8518
8559
  writeFileSync8(`${id.streamPath}.meta.json`, JSON.stringify({
8519
8560
  route: id.route,
8520
8561
  role: id.role,
8521
- ticket: id.ticket,
8522
- attributedTicket: attributedTicket ?? null,
8562
+ hint: id.ticket,
8563
+ ticket: liveTicket ?? id.ticket,
8523
8564
  startedAt,
8524
- exitCode: end.code,
8525
8565
  model: id.model,
8566
+ toolchain: id.toolchain,
8567
+ harness: id.harness,
8568
+ provider: id.provider,
8569
+ exitCode: null,
8570
+ running: true,
8571
+ ...extra
8572
+ }));
8573
+ } catch {
8574
+ }
8575
+ }
8576
+ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8577
+ const attributedTicket = obs.attribution.winner(obs.liveTicket);
8578
+ const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
8579
+ const { result } = obs;
8580
+ try {
8581
+ writeRunMeta(id, startedAt, obs.liveTicket, {
8582
+ attributedTicket: attributedTicket ?? null,
8583
+ running: void 0,
8584
+ exitCode: end.code,
8526
8585
  ...end.interrupted ? { interrupted: end.interrupted } : {},
8527
- ...identity,
8528
8586
  sessionId: result?.sessionId ?? null,
8529
8587
  numTurns: result?.numTurns ?? null,
8530
8588
  totalCostUsd: result?.costUsd ?? null
8531
- }));
8589
+ });
8532
8590
  } catch {
8533
8591
  }
8534
8592
  const ms2 = Date.now() - Date.parse(startedAt);
@@ -8616,6 +8674,9 @@ function spawnAgent(plan, emit) {
8616
8674
  }
8617
8675
  }
8618
8676
  const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
8677
+ const meta = { pid: child.pid ?? null };
8678
+ obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
8679
+ if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
8619
8680
  const tail = new FileTail(plan.streamPath);
8620
8681
  const timer = setInterval(() => obs.feed(tail.poll()), TAIL_POLL_MS);
8621
8682
  let spawnFailed = false;
@@ -11996,7 +12057,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
11996
12057
  }
11997
12058
 
11998
12059
  // src/cli.ts
11999
- import { readFileSync as readFileSync18, existsSync as existsSync25, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
12060
+ import { readFileSync as readFileSync19, existsSync as existsSync25, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
12000
12061
  import { createInterface } from "readline/promises";
12001
12062
  import { dirname as dirOf, resolve as resolvePath } from "path";
12002
12063
 
@@ -12736,11 +12797,11 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
12736
12797
  }
12737
12798
 
12738
12799
  // src/daemon.ts
12739
- import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync17, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
12800
+ import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
12740
12801
  import { join as join24 } from "path";
12741
12802
 
12742
12803
  // src/interrupted.ts
12743
- import { existsSync as existsSync23, readdirSync as readdirSync8 } from "fs";
12804
+ import { existsSync as existsSync23, readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
12744
12805
  import { join as join23 } from "path";
12745
12806
  var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
12746
12807
  function interruptedComment(b) {
@@ -12757,6 +12818,15 @@ var pidAlive = (pid) => {
12757
12818
  return e.code === "EPERM";
12758
12819
  }
12759
12820
  };
12821
+ function runFinished(streamPath) {
12822
+ const path = `${streamPath}.meta.json`;
12823
+ if (!existsSync23(path)) return false;
12824
+ try {
12825
+ return JSON.parse(readFileSync17(path, "utf8")).running !== true;
12826
+ } catch {
12827
+ return true;
12828
+ }
12829
+ }
12760
12830
  function orphanedStreams(o) {
12761
12831
  const dir = join23(o.stateDir, "streams");
12762
12832
  if (!existsSync23(dir)) return [];
@@ -12765,7 +12835,7 @@ function orphanedStreams(o) {
12765
12835
  for (const name of readdirSync8(dir)) {
12766
12836
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
12767
12837
  const streamPath = join23(dir, name);
12768
- if (existsSync23(`${streamPath}.meta.json`)) continue;
12838
+ if (runFinished(streamPath)) continue;
12769
12839
  const sidecar = readRunSidecar(streamPath);
12770
12840
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
12771
12841
  if (ownerAlive(sidecar.ownerPid)) continue;
@@ -12889,7 +12959,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
12889
12959
  }
12890
12960
  function readDaemonStarted(stateDir) {
12891
12961
  try {
12892
- const v = JSON.parse(readFileSync17(daemonStampFor(stateDir), "utf8")).startedAt;
12962
+ const v = JSON.parse(readFileSync18(daemonStampFor(stateDir), "utf8")).startedAt;
12893
12963
  return typeof v === "number" ? v : null;
12894
12964
  } catch {
12895
12965
  return null;
@@ -13820,7 +13890,7 @@ if (command === "passenger-sync-daemon") {
13820
13890
  for (const a of plan.actions) {
13821
13891
  if (a.kind === "enable") {
13822
13892
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
13823
- writeFileSync14(cfg.configFile, planEnable(readFileSync18(cfg.configFile, "utf8"), a.route));
13893
+ writeFileSync14(cfg.configFile, planEnable(readFileSync19(cfg.configFile, "utf8"), a.route));
13824
13894
  enabledSound.add(a.route);
13825
13895
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
13826
13896
  } else {
@@ -14958,7 +15028,7 @@ ${changed2.map((c) => ` - ${c}
14958
15028
  `);
14959
15029
  if (!dryRun) {
14960
15030
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
14961
- const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync18(resolvedPath, "utf8")) : {};
15031
+ const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
14962
15032
  raw.agentPersonas = personaResult.agentPersonas;
14963
15033
  raw.agentSkills = skillResult.agentSkills;
14964
15034
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
@@ -15018,7 +15088,7 @@ ${changed2.map((c) => ` - ${c}
15018
15088
  `);
15019
15089
  if (!dryRun) {
15020
15090
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
15021
- const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync18(resolvedPath, "utf8")) : {};
15091
+ const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
15022
15092
  raw.agentSkills = result.agentSkills;
15023
15093
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
15024
15094
  writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -15034,7 +15104,7 @@ ${changed2.map((c) => ` - ${c}
15034
15104
  process.exit(2);
15035
15105
  };
15036
15106
  if (sub !== "add" && sub !== "list") usage2();
15037
- const configText = readFileSync18(cfg.configFile, "utf8");
15107
+ const configText = readFileSync19(cfg.configFile, "utf8");
15038
15108
  let routeName = positional[2];
15039
15109
  let addPath = ".";
15040
15110
  let addName;
@@ -15058,7 +15128,7 @@ ${changed2.map((c) => ` - ${c}
15058
15128
  let resolvedRemotes;
15059
15129
  if (existsSync25(resolvedFile)) {
15060
15130
  try {
15061
- resolvedRemotes = JSON.parse(readFileSync18(resolvedFile, "utf8"));
15131
+ resolvedRemotes = JSON.parse(readFileSync19(resolvedFile, "utf8"));
15062
15132
  } catch {
15063
15133
  }
15064
15134
  }
@@ -15130,7 +15200,7 @@ ${changed2.map((c) => ` - ${c}
15130
15200
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
15131
15201
  `);
15132
15202
  if (existsSync25(resolvedFile)) {
15133
- const raw = JSON.parse(readFileSync18(resolvedFile, "utf8"));
15203
+ const raw = JSON.parse(readFileSync19(resolvedFile, "utf8"));
15134
15204
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
15135
15205
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
15136
15206
  writeFileSync14(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -15379,7 +15449,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15379
15449
  const hadPreviousFile = existsSync25(resolvedPath);
15380
15450
  if (hadPreviousFile) {
15381
15451
  try {
15382
- previous = JSON.parse(readFileSync18(resolvedPath, "utf8"));
15452
+ previous = JSON.parse(readFileSync19(resolvedPath, "utf8"));
15383
15453
  } catch {
15384
15454
  }
15385
15455
  }
@@ -16071,7 +16141,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16071
16141
  }
16072
16142
  case "log": {
16073
16143
  try {
16074
- const text = readFileSync18(cfg.ship.logFile, "utf8").split("\n");
16144
+ const text = readFileSync19(cfg.ship.logFile, "utf8").split("\n");
16075
16145
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
16076
16146
  `);
16077
16147
  } catch {
@@ -16292,7 +16362,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16292
16362
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
16293
16363
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
16294
16364
  try {
16295
- return readFileSync18(p, "utf8");
16365
+ return readFileSync19(p, "utf8");
16296
16366
  } catch {
16297
16367
  return null;
16298
16368
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.50.0",
3
+ "version": "0.50.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",