@tablation/crew 0.117.0 → 0.118.0

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
@@ -6932,6 +6932,69 @@ var PERISCOPE_COLUMNS = [
6932
6932
  "expires_at"
6933
6933
  ];
6934
6934
 
6935
+ // src/tool-classify.ts
6936
+ var INTEL_SERVER_NAME2 = "code";
6937
+ var SEARCH_TOOLS = /* @__PURE__ */ new Set([
6938
+ "Grep",
6939
+ "Glob",
6940
+ "LS",
6941
+ // Claude
6942
+ "search_file_content",
6943
+ "glob",
6944
+ "list_directory",
6945
+ // Gemini
6946
+ "grep_search",
6947
+ "file_search"
6948
+ ]);
6949
+ var EDIT_TOOLS = /* @__PURE__ */ new Set([
6950
+ "Edit",
6951
+ "Write",
6952
+ "MultiEdit",
6953
+ "NotebookEdit",
6954
+ // Claude
6955
+ "file_change",
6956
+ // Codex
6957
+ "replace",
6958
+ "write_file"
6959
+ // Gemini
6960
+ ]);
6961
+ var SHELL_TOOLS = /* @__PURE__ */ new Set(["Bash", "command", "run_shell_command"]);
6962
+ var SEARCH_COMMANDS = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "rg", "find", "ls", "fd", "ag"]);
6963
+ function classifyTool(tool, target) {
6964
+ if (!tool) return "other";
6965
+ if (tool.startsWith(`mcp__${INTEL_SERVER_NAME2}__`)) return "symbol";
6966
+ if (EDIT_TOOLS.has(tool)) return "edit";
6967
+ if (SEARCH_TOOLS.has(tool)) return "search";
6968
+ if (SHELL_TOOLS.has(tool)) {
6969
+ const first = (target ?? "").split("/").pop();
6970
+ return first && SEARCH_COMMANDS.has(first) ? "search" : "other";
6971
+ }
6972
+ return "other";
6973
+ }
6974
+ var BeforeFirstEditTracker = class {
6975
+ calls = { search: 0, symbol: 0, other: 0 };
6976
+ edited = false;
6977
+ seconds;
6978
+ /** `elapsedMs` is the time since session start, when known (not on a replayed stream). */
6979
+ note(tool, target, elapsedMs) {
6980
+ if (this.edited) return;
6981
+ const kind = classifyTool(tool, target);
6982
+ if (kind === "edit") {
6983
+ this.edited = true;
6984
+ if (elapsedMs !== void 0) this.seconds = Math.max(0, Math.round(elapsedMs / 1e3));
6985
+ } else {
6986
+ this.calls[kind]++;
6987
+ }
6988
+ }
6989
+ result() {
6990
+ return {
6991
+ calls: { ...this.calls },
6992
+ edited: this.edited,
6993
+ ...this.seconds !== void 0 ? { secondsToFirstEdit: this.seconds } : {}
6994
+ };
6995
+ }
6996
+ };
6997
+
6935
6998
  // src/agent-log.ts
6936
6999
  import { existsSync as existsSync17, mkdirSync as mkdirSync12, readdirSync as readdirSync6, readFileSync as readFileSync17, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "fs";
6937
7000
  import { join as join18 } from "path";
@@ -7006,6 +7069,36 @@ function counterIncrements(run3, cols, have) {
7006
7069
  }
7007
7070
  return out;
7008
7071
  }
7072
+ var BEFORE_EDIT_COLUMNS = {
7073
+ search: "calls_before_edit_search",
7074
+ symbol: "calls_before_edit_symbol",
7075
+ other: "calls_before_edit_other",
7076
+ seconds: "seconds_to_first_edit",
7077
+ edited: "edited"
7078
+ };
7079
+ function beforeFirstEditBody(b) {
7080
+ return {
7081
+ [BEFORE_EDIT_COLUMNS.search]: b.calls.search,
7082
+ [BEFORE_EDIT_COLUMNS.symbol]: b.calls.symbol,
7083
+ [BEFORE_EDIT_COLUMNS.other]: b.calls.other,
7084
+ [BEFORE_EDIT_COLUMNS.edited]: b.edited,
7085
+ ...b.secondsToFirstEdit !== void 0 ? { [BEFORE_EDIT_COLUMNS.seconds]: b.secondsToFirstEdit } : {}
7086
+ };
7087
+ }
7088
+ async function writeBeforeFirstEdit(target, rowId, b) {
7089
+ try {
7090
+ const model = await request(
7091
+ target,
7092
+ "GET",
7093
+ `/data-models/agent_log?workspaceId=${encodeURIComponent(target.workspaceId)}`
7094
+ );
7095
+ const have = await columnsOf(target, model.id);
7096
+ const body = Object.fromEntries(Object.entries(beforeFirstEditBody(b)).filter(([column]) => have.has(column)));
7097
+ if (Object.keys(body).length === 0) return;
7098
+ await request(target, "PATCH", `/data-models/${model.id}/records/${rowId}`, body);
7099
+ } catch {
7100
+ }
7101
+ }
7009
7102
  async function post(target, path, body) {
7010
7103
  const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
7011
7104
  const res = await fetch(url, {
@@ -7074,6 +7167,7 @@ async function reportAgentRun(target, run3) {
7074
7167
  ...cycle.role ? { role: cycle.role } : {}
7075
7168
  });
7076
7169
  }
7170
+ if (run3.beforeFirstEdit) await writeBeforeFirstEdit(target, entry.id, run3.beforeFirstEdit);
7077
7171
  const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run3) : void 0;
7078
7172
  return {
7079
7173
  agentLogId: entry.id,
@@ -10588,6 +10682,8 @@ var RunObserver = class {
10588
10682
  result;
10589
10683
  /** Prefix tokens of the first assistant call that carried usage; kept once. */
10590
10684
  prefixTokens;
10685
+ /** Tool calls before the run's first edit (CREW-2879). */
10686
+ beforeFirstEdit = new BeforeFirstEditTracker();
10591
10687
  /** Set once the stream carries the harness's final result (turn count or cost). */
10592
10688
  sawResult = false;
10593
10689
  muted = false;
@@ -10599,6 +10695,7 @@ var RunObserver = class {
10599
10695
  /** Called after `liveTicket` changes, so the sidecar can be rewritten. */
10600
10696
  onClaim;
10601
10697
  buf = "";
10698
+ startMs = Date.now();
10602
10699
  mapper;
10603
10700
  id;
10604
10701
  adapter;
@@ -10654,6 +10751,7 @@ var RunObserver = class {
10654
10751
  for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
10655
10752
  for (const ev of events) {
10656
10753
  if (ev.kind === "tool") this.attribution.noteWrite(ev.tool, ev.input);
10754
+ if (ev.kind === "tool") this.beforeFirstEdit.note(ev.tool, ev.target, this.muted ? void 0 : Date.now() - this.startMs);
10657
10755
  if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
10658
10756
  const claimed = this.attribution.current;
10659
10757
  if (claimed && claimed !== this.liveTicket) {
@@ -10768,6 +10866,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10768
10866
  emit.warn(`could not save the pending hand-off: ${e.message}`, { step: "agent" });
10769
10867
  }
10770
10868
  }
10869
+ const before = obs.beforeFirstEdit.result();
10771
10870
  try {
10772
10871
  writeRunMeta(id, startedAt, obs.liveTicket, {
10773
10872
  attributedTicket: attributedTicket ?? null,
@@ -10779,6 +10878,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10779
10878
  sessionId: result?.sessionId ?? null,
10780
10879
  numTurns: result?.numTurns ?? null,
10781
10880
  prefixTokens: obs.prefixTokens ?? null,
10881
+ beforeFirstEdit: before,
10782
10882
  totalCostUsd: result?.costUsd ?? null
10783
10883
  });
10784
10884
  } catch {
@@ -10794,6 +10894,9 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10794
10894
  attributedTicket: attributedTicket ?? null,
10795
10895
  attributedBy,
10796
10896
  ...result?.sessionId ? { sessionId: result.sessionId } : {},
10897
+ callsBeforeFirstEdit: before.calls,
10898
+ edited: before.edited,
10899
+ ...before.secondsToFirstEdit !== void 0 ? { secondsToFirstEdit: before.secondsToFirstEdit } : {},
10797
10900
  ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
10798
10901
  ...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
10799
10902
  }
@@ -10814,6 +10917,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10814
10917
  // Both come from a finished run: without the result line (interrupted) neither is trustworthy.
10815
10918
  ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
10816
10919
  ...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
10920
+ beforeFirstEdit: before,
10817
10921
  harness: identity.harness,
10818
10922
  provider: identity.provider,
10819
10923
  promptVersion: id.promptVersion,
@@ -19591,7 +19695,20 @@ async function listLogEntries(route2, opts = {}) {
19591
19695
  limit: opts.limit ?? 20,
19592
19696
  ...filters.length > 0 ? { filters: JSON.stringify(filters) } : {}
19593
19697
  });
19594
- return [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
19698
+ const newestFirst = [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
19699
+ if (!opts.beforeEdit) return newestFirst;
19700
+ const searches = (e) => e.calls_before_edit_search ?? -1;
19701
+ return newestFirst.sort((a, b) => searches(b) - searches(a));
19702
+ }
19703
+ function formatBeforeEdit(e) {
19704
+ if (e.calls_before_edit_search == null && e.calls_before_edit_symbol == null && e.calls_before_edit_other == null) return void 0;
19705
+ const parts = [
19706
+ `search ${e.calls_before_edit_search ?? 0}`,
19707
+ `symbol ${e.calls_before_edit_symbol ?? 0}`,
19708
+ `other ${e.calls_before_edit_other ?? 0}`
19709
+ ];
19710
+ const when = e.edited === false ? "no edit" : e.seconds_to_first_edit != null ? `${e.seconds_to_first_edit}s to first edit` : void 0;
19711
+ return `${parts.join(", ")}${when ? ` (${when})` : ""}`;
19595
19712
  }
19596
19713
  async function showLogEntry(route2, entryId, opts = {}) {
19597
19714
  requireWorkspace(route2);
@@ -23526,7 +23643,7 @@ function usage(problem, requested = false) {
23526
23643
  crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
23527
23644
  crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
23528
23645
  crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches (--json: every Repos row too)
23529
- crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
23646
+ crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket; --before-edit sorts by searches before the first edit
23530
23647
  crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
23531
23648
  crew passengers [route] sync Host Passengers containers/tunnels once; what the passengers unit invokes
23532
23649
  crew install write and load this platform's scheduler unit
@@ -26179,7 +26296,7 @@ ${cu.tail.trimEnd()}
26179
26296
  const sub = positional[1];
26180
26297
  if (sub !== "list" && sub !== "show") {
26181
26298
  process.stderr.write(
26182
- "crew logbook list [route] [--role R] [--ticket T] [--limit N]\ncrew logbook show [route] <entryId> [--prompt]\n"
26299
+ "crew logbook list [route] [--role R] [--ticket T] [--limit N] [--before-edit]\ncrew logbook show [route] <entryId> [--prompt]\n"
26183
26300
  );
26184
26301
  process.exit(2);
26185
26302
  }
@@ -26206,7 +26323,8 @@ ${cu.tail.trimEnd()}
26206
26323
  userAgent: cfg.ship.userAgent,
26207
26324
  role: value("role"),
26208
26325
  ticket: value("ticket"),
26209
- limit: value("limit") ? Number(value("limit")) : void 0
26326
+ limit: value("limit") ? Number(value("limit")) : void 0,
26327
+ beforeEdit: flag("before-edit")
26210
26328
  });
26211
26329
  if (entries.length === 0) {
26212
26330
  process.stdout.write("(no Agent Log entries match)\n");
@@ -26214,7 +26332,7 @@ ${cu.tail.trimEnd()}
26214
26332
  }
26215
26333
  for (const e of entries) {
26216
26334
  process.stdout.write(
26217
- `${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}` : ""}
26335
+ `${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}` : ""}${formatBeforeEdit(e) ? ` before edit: ${formatBeforeEdit(e)}` : ""}
26218
26336
  `
26219
26337
  );
26220
26338
  }
@@ -26234,6 +26352,7 @@ harness: ${entry.harness ?? "?"}
26234
26352
  provider: ${entry.provider ?? "?"}
26235
26353
  ` + (entry.num_turns != null ? `turns: ${entry.num_turns}
26236
26354
  ` : "") + (entry.prefix_tokens != null ? `prefix: ${entry.prefix_tokens} tokens
26355
+ ` : "") + (formatBeforeEdit(entry) ? `before edit: ${formatBeforeEdit(entry)}
26237
26356
  ` : "")
26238
26357
  );
26239
26358
  if (flag("prompt")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.117.0",
3
+ "version": "0.118.0",
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",
@@ -93,6 +93,11 @@ available only in a repository whose hooks table declares `preview`.
93
93
  the URL. Tell them where it is once, early: the URL always, and, when you
94
94
  are the operator's own session on this ship (not a Passenger's chat), the
95
95
  worktree path as well, so they can open the files directly.
96
+ - **When the person refers to something on the page** ("this", "here",
97
+ "that heading"), read the newest line of `.crew/pointed-at.jsonl` in the
98
+ periscope's worktree before searching; it names the file and line they
99
+ clicked. If the file is empty, ask them to point at it with the inspector
100
+ or paste the location.
96
101
  - **When a dev lane is mid-flight on the same files** (visible from the
97
102
  roster and the board), say so, so the person knows a merge is coming.
98
103
  - **Before keep,** read back the ticket title and a one-paragraph summary of
@@ -424,6 +424,26 @@ before going further, and never change a ticket's `report_type` yourself.
424
424
  sets the ticket back to `in_progress` instead of passing it to QA. A ticket
425
425
  with no Acceptance section is unaffected.
426
426
 
427
+ ### Finding where something lives
428
+
429
+ Before you read or edit, locate the code once, in this order:
430
+
431
+ 1. **The map.** Read the "Repository map" section of this brief (when the
432
+ repository has one) and the repository's own instructions file, which may
433
+ also say where things live.
434
+ 2. **The symbol tools.** When the Environment section lists code tools, use
435
+ them for code questions before searching text: find a symbol by name, list
436
+ its references, read its definition, outline a file. Text search is for
437
+ strings that are not symbols.
438
+ 3. **One broad search.** To find a visible string, search for it once across
439
+ the whole relevant tree, excluding tests, and prefer registries over
440
+ components. A search that found only a test file is widened, not
441
+ narrowed.
442
+
443
+ Count the calls: if more than three searches have not found the location, say
444
+ so in your progress comment and ask for the map or the person's pointer
445
+ rather than continuing to search.
446
+
427
447
  ## Step 4 — you never merge, and you never deploy
428
448
 
429
449
  **Nothing you do touches the base branch.** The release phase takes every
@@ -93,6 +93,11 @@ available only in a repository whose hooks table declares `preview`.
93
93
  the URL. Tell them where it is once, early: the URL always, and, when you
94
94
  are the operator's own session on this ship (not a Passenger's chat), the
95
95
  worktree path as well, so they can open the files directly.
96
+ - **When the person refers to something on the page** ("this", "here",
97
+ "that heading"), read the newest line of `.crew/pointed-at.jsonl` in the
98
+ periscope's worktree before searching; it names the file and line they
99
+ clicked. If the file is empty, ask them to point at it with the inspector
100
+ or paste the location.
96
101
  - **When a dev lane is mid-flight on the same files** (visible from the
97
102
  roster and the board), say so, so the person knows a merge is coming.
98
103
  - **Before keep,** read back the ticket title and a one-paragraph summary of
@@ -419,6 +419,26 @@ before going further, and never change a ticket's `report_type` yourself.
419
419
  sets the ticket back to `in_progress` instead of passing it to QA. A ticket
420
420
  with no Acceptance section is unaffected.
421
421
 
422
+ ### Finding where something lives
423
+
424
+ Before you read or edit, locate the code once, in this order:
425
+
426
+ 1. **The map.** Read the "Repository map" section of this brief (when the
427
+ repository has one) and the repository's own instructions file, which may
428
+ also say where things live.
429
+ 2. **The symbol tools.** When the Environment section lists code tools, use
430
+ them for code questions before searching text: find a symbol by name, list
431
+ its references, read its definition, outline a file. Text search is for
432
+ strings that are not symbols.
433
+ 3. **One broad search.** To find a visible string, search for it once across
434
+ the whole relevant tree, excluding tests, and prefer registries over
435
+ components. A search that found only a test file is widened, not
436
+ narrowed.
437
+
438
+ Count the calls: if more than three searches have not found the location, say
439
+ so in your progress comment and ask for the map or the person's pointer
440
+ rather than continuing to search.
441
+
422
442
  ## Step 4 — you never merge, and you never deploy
423
443
 
424
444
  **Nothing you do touches the base branch.** The release phase takes every