@jaggerxtrm/specialists 3.16.0 → 3.17.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.
Files changed (53) hide show
  1. package/README.md +3 -1
  2. package/config/mandatory-rules/research-tool-routing.md +4 -0
  3. package/config/mandatory-rules/test-runner-execution-scope.md +1 -1
  4. package/config/skills/specialists-creator/SKILL.md +28 -1
  5. package/config/skills/using-specialists-auto/SKILL.md +1 -1
  6. package/config/skills/using-specialists-v2/SKILL.md +7 -7
  7. package/config/skills/using-specialists-v3/SKILL.md +223 -147
  8. package/config/specialists/changelog-drafter.specialist.json +4 -2
  9. package/config/specialists/changelog-keeper.specialist.json +4 -2
  10. package/config/specialists/debugger.specialist.json +6 -4
  11. package/config/specialists/executor.specialist.json +7 -5
  12. package/config/specialists/explorer.specialist.json +4 -2
  13. package/config/specialists/memory-processor.specialist.json +4 -2
  14. package/config/specialists/node-coordinator.specialist.json +4 -2
  15. package/config/specialists/obligations-scanner.specialist.json +99 -0
  16. package/config/specialists/overthinker.specialist.json +4 -2
  17. package/config/specialists/planner.specialist.json +4 -2
  18. package/config/specialists/researcher.specialist.json +12 -7
  19. package/config/specialists/reviewer.specialist.json +10 -8
  20. package/config/specialists/seconder.specialist.json +170 -0
  21. package/config/specialists/security-auditor.specialist.json +4 -2
  22. package/config/specialists/service-skills-sync.specialist.json +78 -0
  23. package/config/specialists/specialists-creator.specialist.json +6 -4
  24. package/config/specialists/sync-docs.specialist.json +5 -3
  25. package/config/specialists/test-engineer.specialist.json +134 -0
  26. package/config/specialists/test-runner.specialist.json +8 -6
  27. package/config/specialists/transcriber.specialist.json +59 -0
  28. package/config/specialists/xt-merge.specialist.json +4 -2
  29. package/dist/asset-contract.json +20 -8
  30. package/dist/index.js +1136 -293
  31. package/dist/lib.js +1 -0
  32. package/dist/types/cli/format-helpers.d.ts.map +1 -1
  33. package/dist/types/cli/help.d.ts.map +1 -1
  34. package/dist/types/cli/log.d.ts +2 -0
  35. package/dist/types/cli/log.d.ts.map +1 -0
  36. package/dist/types/cli/result.d.ts.map +1 -1
  37. package/dist/types/cli/resume.d.ts.map +1 -1
  38. package/dist/types/cli/steer.d.ts.map +1 -1
  39. package/dist/types/index.d.ts +1 -1
  40. package/dist/types/specialist/control.d.ts.map +1 -1
  41. package/dist/types/specialist/job-file-output.d.ts +2 -0
  42. package/dist/types/specialist/job-file-output.d.ts.map +1 -1
  43. package/dist/types/specialist/launch.d.ts.map +1 -1
  44. package/dist/types/specialist/runner.d.ts +3 -0
  45. package/dist/types/specialist/runner.d.ts.map +1 -1
  46. package/dist/types/specialist/schema.d.ts +18 -9
  47. package/dist/types/specialist/schema.d.ts.map +1 -1
  48. package/dist/types/specialist/supervisor.d.ts +25 -2
  49. package/dist/types/specialist/supervisor.d.ts.map +1 -1
  50. package/dist/types/specialist/timeline-events.d.ts +20 -1
  51. package/dist/types/specialist/timeline-events.d.ts.map +1 -1
  52. package/package.json +13 -11
  53. package/config/specialists/code-sanity.specialist.json +0 -108
package/dist/index.js CHANGED
@@ -7895,7 +7895,7 @@ var require_core = __commonJS((exports) => {
7895
7895
  constructor(opts = {}) {
7896
7896
  this.schemas = {};
7897
7897
  this.refs = {};
7898
- this.formats = {};
7898
+ this.formats = Object.create(null);
7899
7899
  this._compilations = new Set;
7900
7900
  this._loading = {};
7901
7901
  this._cache = new Map;
@@ -17734,6 +17734,7 @@ var init_schema = __esm(() => {
17734
17734
  stall_detection: StallDetectionSchema,
17735
17735
  mandatory_rules: MandatoryRulesSchema,
17736
17736
  output_file: stringType().optional(),
17737
+ notes_mode: enumType(["full-trail", "final-only"]).default("full-trail"),
17737
17738
  beads_integration: enumType(["auto", "always", "never"]).default("auto"),
17738
17739
  beads_write_notes: booleanType().default(true)
17739
17740
  }).passthrough()
@@ -17926,6 +17927,14 @@ function detectJobFileOutputMode(env = process.env) {
17926
17927
  function isJobFileOutputEnabled(env = process.env) {
17927
17928
  return detectJobFileOutputMode(env) === "on";
17928
17929
  }
17930
+ async function writeJobFileOutput(path, content, mode) {
17931
+ const { appendFile, writeFile } = await import("fs/promises");
17932
+ if (mode === "append") {
17933
+ await appendFile(path, content, "utf-8");
17934
+ return;
17935
+ }
17936
+ await writeFile(path, content, "utf-8");
17937
+ }
17929
17938
 
17930
17939
  // src/specialist/templateEngine.ts
17931
17940
  function renderTemplate(template, variables) {
@@ -22088,7 +22097,6 @@ __export(exports_runner, {
22088
22097
  SpecialistRunner: () => SpecialistRunner
22089
22098
  });
22090
22099
  import { createHash as createHash2 } from "crypto";
22091
- import { writeFile } from "fs/promises";
22092
22100
  import { execSync as execSync2, spawnSync as spawnSync4 } from "child_process";
22093
22101
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
22094
22102
  import { basename as basename2, resolve as resolve4 } from "path";
@@ -23061,8 +23069,8 @@ ${outputContractWarnings.map((msg) => ` \u26A0 ${msg}`).join(`
23061
23069
  `)}
23062
23070
  `);
23063
23071
  }
23064
- if (output_file && isJobFileOutputEnabled()) {
23065
- await writeFile(output_file, output, "utf-8").catch(() => {});
23072
+ if (output_file && !options.suppressRunnerFileOutput) {
23073
+ await writeJobFileOutput(output_file, output, "overwrite").catch(() => {});
23066
23074
  }
23067
23075
  await hooks.emit("post_execute", invocationId, metadata.name, metadata.version, {
23068
23076
  status: "COMPLETE",
@@ -23916,6 +23924,14 @@ function createRunCompleteEvent(status, elapsed_s, options) {
23916
23924
  ...options
23917
23925
  };
23918
23926
  }
23927
+ function createControlSignalEvent(action, options) {
23928
+ return {
23929
+ t: Date.now(),
23930
+ type: TIMELINE_EVENT_TYPES.CONTROL_SIGNAL,
23931
+ action,
23932
+ ...options
23933
+ };
23934
+ }
23919
23935
  function createAutoCommitEvent(status, options) {
23920
23936
  const type = status === "success" ? TIMELINE_EVENT_TYPES.AUTO_COMMIT_SUCCESS : status === "skipped" ? TIMELINE_EVENT_TYPES.AUTO_COMMIT_SKIPPED : TIMELINE_EVENT_TYPES.AUTO_COMMIT_FAILED;
23921
23937
  return {
@@ -23991,6 +24007,7 @@ var init_timeline_events = __esm(() => {
23991
24007
  AUTO_COMMIT_SUCCESS: "auto_commit_success",
23992
24008
  AUTO_COMMIT_SKIPPED: "auto_commit_skipped",
23993
24009
  AUTO_COMMIT_FAILED: "auto_commit_failed",
24010
+ CONTROL_SIGNAL: "control_signal",
23994
24011
  DONE: "done",
23995
24012
  AGENT_END: "agent_end"
23996
24013
  };
@@ -24498,24 +24515,37 @@ function getCurrentGitSha() {
24498
24515
  const sha = result.stdout?.trim();
24499
24516
  return sha || undefined;
24500
24517
  }
24501
- function formatBeadNotes(result) {
24502
- const statusLabel = result.status === "waiting" ? "WAITING \u2014 more output may follow" : result.status.toUpperCase();
24503
- const metadata = [
24504
- `timestamp=${result.timestamp}`,
24505
- `status=${result.status}`,
24506
- `prompt_hash=${result.promptHash ?? "unknown"}`,
24507
- `git_sha=${getCurrentGitSha() ?? "unknown"}`,
24508
- `elapsed_ms=${result.durationMs !== undefined ? Math.round(result.durationMs) : "unknown"}`,
24509
- `model=${result.model}`,
24510
- `backend=${result.backend}`
24511
- ].join(`
24512
- `);
24513
- return `### Specialist Output \u2014 ${result.specialist} (job ${result.jobId}) [${statusLabel}]
24518
+ function normalizeHandoffModel(model) {
24519
+ return model.split("/").at(-1) ?? model;
24520
+ }
24521
+ function formatHandoffBlock(result, options) {
24522
+ const statusToken = options.final ? `FINAL \xB7 ${result.status === "cancelled" ? "CANCELLED" : result.status === "error" ? "ERROR" : "DONE"}` : result.status === "waiting" ? "WAITING" : "WORKING";
24523
+ const model = normalizeHandoffModel(result.model);
24524
+ const header = options.final ? `## ${result.specialist} \xB7 ${model} \xB7 [${statusToken}]` : `### ${result.specialist} \xB7 ${model} \xB7 [turn ${result.turnIndex ?? "unknown"} \xB7 ${statusToken}]`;
24525
+ const tokenUsage = result.tokenUsage;
24526
+ const gitSha = getCurrentGitSha();
24527
+ const footerParts = [
24528
+ options.final ? "final" : `turn ${result.turnIndex ?? "unknown"}`,
24529
+ result.durationMs !== undefined ? `${Math.round(result.durationMs)} ms` : undefined,
24530
+ tokenUsage?.input_tokens && tokenUsage?.output_tokens ? `${tokenUsage.input_tokens} to ${tokenUsage.output_tokens} tok` : tokenUsage?.input_tokens ? `${tokenUsage.input_tokens} tok in` : tokenUsage?.output_tokens ? `${tokenUsage.output_tokens} tok out` : undefined,
24531
+ result.timestamp ? new Date(result.timestamp).toISOString().slice(0, 16).replace("T", " ") : undefined,
24532
+ gitSha ? `git ${gitSha.slice(0, 8)}` : undefined
24533
+ ].filter((part) => Boolean(part));
24534
+ const footer = footerParts.length > 0 ? `_${footerParts.join(" \xB7 ")}_` : "";
24535
+ return `
24514
24536
 
24515
- ${result.output}
24537
+ ${header}
24516
24538
 
24517
- ---
24518
- ${metadata}`;
24539
+ ${result.output}${footer ? `
24540
+
24541
+ ${footer}` : ""}`;
24542
+ }
24543
+ function shouldPersistHandoffBlock(params) {
24544
+ if (!params.output.trim())
24545
+ return false;
24546
+ if (params.notesMode === "final-only" && !params.final)
24547
+ return false;
24548
+ return true;
24519
24549
  }
24520
24550
  function getModelContextWindow(model) {
24521
24551
  if (!model)
@@ -24912,10 +24942,6 @@ class Supervisor {
24912
24942
  observabilityDbPath() {
24913
24943
  return resolveObservabilityDbLocation(this.opts.runOptions?.workingDirectory ?? process.cwd()).dbPath;
24914
24944
  }
24915
- shouldWriteJobFiles() {
24916
- const mode = String(process.env.SPECIALISTS_JOB_FILE_OUTPUT ?? "").trim().toLowerCase();
24917
- return mode === "" || mode === "on" || mode === "true" || mode === "1";
24918
- }
24919
24945
  eventsPath(id) {
24920
24946
  return join9(this.jobDir(id), "events.jsonl");
24921
24947
  }
@@ -24932,6 +24958,45 @@ class Supervisor {
24932
24958
  is_dead: isJobDead(status)
24933
24959
  };
24934
24960
  }
24961
+ reconcileDeadStatus(id, status) {
24962
+ if (!isJobDead(status) || status.status !== "running" && status.status !== "starting") {
24963
+ return this.withComputedLiveness(status);
24964
+ }
24965
+ if (!Number.isFinite(status.started_at_ms)) {
24966
+ return this.withComputedLiveness(status);
24967
+ }
24968
+ const now = Date.now();
24969
+ const recoveredStatus = {
24970
+ ...status,
24971
+ status: "error",
24972
+ current_event: undefined,
24973
+ error: "Process crashed or was killed",
24974
+ last_event_at_ms: now
24975
+ };
24976
+ const elapsed = Math.max(0, Math.round((now - status.started_at_ms) / 1000));
24977
+ const runCompleteEvent = createRunCompleteEvent("ERROR", elapsed, {
24978
+ error: recoveredStatus.error,
24979
+ exit_reason: "crashed"
24980
+ });
24981
+ if (this.sqliteClient) {
24982
+ const persisted = this.withSqliteOperation("upsertStatusWithEvent:readStatus", (client) => {
24983
+ client.upsertStatusWithEvent(recoveredStatus, runCompleteEvent);
24984
+ return true;
24985
+ });
24986
+ if (persisted === undefined) {
24987
+ throw new Error("[supervisor] SQLite upsertStatusWithEvent failed during readStatus recovery: database client unavailable");
24988
+ }
24989
+ } else {
24990
+ this.writeStatusFileOnly(id, recoveredStatus);
24991
+ if (this.isJobFileOutputEnabled) {
24992
+ const eventsPath = this.eventsPath(id);
24993
+ mkdirSync4(this.jobDir(id), { recursive: true });
24994
+ appendFileSync(eventsPath, JSON.stringify(runCompleteEvent) + `
24995
+ `, "utf-8");
24996
+ }
24997
+ }
24998
+ return this.withComputedLiveness(recoveredStatus);
24999
+ }
24935
25000
  readStatus(id) {
24936
25001
  try {
24937
25002
  if (this.isDisposed) {
@@ -24939,7 +25004,7 @@ class Supervisor {
24939
25004
  }
24940
25005
  const sqliteStatus = this.withSqliteOperation("readStatus", (client) => client.readStatus(id));
24941
25006
  if (sqliteStatus)
24942
- return this.withComputedLiveness(sqliteStatus);
25007
+ return this.reconcileDeadStatus(id, sqliteStatus);
24943
25008
  } catch (error2) {
24944
25009
  if (!(error2 instanceof Error && error2.message.includes("supervisor is disposed"))) {
24945
25010
  console.warn(`[supervisor] SQLite readStatus failed, falling back to file state: ${String(error2)}`);
@@ -24950,7 +25015,7 @@ class Supervisor {
24950
25015
  return null;
24951
25016
  try {
24952
25017
  const status = JSON.parse(readFileSync8(path, "utf-8"));
24953
- return this.withComputedLiveness(status);
25018
+ return this.reconcileDeadStatus(id, status);
24954
25019
  } catch {
24955
25020
  return null;
24956
25021
  }
@@ -24983,7 +25048,7 @@ class Supervisor {
24983
25048
  throw this.createDisposedSqliteError("readResult");
24984
25049
  }
24985
25050
  const sqliteResult = this.withSqliteOperation("readResult", (client) => client.readResult(id));
24986
- if (sqliteResult)
25051
+ if (typeof sqliteResult === "string" && sqliteResult.trim().length > 0)
24987
25052
  return sqliteResult;
24988
25053
  } catch (error2) {
24989
25054
  if (!(error2 instanceof Error && error2.message.includes("supervisor is disposed"))) {
@@ -25013,25 +25078,52 @@ class Supervisor {
25013
25078
  if (!finalized)
25014
25079
  return null;
25015
25080
  this.aggregateJobMetricsBestEffort(id);
25081
+ if (finalized.bead_id && this.opts.beadsClient) {
25082
+ const outputPath = this.resultPath(id);
25083
+ const output = existsSync10(outputPath) ? readFileSync8(outputPath, "utf-8") || this.readResult(id) || "" : this.readResult(id) || "";
25084
+ if (output.trim()) {
25085
+ this.opts.beadsClient.updateBeadNotes(finalized.bead_id, formatHandoffBlock({
25086
+ output,
25087
+ model: finalized.model ?? "unknown",
25088
+ backend: finalized.backend ?? "unknown",
25089
+ specialist: finalized.specialist,
25090
+ jobId: id,
25091
+ status: "done",
25092
+ timestamp: new Date().toISOString()
25093
+ }, { final: true }));
25094
+ }
25095
+ }
25016
25096
  return finalized;
25017
25097
  }
25098
+ appendEventBestEffort(jobId, operation, event) {
25099
+ try {
25100
+ const status = this.readStatus(jobId);
25101
+ const persisted = this.withSqliteOperation(operation, (client) => {
25102
+ client.appendEvent(jobId, status?.specialist ?? "unknown", status?.bead_id, event);
25103
+ return true;
25104
+ });
25105
+ if (persisted === undefined) {
25106
+ console.warn(`[supervisor] SQLite ${operation} skipped: database client unavailable`);
25107
+ }
25108
+ } catch (error2) {
25109
+ console.warn(`[supervisor] SQLite ${operation} failed: ${String(error2)}`);
25110
+ }
25111
+ }
25018
25112
  emitMetaEvent(jobId, model, backend) {
25019
25113
  if (this.isDisposed)
25020
25114
  return;
25021
- const event = createMetaEvent(model, backend);
25022
- const persisted = this.withSqliteOperation("appendEvent", (client) => {
25023
- const status = this.readStatus(jobId);
25024
- client.appendEvent(jobId, status?.specialist ?? "unknown", status?.bead_id, event);
25025
- return true;
25026
- });
25027
- if (persisted === undefined) {
25028
- throw new Error("[supervisor] SQLite appendEvent failed: database client unavailable");
25029
- }
25115
+ this.appendEventBestEffort(jobId, "appendEvent", createMetaEvent(model, backend));
25116
+ }
25117
+ emitControlEvent(jobId, action, options) {
25118
+ if (this.isDisposed)
25119
+ return;
25120
+ this.appendEventBestEffort(jobId, "appendEvent", createControlSignalEvent(action, options));
25030
25121
  }
25031
25122
  updateJobStatus(id, status, error2) {
25032
25123
  const currentStatus = this.readStatus(id);
25033
25124
  if (!currentStatus)
25034
25125
  return null;
25126
+ const previousStatus = currentStatus.status;
25035
25127
  const updatedStatus = {
25036
25128
  ...currentStatus,
25037
25129
  status,
@@ -25039,7 +25131,10 @@ class Supervisor {
25039
25131
  error: error2,
25040
25132
  last_event_at_ms: Date.now()
25041
25133
  };
25042
- this.writeStatusFile(id, updatedStatus);
25134
+ this.writeStatusFile(id, updatedStatus, { sqliteFailureMode: "warn" });
25135
+ if (previousStatus !== status) {
25136
+ this.appendEventBestEffort(id, "appendEvent:status_change", createStatusChangeEvent(status, previousStatus));
25137
+ }
25043
25138
  return this.withComputedLiveness(updatedStatus);
25044
25139
  }
25045
25140
  aggregateJobMetricsBestEffort(jobId) {
@@ -25109,41 +25204,49 @@ class Supervisor {
25109
25204
  writeFileSync3(tmp, JSON.stringify(normalizedStatus, null, 2), "utf-8");
25110
25205
  renameSync(tmp, path);
25111
25206
  }
25112
- writeStatusFile(id, data) {
25207
+ writeStatusFile(id, data, options = {}) {
25113
25208
  const normalizedStatus = this.withStatusLineageDefaults(id, data);
25114
25209
  this.writeStatusFileOnly(id, normalizedStatus);
25115
- const persisted = this.withSqliteOperation("upsertStatus", (client) => {
25116
- client.upsertStatus(normalizedStatus);
25117
- const chainId = resolveChainId(normalizedStatus);
25118
- if (!normalizedStatus.epic_id || !chainId) {
25119
- return true;
25120
- }
25121
- client.upsertEpicRun({
25122
- epic_id: normalizedStatus.epic_id,
25123
- status: "open",
25124
- updated_at_ms: Date.now(),
25125
- status_json: JSON.stringify({
25210
+ try {
25211
+ const persisted = this.withSqliteOperation("upsertStatus", (client) => {
25212
+ client.upsertStatus(normalizedStatus);
25213
+ const chainId = resolveChainId(normalizedStatus);
25214
+ if (!normalizedStatus.epic_id || !chainId) {
25215
+ return true;
25216
+ }
25217
+ client.upsertEpicRun({
25126
25218
  epic_id: normalizedStatus.epic_id,
25127
25219
  status: "open",
25128
- source: "supervisor",
25220
+ updated_at_ms: Date.now(),
25221
+ status_json: JSON.stringify({
25222
+ epic_id: normalizedStatus.epic_id,
25223
+ status: "open",
25224
+ source: "supervisor",
25225
+ chain_id: chainId,
25226
+ chain_root_bead_id: normalizedStatus.chain_root_bead_id ?? null,
25227
+ chain_root_job_id: normalizedStatus.chain_root_job_id ?? normalizedStatus.id
25228
+ })
25229
+ });
25230
+ client.upsertEpicChainMembership({
25129
25231
  chain_id: chainId,
25130
- chain_root_bead_id: normalizedStatus.chain_root_bead_id ?? null,
25131
- chain_root_job_id: normalizedStatus.chain_root_job_id ?? normalizedStatus.id
25132
- })
25133
- });
25134
- client.upsertEpicChainMembership({
25135
- chain_id: chainId,
25136
- epic_id: normalizedStatus.epic_id,
25137
- chain_root_bead_id: normalizedStatus.chain_root_bead_id,
25138
- chain_root_job_id: normalizedStatus.chain_root_job_id ?? normalizedStatus.id,
25139
- updated_at_ms: Date.now()
25232
+ epic_id: normalizedStatus.epic_id,
25233
+ chain_root_bead_id: normalizedStatus.chain_root_bead_id,
25234
+ chain_root_job_id: normalizedStatus.chain_root_job_id ?? normalizedStatus.id,
25235
+ updated_at_ms: Date.now()
25236
+ });
25237
+ const readiness = loadEpicReadinessSummary(client, normalizedStatus.epic_id);
25238
+ syncEpicStateFromReadiness(client, readiness);
25239
+ return true;
25140
25240
  });
25141
- const readiness = loadEpicReadinessSummary(client, normalizedStatus.epic_id);
25142
- syncEpicStateFromReadiness(client, readiness);
25143
- return true;
25144
- });
25145
- if (persisted === undefined) {
25146
- throw new Error("[supervisor] SQLite upsertStatus failed: database client unavailable");
25241
+ if (persisted === undefined) {
25242
+ throw new Error("[supervisor] SQLite upsertStatus failed: database client unavailable");
25243
+ }
25244
+ } catch (error2) {
25245
+ if (options.sqliteFailureMode === "warn") {
25246
+ console.warn(`[supervisor] SQLite upsertStatus failed: ${String(error2)}`);
25247
+ return;
25248
+ }
25249
+ throw error2;
25147
25250
  }
25148
25251
  }
25149
25252
  gc() {
@@ -25475,6 +25578,10 @@ class Supervisor {
25475
25578
  const shouldAutoCloseReadOnlyKeepAlive = (output) => isReadOnlySpecialist && TERMINAL_COMPLIANCE_VERDICT_REGEX.test(output);
25476
25579
  const shouldAutoFinalizeKeepAlive = (output) => PASS_COMPLIANCE_VERDICT_REGEX.test(output);
25477
25580
  const shouldWriteExternalBeadNotes = runOptions.beadsWriteNotes ?? true;
25581
+ const notesMode = runOptions.notesMode ?? "full-trail";
25582
+ const outputFile = runOptions.output_file;
25583
+ let lastTurnSummaryTextContent = "";
25584
+ let lastTurnSummaryIndex = 0;
25478
25585
  let skipFinalKeepAliveInputBeadAppend = false;
25479
25586
  let lastGitnexusAnalyzedSha;
25480
25587
  const triggerGitnexusAnalyzeIfNeeded = (sha, source) => {
@@ -25491,12 +25598,10 @@ class Supervisor {
25491
25598
  appendTimelineEvent(createMetaEvent("gitnexus_analyze_start_failed", `${source}: ${String(err?.message ?? err)}`));
25492
25599
  }
25493
25600
  };
25494
- const appendResultToInputBead = (params) => {
25495
- const inputBeadId = runOptions.inputBeadId;
25496
- const shouldAppendResultToInputBead = Boolean(shouldWriteExternalBeadNotes && inputBeadId && this.opts.beadsClient);
25497
- if (!shouldAppendResultToInputBead || !inputBeadId || !this.opts.beadsClient)
25601
+ const writeUnifiedHandoff = (params) => {
25602
+ if (!shouldPersistHandoffBlock({ output: params.output, notesMode, final: params.final }))
25498
25603
  return false;
25499
- const notes = formatBeadNotes({
25604
+ const rendered = formatHandoffBlock({
25500
25605
  output: params.output,
25501
25606
  promptHash: params.promptHash,
25502
25607
  durationMs: params.durationMs,
@@ -25505,24 +25610,42 @@ class Supervisor {
25505
25610
  specialist: runOptions.name,
25506
25611
  jobId: id,
25507
25612
  status: params.status,
25508
- timestamp: new Date().toISOString()
25509
- });
25510
- const appendResult = this.opts.beadsClient.updateBeadNotes(inputBeadId, notes);
25511
- if (appendResult.ok)
25512
- return true;
25513
- const appendError = `[bead-append-failed] ${appendResult.error ?? "Unknown error"}`;
25514
- appendTimelineEvent(createMetaEvent("bead_append_failed", appendError));
25515
- setStatus({ current_event: "bead_append_failed", last_event_at_ms: Date.now() });
25516
- if (this.isJobFileOutputEnabled) {
25517
- try {
25518
- appendFileSync(this.resultPath(id), `
25613
+ timestamp: new Date().toISOString(),
25614
+ tokenUsage: params.tokenUsage,
25615
+ turnIndex: params.turnIndex
25616
+ }, { final: params.final });
25617
+ const inputBeadId = runOptions.inputBeadId;
25618
+ if (shouldWriteExternalBeadNotes && inputBeadId && this.opts.beadsClient) {
25619
+ const noteText = notesMode === "final-only" && !params.final ? "" : rendered;
25620
+ if (noteText) {
25621
+ const appendResult = this.opts.beadsClient.updateBeadNotes(inputBeadId, noteText);
25622
+ if (!appendResult.ok) {
25623
+ const appendError = `[bead-append-failed] ${appendResult.error ?? "Unknown error"}`;
25624
+ appendTimelineEvent(createMetaEvent("bead_append_failed", appendError));
25625
+ setStatus({ current_event: "bead_append_failed", last_event_at_ms: Date.now() });
25626
+ if (this.isJobFileOutputEnabled) {
25627
+ try {
25628
+ appendFileSync(this.resultPath(id), `
25519
25629
 
25520
25630
  ${appendError}
25521
25631
  `, "utf-8");
25632
+ } catch {}
25633
+ }
25634
+ }
25635
+ }
25636
+ }
25637
+ if (outputFile) {
25638
+ try {
25639
+ if (notesMode === "final-only") {
25640
+ writeFileSync3(outputFile, rendered, "utf-8");
25641
+ } else {
25642
+ appendFileSync(outputFile, rendered, "utf-8");
25643
+ }
25522
25644
  } catch {}
25523
25645
  }
25524
- return false;
25646
+ return true;
25525
25647
  };
25648
+ const appendResultToInputBead = (params) => writeUnifiedHandoff(params);
25526
25649
  const applyAutoCommitCheckpoint = (target, autoCommitPolicy2) => {
25527
25650
  const autoCommitResult = runAutoCommitCheckpoint({
25528
25651
  autoCommitPolicy: autoCommitPolicy2,
@@ -25558,14 +25681,24 @@ ${appendError}
25558
25681
  return;
25559
25682
  const now = Date.now();
25560
25683
  lastActivityMs = now;
25684
+ const previousStatus = statusSnapshot.status;
25561
25685
  setStatus({ status: "running", current_event: "starting", last_event_at_ms: now });
25686
+ if (previousStatus !== "running") {
25687
+ appendTimelineEvent(createStatusChangeEvent("running", previousStatus));
25688
+ }
25689
+ appendTimelineEvent(createControlSignalEvent("resume_consumed", {
25690
+ source: "runtime",
25691
+ previous_status: previousStatus,
25692
+ next_status: "running",
25693
+ task_preview: task.replace(/\s+/g, " ").slice(0, 240)
25694
+ }));
25562
25695
  silenceWarnEmitted = false;
25563
25696
  try {
25564
25697
  const output = await resumeFn(task);
25565
25698
  latestOutput = output;
25566
25699
  if (this.isJobFileOutputEnabled) {
25567
25700
  mkdirSync4(this.jobDir(id), { recursive: true });
25568
- writeFileSync3(this.resultPath(id), output, "utf-8");
25701
+ writeFileSync3(this.resultPath(id), lastTurnSummaryTextContent || output, "utf-8");
25569
25702
  }
25570
25703
  try {
25571
25704
  this.withSqliteOperation("upsertResult:resume_turn", (client) => client.upsertResult(id, output));
@@ -25582,11 +25715,14 @@ ${appendError}
25582
25715
  const readOnlyClose = shouldAutoCloseReadOnlyKeepAlive(output);
25583
25716
  const isWaitingTurn = !readOnlyClose && !passFinalize;
25584
25717
  applyAutoCommitCheckpoint(isWaitingTurn ? "waiting" : "terminal", autoCommitPolicy);
25585
- appendResultToInputBead({
25718
+ writeUnifiedHandoff({
25586
25719
  output,
25587
25720
  model: statusSnapshot.model ?? "unknown",
25588
25721
  backend: statusSnapshot.backend ?? "unknown",
25589
- status: isWaitingTurn ? "waiting" : "done"
25722
+ status: isWaitingTurn ? "waiting" : "done",
25723
+ final: false,
25724
+ turnIndex: runMetrics.turns,
25725
+ tokenUsage: runMetrics.token_usage
25590
25726
  });
25591
25727
  if (!isWaitingTurn) {
25592
25728
  closeKeepAliveSession();
@@ -25663,11 +25799,14 @@ ${appendError}
25663
25799
  if (keepAliveSession) {
25664
25800
  const hasPendingKeepAliveOutput = Boolean(latestOutput && statusSnapshot.status === "waiting" && !shouldAutoCloseReadOnlyKeepAlive(latestOutput));
25665
25801
  if (hasPendingKeepAliveOutput) {
25666
- const appendSucceeded = appendResultToInputBead({
25802
+ const appendSucceeded = writeUnifiedHandoff({
25667
25803
  output: latestOutput,
25668
25804
  model: statusSnapshot.model ?? "unknown",
25669
25805
  backend: statusSnapshot.backend ?? "unknown",
25670
- status: "cancelled"
25806
+ status: "cancelled",
25807
+ final: false,
25808
+ turnIndex: runMetrics.turns,
25809
+ tokenUsage: runMetrics.token_usage
25671
25810
  });
25672
25811
  if (appendSucceeded) {
25673
25812
  skipFinalKeepAliveInputBeadAppend = true;
@@ -25679,7 +25818,7 @@ ${appendError}
25679
25818
  killFn?.();
25680
25819
  };
25681
25820
  process.once("SIGTERM", sigtermHandler);
25682
- const runOptionsWithBoundary = runOptions.workingDirectory ? { ...runOptions, worktreeBoundary: runOptions.workingDirectory } : runOptions;
25821
+ const runOptionsWithBoundary = runOptions.workingDirectory ? { ...runOptions, worktreeBoundary: runOptions.workingDirectory, suppressRunnerFileOutput: true } : { ...runOptions, suppressRunnerFileOutput: true };
25683
25822
  try {
25684
25823
  const result = await runner.run(runOptionsWithBoundary, (delta) => {
25685
25824
  const toolMatch = delta.match(/\u2699 (.+?)\u2026/);
@@ -25847,7 +25986,20 @@ ${appendError}
25847
25986
  context_pct: contextUtilization?.context_pct,
25848
25987
  context_health: contextUtilization?.context_health
25849
25988
  });
25989
+ lastTurnSummaryIndex = metricEvent.turn_index;
25990
+ lastTurnSummaryTextContent = turnTextAccumulator;
25850
25991
  appendTimelineEvent(createTurnSummaryEvent(metricEvent.turn_index, metricEvent.token_usage, metricEvent.finish_reason, turnTextAccumulator || undefined, contextUtilization?.context_pct, contextUtilization?.context_health));
25992
+ if (!keepAliveSession && !runOptions.keepAlive) {
25993
+ writeUnifiedHandoff({
25994
+ output: turnTextAccumulator,
25995
+ model: statusSnapshot.model ?? "unknown",
25996
+ backend: statusSnapshot.backend ?? "unknown",
25997
+ status: "done",
25998
+ final: false,
25999
+ turnIndex: metricEvent.turn_index,
26000
+ tokenUsage: metricEvent.token_usage ?? runMetrics.token_usage
26001
+ });
26002
+ }
25851
26003
  turnTextAccumulator = "";
25852
26004
  return;
25853
26005
  }
@@ -25891,16 +26043,33 @@ ${appendError}
25891
26043
  try {
25892
26044
  const parsed = JSON.parse(line);
25893
26045
  if (parsed?.type === "steer" && typeof parsed.message === "string") {
25894
- steerFn?.(parsed.message).catch(() => {});
26046
+ appendTimelineEvent(createControlSignalEvent("steer_consumed", {
26047
+ source: "runtime",
26048
+ previous_status: statusSnapshot.status,
26049
+ message_preview: parsed.message.replace(/\s+/g, " ").slice(0, 240)
26050
+ }));
26051
+ steerFn?.(parsed.message).catch((error2) => {
26052
+ appendTimelineEvent(createControlSignalEvent("steer_failed", {
26053
+ source: "runtime",
26054
+ previous_status: statusSnapshot.status,
26055
+ error_message: error2 instanceof Error ? error2.message : String(error2)
26056
+ }));
26057
+ });
25895
26058
  } else if (parsed?.type === "resume" && typeof parsed.task === "string") {
25896
26059
  handleResumeTurn(parsed.task);
25897
26060
  } else if (parsed?.type === "prompt" && typeof parsed.message === "string") {
25898
26061
  console.error('[specialists] DEPRECATED: FIFO message {type:"prompt"} is deprecated. Use {type:"resume", task:"..."} instead.');
25899
26062
  handleResumeTurn(parsed.message);
25900
26063
  } else if (parsed?.type === "close") {
26064
+ appendTimelineEvent(createControlSignalEvent("close_consumed", { source: "runtime", previous_status: statusSnapshot.status }));
25901
26065
  closeKeepAliveSession();
25902
26066
  }
25903
- } catch {}
26067
+ } catch (error2) {
26068
+ appendTimelineEvent(createControlSignalEvent("fifo_parse_error", {
26069
+ source: "runtime",
26070
+ error_message: error2 instanceof Error ? error2.message : String(error2)
26071
+ }));
26072
+ }
25904
26073
  });
25905
26074
  fifoReadline.on("error", (error2) => {
25906
26075
  console.error(`[supervisor] FIFO read error: ${String(error2)}`);
@@ -25976,7 +26145,7 @@ ${appendError}
25976
26145
  latestOutput = result.output;
25977
26146
  if (this.isJobFileOutputEnabled) {
25978
26147
  mkdirSync4(this.jobDir(id), { recursive: true });
25979
- writeFileSync3(this.resultPath(id), latestOutput, "utf-8");
26148
+ writeFileSync3(this.resultPath(id), lastTurnSummaryTextContent || latestOutput, "utf-8");
25980
26149
  }
25981
26150
  const elapsed = Math.round((Date.now() - startedAtMs) / 1000);
25982
26151
  const finalResult = {
@@ -26010,13 +26179,16 @@ ${appendError}
26010
26179
  } else if (shouldAutoFinalizeKeepAlive(finalResult.output)) {
26011
26180
  await closeKeepAliveSession();
26012
26181
  } else {
26013
- appendResultToInputBead({
26182
+ writeUnifiedHandoff({
26014
26183
  output: finalResult.output,
26015
26184
  model: finalResult.model,
26016
26185
  backend: finalResult.backend,
26017
26186
  status: "waiting",
26187
+ final: false,
26188
+ turnIndex: runMetrics.turns,
26018
26189
  promptHash: finalResult.promptHash,
26019
- durationMs: finalResult.durationMs
26190
+ durationMs: finalResult.durationMs,
26191
+ tokenUsage: finalResult.metrics?.token_usage
26020
26192
  });
26021
26193
  skipFinalKeepAliveInputBeadAppend = true;
26022
26194
  setWaitingStatus({
@@ -26035,18 +26207,21 @@ ${appendError}
26035
26207
  const appendedStatus = keepAliveSession && !shouldAutoCloseReadOnlyKeepAlive(finalResult.output) ? "waiting" : "done";
26036
26208
  const shouldSkipFinalInputBeadAppend = keepAliveSession && skipFinalKeepAliveInputBeadAppend;
26037
26209
  if (!shouldSkipFinalInputBeadAppend) {
26038
- appendResultToInputBead({
26039
- output: finalResult.output,
26210
+ writeUnifiedHandoff({
26211
+ output: lastTurnSummaryTextContent || finalResult.output,
26040
26212
  model: finalResult.model,
26041
26213
  backend: finalResult.backend,
26042
26214
  status: appendedStatus,
26215
+ final: true,
26216
+ turnIndex: lastTurnSummaryIndex || runMetrics.turns,
26043
26217
  promptHash: finalResult.promptHash,
26044
- durationMs: finalResult.durationMs
26218
+ durationMs: finalResult.durationMs,
26219
+ tokenUsage: finalResult.metrics?.token_usage
26045
26220
  });
26046
26221
  }
26047
26222
  if (ownsBead && finalResult.beadId) {
26048
- this.opts.beadsClient?.updateBeadNotes(finalResult.beadId, formatBeadNotes({
26049
- output: finalResult.output,
26223
+ this.opts.beadsClient?.updateBeadNotes(finalResult.beadId, formatHandoffBlock({
26224
+ output: lastTurnSummaryTextContent || finalResult.output,
26050
26225
  promptHash: finalResult.promptHash,
26051
26226
  durationMs: finalResult.durationMs,
26052
26227
  model: finalResult.model,
@@ -26054,11 +26229,13 @@ ${appendError}
26054
26229
  specialist: runOptions.name,
26055
26230
  jobId: id,
26056
26231
  status: "done",
26057
- timestamp: new Date().toISOString()
26058
- }));
26232
+ timestamp: new Date().toISOString(),
26233
+ tokenUsage: finalResult.metrics?.token_usage,
26234
+ turnIndex: lastTurnSummaryIndex || runMetrics.turns
26235
+ }, { final: true }));
26059
26236
  } else if (shouldWriteExternalBeadNotes && !inputBeadId && finalResult.beadId) {
26060
- this.opts.beadsClient?.updateBeadNotes(finalResult.beadId, formatBeadNotes({
26061
- output: finalResult.output,
26237
+ this.opts.beadsClient?.updateBeadNotes(finalResult.beadId, formatHandoffBlock({
26238
+ output: lastTurnSummaryTextContent || finalResult.output,
26062
26239
  promptHash: finalResult.promptHash,
26063
26240
  durationMs: finalResult.durationMs,
26064
26241
  model: finalResult.model,
@@ -26066,8 +26243,10 @@ ${appendError}
26066
26243
  specialist: runOptions.name,
26067
26244
  jobId: id,
26068
26245
  status: "done",
26069
- timestamp: new Date().toISOString()
26070
- }));
26246
+ timestamp: new Date().toISOString(),
26247
+ tokenUsage: finalResult.metrics?.token_usage,
26248
+ turnIndex: lastTurnSummaryIndex || runMetrics.turns
26249
+ }, { final: true }));
26071
26250
  }
26072
26251
  if (finalResult.beadId) {
26073
26252
  const liveJobs = this.listLiveJobsForBead(finalResult.beadId).filter((liveJobId) => liveJobId !== id);
@@ -26171,7 +26350,10 @@ ${appendError}
26171
26350
  output: latestOutput || errorMsg,
26172
26351
  model: statusSnapshot.model ?? "unknown",
26173
26352
  backend: statusSnapshot.backend ?? "unknown",
26174
- status: "error"
26353
+ status: "error",
26354
+ final: false,
26355
+ turnIndex: runMetrics.turns,
26356
+ tokenUsage: runMetrics.token_usage
26175
26357
  });
26176
26358
  this.writeReadyMarker(id);
26177
26359
  throw err;
@@ -30132,11 +30314,13 @@ async function launchSpecialist(opts) {
30132
30314
  keepAlive: opts.args.keepAlive,
30133
30315
  noKeepAlive: opts.args.noKeepAlive,
30134
30316
  beadsWriteNotes: opts.beadsWriteNotes,
30317
+ notesMode: opts.specialist.specialist.notes_mode,
30135
30318
  forceJob: opts.args.forceJob,
30136
30319
  permissionRequired: opts.perm,
30137
30320
  workingDirectory: opts.workingDirectory,
30138
30321
  reusedFromJobId: opts.reusedFromJobId,
30139
- worktreeOwnerJobId: opts.worktreeOwnerJobId
30322
+ worktreeOwnerJobId: opts.worktreeOwnerJobId,
30323
+ output_file: opts.specialist.specialist.output_file
30140
30324
  },
30141
30325
  beadsClient: opts.beadsClient,
30142
30326
  stallDetection: opts.specialist.specialist.stall_detection,
@@ -39646,6 +39830,7 @@ var init_stdin_buffer = __esm(() => {
39646
39830
  import * as fs2 from "fs";
39647
39831
  import { createRequire as createRequire3 } from "module";
39648
39832
  import * as path2 from "path";
39833
+ import { fileURLToPath as fileURLToPath4 } from "url";
39649
39834
 
39650
39835
  class ProcessTerminal {
39651
39836
  wasRaw = false;
@@ -39730,17 +39915,23 @@ class ProcessTerminal {
39730
39915
  if (process.platform !== "win32")
39731
39916
  return;
39732
39917
  try {
39733
- const koffi = cjsRequire("koffi");
39734
- const k32 = koffi.load("kernel32.dll");
39735
- const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)");
39736
- const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)");
39737
- const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)");
39738
- const STD_INPUT_HANDLE = -10;
39739
- const ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
39740
- const handle = GetStdHandle(STD_INPUT_HANDLE);
39741
- const mode = new Uint32Array(1);
39742
- GetConsoleMode(handle, mode);
39743
- SetConsoleMode(handle, mode[0] | ENABLE_VIRTUAL_TERMINAL_INPUT);
39918
+ const arch = process.arch;
39919
+ if (arch !== "x64" && arch !== "arm64")
39920
+ return;
39921
+ const moduleDir = path2.dirname(fileURLToPath4(import.meta.url));
39922
+ const nativePath = path2.join("native", "win32", "prebuilds", `win32-${arch}`, "win32-console-mode.node");
39923
+ const candidates = [
39924
+ path2.join(moduleDir, "..", nativePath),
39925
+ path2.join(moduleDir, nativePath),
39926
+ path2.join(path2.dirname(process.execPath), nativePath)
39927
+ ];
39928
+ for (const modulePath of candidates) {
39929
+ try {
39930
+ const helper = cjsRequire(modulePath);
39931
+ helper.enableVirtualTerminalInput?.();
39932
+ return;
39933
+ } catch {}
39934
+ }
39744
39935
  } catch {}
39745
39936
  }
39746
39937
  async drainInput(maxMs = 1000, idleMs = 50) {
@@ -40309,6 +40500,16 @@ var init_control = __esm(() => {
40309
40500
  function formatTime(t) {
40310
40501
  return new Date(t).toISOString().slice(11, 19);
40311
40502
  }
40503
+ function formatDateTime(t) {
40504
+ const d = new Date(t);
40505
+ const yyyy = d.getFullYear();
40506
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
40507
+ const dd = String(d.getDate()).padStart(2, "0");
40508
+ const hh = String(d.getHours()).padStart(2, "0");
40509
+ const mi = String(d.getMinutes()).padStart(2, "0");
40510
+ const ss = String(d.getSeconds()).padStart(2, "0");
40511
+ return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`;
40512
+ }
40312
40513
  function formatElapsed(seconds) {
40313
40514
  if (seconds < 60)
40314
40515
  return `${seconds}s`;
@@ -40415,6 +40616,26 @@ function formatEventLine(event, options2) {
40415
40616
  } else if (event.type === "error") {
40416
40617
  detailParts.push(`source=${event.source}`);
40417
40618
  detailParts.push(`error=${event.error_message}`);
40619
+ } else if (event.type === "control_signal") {
40620
+ detailParts.push(`action=${event.action}`);
40621
+ detailParts.push(`source=${event.source}`);
40622
+ if (event.previous_status || event.next_status) {
40623
+ detailParts.push(`status=${event.previous_status ?? "?"}->${event.next_status ?? "?"}`);
40624
+ }
40625
+ if (event.pid !== undefined)
40626
+ detailParts.push(`pid=${event.pid}`);
40627
+ if (event.signal)
40628
+ detailParts.push(`signal=${event.signal}`);
40629
+ if (event.force !== undefined)
40630
+ detailParts.push(`force=${event.force}`);
40631
+ if (event.reason)
40632
+ detailParts.push(`reason=${event.reason}`);
40633
+ if (event.message_preview)
40634
+ detailParts.push(`message="${event.message_preview}"`);
40635
+ if (event.task_preview)
40636
+ detailParts.push(`task="${event.task_preview}"`);
40637
+ if (event.error_message)
40638
+ detailParts.push(`error=${event.error_message}`);
40418
40639
  } else if (event.type === "auto_commit_success" || event.type === "auto_commit_skipped" || event.type === "auto_commit_failed") {
40419
40640
  const status = event.type.replace("auto_commit_", "");
40420
40641
  detailParts.push(`status=${status}`);
@@ -40518,6 +40739,8 @@ function formatEventInline(event) {
40518
40739
  }
40519
40740
  case "stale_warning":
40520
40741
  return yellow10(`[warning] ${event.reason}: ${Math.round(event.silence_ms / 1000)}s silent`);
40742
+ case "control_signal":
40743
+ return dim9(`[control] ${event.action}`);
40521
40744
  case "error":
40522
40745
  return red2(`[error] ${event.source}: ${event.error_message}`);
40523
40746
  default:
@@ -40556,7 +40779,8 @@ var init_format_helpers = __esm(() => {
40556
40779
  error: "ERROR",
40557
40780
  auto_commit_success: "AUTO+",
40558
40781
  auto_commit_skipped: "AUTO-",
40559
- auto_commit_failed: "AUTO!"
40782
+ auto_commit_failed: "AUTO!",
40783
+ control_signal: "CTRL"
40560
40784
  };
40561
40785
  });
40562
40786
 
@@ -40614,7 +40838,7 @@ function createFinalizeSupervisor(jobsDir) {
40614
40838
  throw new Error("finalize supervisor runner is not used");
40615
40839
  } };
40616
40840
  const runOptions = {};
40617
- return new Supervisor({ runner, runOptions, jobsDir });
40841
+ return new Supervisor({ runner, runOptions, jobsDir, beadsClient: new BeadsClient });
40618
40842
  }
40619
40843
  function findReviewerPassInChain(supervisor, chainId) {
40620
40844
  for (const id of supervisor.listChainJobIds(chainId)) {
@@ -40628,13 +40852,27 @@ function findReviewerPassInChain(supervisor, chainId) {
40628
40852
  }
40629
40853
  async function stopJob(jobId, opts = {}) {
40630
40854
  const jobsDir = opts.jobsDir ?? resolveJobsDir(process.cwd());
40631
- const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir });
40855
+ const supervisor = new Supervisor({ runner: null, runOptions: null, jobsDir, beadsClient: new BeadsClient });
40632
40856
  try {
40633
40857
  const status = supervisor.readStatus(jobId);
40634
40858
  if (!status)
40635
40859
  throw new Error(`No job found: ${jobId}`);
40636
40860
  if (status.status === "done" || status.status === "error" || status.status === "cancelled") {
40637
40861
  process.stderr.write(`${dim10(`Job ${jobId} already finalized (${status.status}).`)}
40862
+ `);
40863
+ return;
40864
+ }
40865
+ let statusForBeadClose = status;
40866
+ let finalizedFromWaiting = false;
40867
+ if (status.status === "waiting" && status.bead_id) {
40868
+ const finalized = supervisor.finalizeWaitingJob(jobId);
40869
+ if (finalized) {
40870
+ statusForBeadClose = finalized;
40871
+ finalizedFromWaiting = true;
40872
+ }
40873
+ }
40874
+ if (!finalizedFromWaiting && (statusForBeadClose.status === "done" || statusForBeadClose.status === "error" || statusForBeadClose.status === "cancelled")) {
40875
+ process.stderr.write(`${dim10(`Job ${jobId} already finalized (${statusForBeadClose.status}).`)}
40638
40876
  `);
40639
40877
  return;
40640
40878
  }
@@ -40644,8 +40882,18 @@ async function stopJob(jobId, opts = {}) {
40644
40882
  const tmuxSession = status.tmux_session;
40645
40883
  const isAlreadyDead = !isProcessAlive(pid, status.started_at_ms);
40646
40884
  const force = opts.force ?? false;
40885
+ supervisor.emitControlEvent(jobId, "stop_requested", {
40886
+ source: "cli",
40887
+ pid,
40888
+ previous_status: status.status,
40889
+ force,
40890
+ reason: isAlreadyDead ? "pid_already_dead" : "operator_stop",
40891
+ signal: force ? "SIGTERM/SIGKILL" : "SIGTERM",
40892
+ tmux_session: tmuxSession
40893
+ });
40647
40894
  if (force && isAlreadyDead) {
40648
- supervisor.updateJobStatus(jobId, "error");
40895
+ supervisor.updateJobStatus(jobId, "error", `Force stop requested; PID ${pid} already dead`);
40896
+ supervisor.emitControlEvent(jobId, "stop_marked_error", { source: "cli", pid, previous_status: status.status, next_status: "error", force, reason: "pid_already_dead" });
40649
40897
  supervisor.aggregateJobMetricsBestEffort(jobId);
40650
40898
  tryKillProcessGroup(pid);
40651
40899
  process.stdout.write(`${green10("\u2713")} Marked ${jobId} as error (PID ${pid} already dead)
@@ -40653,15 +40901,18 @@ async function stopJob(jobId, opts = {}) {
40653
40901
  } else {
40654
40902
  const terminalStatus = resolveTerminalStatus(jobId);
40655
40903
  supervisor.updateJobStatus(jobId, terminalStatus);
40904
+ supervisor.emitControlEvent(jobId, "status_marked_before_signal", { source: "cli", pid, previous_status: status.status, next_status: terminalStatus, force });
40656
40905
  supervisor.aggregateJobMetricsBestEffort(jobId);
40657
40906
  try {
40658
40907
  process.kill(pid, "SIGTERM");
40908
+ supervisor.emitControlEvent(jobId, "signal_sent", { source: "cli", pid, signal: "SIGTERM", force, next_status: terminalStatus });
40659
40909
  process.stdout.write(`${green10("\u2713")} Marked ${jobId} as ${terminalStatus} and sent SIGTERM to PID ${pid}
40660
40910
  `);
40661
40911
  if (force) {
40662
40912
  const exited = await waitForProcessExit(pid, 5000);
40663
40913
  if (!exited) {
40664
- supervisor.updateJobStatus(jobId, "error");
40914
+ supervisor.updateJobStatus(jobId, "error", `Force stop escalated; PID ${pid} ignored SIGTERM`);
40915
+ supervisor.emitControlEvent(jobId, "force_stop_escalated", { source: "cli", pid, signal: "SIGKILL", previous_status: terminalStatus, next_status: "error", force: true, reason: "sigterm_timeout" });
40665
40916
  tryKillProcessGroup(pid);
40666
40917
  process.stderr.write(`${red3("Force stop:")} PID ${pid} ignored SIGTERM, marked ${jobId} as error and killed process group.
40667
40918
  `);
@@ -40670,7 +40921,8 @@ async function stopJob(jobId, opts = {}) {
40670
40921
  } catch (err) {
40671
40922
  if (err.code === "ESRCH") {
40672
40923
  if (force) {
40673
- supervisor.updateJobStatus(jobId, "error");
40924
+ supervisor.updateJobStatus(jobId, "error", `Force stop escalated; PID ${pid} ignored SIGTERM`);
40925
+ supervisor.emitControlEvent(jobId, "force_stop_escalated", { source: "cli", pid, signal: "SIGKILL", previous_status: terminalStatus, next_status: "error", force: true, reason: "sigterm_timeout" });
40674
40926
  tryKillProcessGroup(pid);
40675
40927
  process.stdout.write(`${green10("\u2713")} Marked ${jobId} as error (PID ${pid} already gone)
40676
40928
  `);
@@ -40684,12 +40936,13 @@ async function stopJob(jobId, opts = {}) {
40684
40936
  }
40685
40937
  }
40686
40938
  if (tmuxSession) {
40939
+ supervisor.emitControlEvent(jobId, "tmux_kill_requested", { source: "cli", pid, tmux_session: tmuxSession });
40687
40940
  killTmuxSession(tmuxSession);
40688
40941
  process.stdout.write(`${dim10(` tmux session ${tmuxSession} killed`)}
40689
40942
  `);
40690
40943
  }
40691
40944
  if (status.bead_id) {
40692
- const finalStatus = supervisor.readStatus(jobId)?.status ?? "cancelled";
40945
+ const finalStatus = supervisor.readStatus(jobId)?.status ?? statusForBeadClose.status ?? "cancelled";
40693
40946
  const beads = new BeadsClient;
40694
40947
  const liveJobs = supervisor.listLiveJobsForBead(status.bead_id).filter((liveJobId) => liveJobId !== jobId);
40695
40948
  if (opts.closeBeadAnyway || liveJobs.length === 0) {
@@ -40774,7 +41027,7 @@ async function appendBeadNote(beadId, text, opts = {}) {
40774
41027
  if (!beadId || !text)
40775
41028
  return { ok: false, error: "beads unavailable or empty payload" };
40776
41029
  return await new Promise((resolve9) => {
40777
- const child = spawn5("bd", ["update", beadId, "--notes", text], {
41030
+ const child = spawn5("bd", ["update", beadId, "--append-notes", text], {
40778
41031
  stdio: ["ignore", "ignore", "pipe"]
40779
41032
  });
40780
41033
  const timer = opts.timeoutMs ? setTimeout(() => {
@@ -47152,7 +47405,7 @@ var init_epic = __esm(() => {
47152
47405
  // src/cli/version-check.ts
47153
47406
  import { spawnSync as spawnSync16 } from "child_process";
47154
47407
  import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync13 } from "fs";
47155
- import { dirname as dirname11, join as join26 } from "path";
47408
+ import { dirname as dirname12, join as join26 } from "path";
47156
47409
  import { createRequire as createRequire4 } from "module";
47157
47410
  function readBundledPackageVersion(requireFn = require3) {
47158
47411
  for (const candidate of ["../package.json", "../../package.json"]) {
@@ -47186,7 +47439,7 @@ function readCachedVersionCheck() {
47186
47439
  return readCache();
47187
47440
  }
47188
47441
  function writeCache(cache) {
47189
- mkdirSync11(dirname11(CACHE_PATH), { recursive: true });
47442
+ mkdirSync11(dirname12(CACHE_PATH), { recursive: true });
47190
47443
  writeFileSync13(CACHE_PATH, `${JSON.stringify(cache, null, 2)}
47191
47444
  `, "utf8");
47192
47445
  }
@@ -49110,8 +49363,32 @@ function deriveStartupSnapshot(status, events) {
49110
49363
  return Object.keys(merged).length > 0 ? merged : null;
49111
49364
  }
49112
49365
  function deriveApiError(events) {
49113
- const errorEvent = [...events].reverse().find((event) => event.type === "error");
49114
- return errorEvent?.error_message ?? null;
49366
+ for (const event of [...events].reverse()) {
49367
+ if (event.type === "error")
49368
+ return event.error_message;
49369
+ if (event.type === "run_complete" && event.error)
49370
+ return event.error;
49371
+ if (event.type === "control_signal" && event.error_message)
49372
+ return event.error_message;
49373
+ }
49374
+ return null;
49375
+ }
49376
+ function deriveTerminalReason(events) {
49377
+ for (const event of [...events].reverse()) {
49378
+ if (event.type === "run_complete") {
49379
+ return event.error ?? event.exit_reason ?? event.status;
49380
+ }
49381
+ if (event.type === "control_signal") {
49382
+ return event.error_message ?? event.reason ?? event.action;
49383
+ }
49384
+ if (event.type === "status_change") {
49385
+ return `status ${event.previous_status ?? "?"} -> ${event.status}`;
49386
+ }
49387
+ }
49388
+ return null;
49389
+ }
49390
+ function logHint(jobId) {
49391
+ return ` Inspect with: specialists log ${jobId} --limit 200`;
49115
49392
  }
49116
49393
  function formatPayloadPreamble(payloadJson) {
49117
49394
  if (!payloadJson)
@@ -49269,7 +49546,7 @@ async function run18() {
49269
49546
  const apiError2 = status2.error ?? deriveApiError(events2);
49270
49547
  const output3 = readResultOutput();
49271
49548
  if (!output3) {
49272
- const message = apiError2 ? `Job ${jobId} failed: ${apiError2}` : `Result not found for job ${jobId}`;
49549
+ const message = apiError2 ? `Job ${jobId} failed: ${apiError2}.${logHint(jobId)}` : `Result not found for job ${jobId}.${logHint(jobId)}`;
49273
49550
  if (args.json) {
49274
49551
  emitJson(status2, null, message, startupContext2);
49275
49552
  } else {
@@ -49288,11 +49565,28 @@ async function run18() {
49288
49565
  }
49289
49566
  if (status2.status === "error") {
49290
49567
  const startupContext2 = deriveStartupSnapshot(status2, readTimelineEventsForResult(sqliteClient, jobsDir, jobId));
49291
- const message = `Job ${jobId} failed: ${status2.error ?? "unknown error"}`;
49568
+ const events2 = readTimelineEventsForResult(sqliteClient, jobsDir, jobId);
49569
+ const reason = status2.error ?? deriveApiError(events2) ?? deriveTerminalReason(events2) ?? "unknown error";
49570
+ const message = `Job ${jobId} failed: ${reason}.${logHint(jobId)}`;
49571
+ if (args.json) {
49572
+ emitJson(status2, null, message, startupContext2);
49573
+ } else {
49574
+ process.stderr.write(`${red4(`Job ${jobId} failed:`)} ${reason}
49575
+ ${dim12(logHint(jobId).trim())}
49576
+ `);
49577
+ }
49578
+ process.exit(1);
49579
+ }
49580
+ if (status2.status === "cancelled") {
49581
+ const events2 = readTimelineEventsForResult(sqliteClient, jobsDir, jobId);
49582
+ const startupContext2 = deriveStartupSnapshot(status2, events2);
49583
+ const reason = status2.error ?? deriveTerminalReason(events2) ?? "cancelled";
49584
+ const message = `Job ${jobId} cancelled: ${reason}.${logHint(jobId)}`;
49292
49585
  if (args.json) {
49293
49586
  emitJson(status2, null, message, startupContext2);
49294
49587
  } else {
49295
- process.stderr.write(`${red4(`Job ${jobId} failed:`)} ${status2.error ?? "unknown error"}
49588
+ process.stderr.write(`${red4(`Job ${jobId} cancelled:`)} ${reason}
49589
+ ${dim12(logHint(jobId).trim())}
49296
49590
  `);
49297
49591
  }
49298
49592
  process.exit(1);
@@ -49368,14 +49662,30 @@ async function run18() {
49368
49662
  }
49369
49663
  return;
49370
49664
  }
49665
+ if (status.status === "cancelled") {
49666
+ const events2 = readTimelineEventsForResult(sqliteClient, jobsDir, jobId);
49667
+ const startupContext2 = deriveStartupSnapshot(status, events2);
49668
+ const reason = status.error ?? deriveTerminalReason(events2) ?? "cancelled";
49669
+ const message = `Job ${jobId} cancelled: ${reason}.${logHint(jobId)}`;
49670
+ if (args.json) {
49671
+ emitJson(status, null, message, startupContext2);
49672
+ } else {
49673
+ process.stderr.write(`${red4(`Job ${jobId} cancelled:`)} ${reason}
49674
+ ${dim12(logHint(jobId).trim())}
49675
+ `);
49676
+ }
49677
+ process.exit(1);
49678
+ }
49371
49679
  if (status.status === "error") {
49372
49680
  const events2 = readTimelineEventsForResult(sqliteClient, jobsDir, jobId);
49373
49681
  const startupContext2 = deriveStartupSnapshot(status, events2);
49374
- const message = `Job ${jobId} failed: ${status.error ?? deriveApiError(events2) ?? "unknown error"}`;
49682
+ const reason = status.error ?? deriveApiError(events2) ?? deriveTerminalReason(events2) ?? "unknown error";
49683
+ const message = `Job ${jobId} failed: ${reason}.${logHint(jobId)}`;
49375
49684
  if (args.json) {
49376
49685
  emitJson(status, null, message, startupContext2);
49377
49686
  } else {
49378
- process.stderr.write(`${red4(`Job ${jobId} failed:`)} ${status.error ?? deriveApiError(events2) ?? "unknown error"}
49687
+ process.stderr.write(`${red4(`Job ${jobId} failed:`)} ${reason}
49688
+ ${dim12(logHint(jobId).trim())}
49379
49689
  `);
49380
49690
  }
49381
49691
  process.exit(1);
@@ -49384,7 +49694,7 @@ async function run18() {
49384
49694
  const apiError = status.error ?? deriveApiError(events);
49385
49695
  const output2 = readResultOutput();
49386
49696
  if (!output2) {
49387
- const message = apiError ? `Job ${jobId} failed: ${apiError}` : `Result not found for job ${jobId}`;
49697
+ const message = apiError ? `Job ${jobId} failed: ${apiError}.${logHint(jobId)}` : `Result not found for job ${jobId}.${logHint(jobId)}`;
49388
49698
  if (args.json) {
49389
49699
  emitJson(status, null, message);
49390
49700
  } else {
@@ -50250,13 +50560,482 @@ var init_feed2 = __esm(() => {
50250
50560
  init_format_helpers();
50251
50561
  });
50252
50562
 
50563
+ // src/cli/log.ts
50564
+ var exports_log = {};
50565
+ __export(exports_log, {
50566
+ run: () => run20
50567
+ });
50568
+ import { existsSync as existsSync28, readdirSync as readdirSync14, statSync as statSync7 } from "fs";
50569
+ import { basename as basename9, join as join32 } from "path";
50570
+ function parseSince2(value) {
50571
+ if (value.includes("T") || value.includes("-"))
50572
+ return new Date(value).getTime();
50573
+ const match = value.match(/^(\d+)([smhd])$/);
50574
+ if (!match)
50575
+ return;
50576
+ const n = Number(match[1]);
50577
+ const unit = match[2];
50578
+ const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
50579
+ return Date.now() - n * ms[unit];
50580
+ }
50581
+ function parseArgs12(argv) {
50582
+ let jobId;
50583
+ let specialist;
50584
+ let beadId;
50585
+ let nodeId;
50586
+ let repo;
50587
+ let since;
50588
+ let limit = 200;
50589
+ let follow2 = false;
50590
+ let json = false;
50591
+ let allEvents = false;
50592
+ for (let i = 0;i < argv.length; i += 1) {
50593
+ const token = argv[i];
50594
+ if ((token === "--job" || token === "--job-id") && argv[i + 1]) {
50595
+ jobId = argv[++i];
50596
+ continue;
50597
+ }
50598
+ if (token === "--specialist" && argv[i + 1]) {
50599
+ specialist = argv[++i];
50600
+ continue;
50601
+ }
50602
+ if ((token === "--bead" || token === "--bead-id") && argv[i + 1]) {
50603
+ beadId = argv[++i];
50604
+ continue;
50605
+ }
50606
+ if (token === "--node" && argv[i + 1]) {
50607
+ nodeId = argv[++i];
50608
+ continue;
50609
+ }
50610
+ if (token === "--repo" && argv[i + 1]) {
50611
+ repo = argv[++i];
50612
+ continue;
50613
+ }
50614
+ if (token === "--since" && argv[i + 1]) {
50615
+ since = parseSince2(argv[++i]);
50616
+ continue;
50617
+ }
50618
+ if (token === "--limit" && argv[i + 1]) {
50619
+ limit = Math.max(1, Number(argv[++i]) || limit);
50620
+ continue;
50621
+ }
50622
+ if (token === "--follow" || token === "-f") {
50623
+ follow2 = true;
50624
+ continue;
50625
+ }
50626
+ if (token === "--json") {
50627
+ json = true;
50628
+ continue;
50629
+ }
50630
+ if (token === "--all-events" || token === "--verbose") {
50631
+ allEvents = true;
50632
+ continue;
50633
+ }
50634
+ if (!token.startsWith("-") && !jobId) {
50635
+ jobId = token;
50636
+ continue;
50637
+ }
50638
+ throw new Error(`Unknown option: ${token}`);
50639
+ }
50640
+ return { jobId, specialist, beadId, nodeId, repo, since, limit, follow: follow2, json, allEvents };
50641
+ }
50642
+ function discoverDbTargets(cwd, repoFilter) {
50643
+ const cwdLocation = resolveObservabilityDbLocation(cwd);
50644
+ if (existsSync28(cwdLocation.dbPath)) {
50645
+ const repo = basename9(cwdLocation.gitRoot);
50646
+ if (!repoFilter || repo === repoFilter) {
50647
+ return [{ repo, root: cwdLocation.gitRoot, dbPath: cwdLocation.dbPath, source: "cwd" }];
50648
+ }
50649
+ return [];
50650
+ }
50651
+ const targets = [];
50652
+ let entries = [];
50653
+ try {
50654
+ entries = readdirSync14(cwd);
50655
+ } catch {
50656
+ return [];
50657
+ }
50658
+ for (const entry of entries) {
50659
+ const root = join32(cwd, entry);
50660
+ try {
50661
+ if (!statSync7(root).isDirectory())
50662
+ continue;
50663
+ } catch {
50664
+ continue;
50665
+ }
50666
+ const dbPath = join32(root, ".specialists", "db", "observability.db");
50667
+ if (!existsSync28(dbPath))
50668
+ continue;
50669
+ if (repoFilter && entry !== repoFilter)
50670
+ continue;
50671
+ targets.push({ repo: entry, root, dbPath, source: "child" });
50672
+ }
50673
+ return targets.sort((a, b) => a.repo.localeCompare(b.repo));
50674
+ }
50675
+ function matches(status, options2) {
50676
+ if (options2.jobId && status.id !== options2.jobId)
50677
+ return false;
50678
+ if (options2.specialist && status.specialist !== options2.specialist)
50679
+ return false;
50680
+ if (options2.beadId && status.bead_id !== options2.beadId)
50681
+ return false;
50682
+ if (options2.nodeId && status.node_id !== options2.nodeId)
50683
+ return false;
50684
+ return true;
50685
+ }
50686
+ function isRuntimeEvent(event) {
50687
+ if (RUNTIME_EVENT_TYPES.has(event.type))
50688
+ return true;
50689
+ if (event.type === "meta") {
50690
+ return Boolean(event.model.startsWith("gitnexus_") || event.model.startsWith("bead_") || event.backend === "supervisor");
50691
+ }
50692
+ return false;
50693
+ }
50694
+ function toRows(target, statuses, options2, readEvents) {
50695
+ const rows = [];
50696
+ for (const status of statuses) {
50697
+ if (!matches(status, options2))
50698
+ continue;
50699
+ const rowPath = status.worktree_path ?? target.root;
50700
+ const events = readEvents(status.id).filter((event) => {
50701
+ if (options2.since !== undefined && event.t < options2.since)
50702
+ return false;
50703
+ if (options2.allEvents)
50704
+ return true;
50705
+ return isRuntimeEvent(event);
50706
+ });
50707
+ for (const event of events) {
50708
+ rows.push({
50709
+ jobId: status.id,
50710
+ specialist: status.specialist,
50711
+ beadId: status.bead_id,
50712
+ nodeId: status.node_id,
50713
+ repo: target.repo,
50714
+ path: rowPath,
50715
+ dbPath: target.dbPath,
50716
+ branch: status.branch,
50717
+ status: status.status,
50718
+ pid: status.pid,
50719
+ model: status.model,
50720
+ backend: status.backend,
50721
+ chainId: status.chain_id,
50722
+ chainRootJobId: status.chain_root_job_id,
50723
+ chainRootBeadId: status.chain_root_bead_id,
50724
+ event
50725
+ });
50726
+ }
50727
+ if (events.length === 0 && (options2.jobId || options2.beadId || options2.specialist || options2.nodeId)) {
50728
+ rows.push({
50729
+ jobId: status.id,
50730
+ specialist: status.specialist,
50731
+ beadId: status.bead_id,
50732
+ nodeId: status.node_id,
50733
+ repo: target.repo,
50734
+ path: rowPath,
50735
+ dbPath: target.dbPath,
50736
+ branch: status.branch,
50737
+ status: status.status,
50738
+ pid: status.pid,
50739
+ model: status.model,
50740
+ backend: status.backend,
50741
+ chainId: status.chain_id,
50742
+ chainRootJobId: status.chain_root_job_id,
50743
+ chainRootBeadId: status.chain_root_bead_id,
50744
+ event: { t: status.last_event_at_ms ?? status.started_at_ms, type: "status_snapshot" }
50745
+ });
50746
+ }
50747
+ }
50748
+ rows.sort((a, b) => a.event.t - b.event.t || a.jobId.localeCompare(b.jobId) || (a.event.seq ?? 0) - (b.event.seq ?? 0));
50749
+ const displayRows = options2.json ? rows : collapseDisplayDuplicates(rows);
50750
+ return displayRows.slice(Math.max(0, displayRows.length - options2.limit));
50751
+ }
50752
+ function normalizedEventForDisplay(event) {
50753
+ const normalized = { ...event };
50754
+ delete normalized.t;
50755
+ delete normalized.seq;
50756
+ if (event.type === "run_complete")
50757
+ delete normalized.elapsed_s;
50758
+ return normalized;
50759
+ }
50760
+ function displayDuplicateKey(row) {
50761
+ return JSON.stringify({
50762
+ jobId: row.jobId,
50763
+ event: normalizedEventForDisplay(row.event)
50764
+ });
50765
+ }
50766
+ function collapseDisplayDuplicates(rows) {
50767
+ const collapsed = [];
50768
+ const lastByKey = new Map;
50769
+ for (const row of rows) {
50770
+ const key = displayDuplicateKey(row);
50771
+ const previous = lastByKey.get(key);
50772
+ if (previous && row.event.t - previous.event.t <= DISPLAY_DUPLICATE_WINDOW_MS)
50773
+ continue;
50774
+ collapsed.push(row);
50775
+ lastByKey.set(key, row);
50776
+ }
50777
+ return collapsed;
50778
+ }
50779
+ function compact(value, max = 240) {
50780
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
50781
+ const flat = (raw ?? "").replace(/\s+/g, " ").trim();
50782
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
50783
+ }
50784
+ function eventDetail(event) {
50785
+ if (event.type === "run_start")
50786
+ return "started";
50787
+ if (event.type === "control_signal") {
50788
+ return [
50789
+ `action=${event.action}`,
50790
+ `source=${event.source}`,
50791
+ event.previous_status || event.next_status ? `status=${event.previous_status ?? "?"}->${event.next_status ?? "?"}` : null,
50792
+ event.pid !== undefined ? `pid=${event.pid}` : null,
50793
+ event.signal ? `signal=${event.signal}` : null,
50794
+ event.force !== undefined ? `force=${event.force}` : null,
50795
+ event.reason ? `reason=${compact(event.reason, 160)}` : null,
50796
+ event.message_preview ? `message="${compact(event.message_preview, 180)}"` : null,
50797
+ event.task_preview ? `task="${compact(event.task_preview, 180)}"` : null,
50798
+ event.error_message ? `error=${compact(event.error_message, 180)}` : null
50799
+ ].filter(Boolean).join(" ");
50800
+ }
50801
+ if (event.type === "status_change") {
50802
+ return `status=${event.previous_status ?? "?"}->${event.status}`;
50803
+ }
50804
+ if (event.type === "run_complete") {
50805
+ return [
50806
+ `status=${event.status}`,
50807
+ `elapsed=${formatElapsed(event.elapsed_s)}`,
50808
+ event.exit_reason ? `exit=${event.exit_reason}` : null,
50809
+ event.finish_reason ? `finish=${event.finish_reason}` : null,
50810
+ event.error ? `error=${compact(event.error, 500)}` : null,
50811
+ ...formatTokenUsageSummary(event.token_usage ?? event.metrics?.token_usage),
50812
+ event.tool_calls ? `tools=${event.tool_calls.length}` : null
50813
+ ].filter(Boolean).join(" ");
50814
+ }
50815
+ if (event.type === "tool") {
50816
+ const args = event.args ? ` args=${compact(event.args, 300)}` : "";
50817
+ const result = event.result_summary ? ` result=${compact(event.result_summary, 300)}` : "";
50818
+ return `tool=${event.tool} phase=${event.phase}${event.is_error ? " error=true" : ""}${args}${result}`;
50819
+ }
50820
+ if (event.type === "error")
50821
+ return `source=${event.source} error=${compact(event.error_message, 500)}`;
50822
+ if (event.type === "meta")
50823
+ return `model=${event.model} backend=${event.backend}${event.source ? ` source=${event.source}` : ""}`;
50824
+ if (event.type === "stale_warning")
50825
+ return `reason=${event.reason} silence_ms=${event.silence_ms} threshold_ms=${event.threshold_ms}${event.tool ? ` tool=${event.tool}` : ""}`;
50826
+ if (event.type === "token_usage")
50827
+ return `${formatTokenUsageSummary(event.token_usage).join(" ")} source=${event.source}`;
50828
+ if (event.type === "turn_summary")
50829
+ return `turn=${event.turn_index}${event.finish_reason ? ` finish=${event.finish_reason}` : ""}${event.context_pct !== undefined ? ` context=${event.context_pct.toFixed(2)}%` : ""}${event.text_content ? ` text="${compact(event.text_content, 160)}"` : ""}`;
50830
+ if (event.type === "auto_commit_success" || event.type === "auto_commit_skipped" || event.type === "auto_commit_failed")
50831
+ return `reason=${event.reason ?? ""}${event.commit_sha ? ` commit=${event.commit_sha}` : ""}${event.committed_files ? ` files=${event.committed_files.join(",")}` : ""}`.trim();
50832
+ if (event.type === "finish_reason")
50833
+ return `reason=${event.finish_reason} source=${event.source}`;
50834
+ if (event.type === "compaction" || event.type === "retry")
50835
+ return `phase=${event.phase}`;
50836
+ if (event.type === "message")
50837
+ return `phase=${event.phase} role=${event.role}`;
50838
+ if (event.type === "turn")
50839
+ return `phase=${event.phase}`;
50840
+ return compact(event, 500);
50841
+ }
50842
+ function eventColor(event) {
50843
+ switch (event.type) {
50844
+ case "run_complete":
50845
+ return event.status === "ERROR" ? red2 : event.status === "CANCELLED" ? yellow10 : green9;
50846
+ case "stale_warning":
50847
+ case "control_signal":
50848
+ case "auto_commit_skipped":
50849
+ return yellow10;
50850
+ case "error":
50851
+ case "extension_error":
50852
+ case "auto_commit_failed":
50853
+ return red2;
50854
+ case "auto_commit_success":
50855
+ return green9;
50856
+ default:
50857
+ return dim9;
50858
+ }
50859
+ }
50860
+ function eventLabel(event) {
50861
+ const rawType = event.type;
50862
+ if (rawType === "status_snapshot")
50863
+ return "SNAP";
50864
+ switch (event.type) {
50865
+ case "run_start":
50866
+ return "START";
50867
+ case "run_complete":
50868
+ return event.status;
50869
+ case "status_change":
50870
+ return "STATUS";
50871
+ case "control_signal":
50872
+ return "CTRL";
50873
+ case "stale_warning":
50874
+ return "WARN";
50875
+ case "auto_commit_success":
50876
+ return "AUTO+";
50877
+ case "auto_commit_skipped":
50878
+ return "AUTO-";
50879
+ case "auto_commit_failed":
50880
+ return "AUTO!";
50881
+ default:
50882
+ return rawType.toUpperCase().slice(0, 6);
50883
+ }
50884
+ }
50885
+ function formatWorktree(row) {
50886
+ const base = row.path ? basename9(row.path) : row.repo ?? "-";
50887
+ if (!row.repo || base === row.repo)
50888
+ return base;
50889
+ return `${row.repo}/${base}`;
50890
+ }
50891
+ function statusColor2(status) {
50892
+ switch (status) {
50893
+ case "done":
50894
+ return green9;
50895
+ case "error":
50896
+ return red2;
50897
+ case "cancelled":
50898
+ return yellow10;
50899
+ case "running":
50900
+ return cyan6;
50901
+ case "waiting":
50902
+ return yellow10;
50903
+ case "starting":
50904
+ return yellow10;
50905
+ default:
50906
+ return dim9;
50907
+ }
50908
+ }
50909
+ function printRow(row, json) {
50910
+ if (json) {
50911
+ console.log(JSON.stringify({
50912
+ timestamp: new Date(row.event.t).toISOString(),
50913
+ job_id: row.jobId,
50914
+ specialist: row.specialist,
50915
+ bead_id: row.beadId ?? null,
50916
+ node_id: row.nodeId ?? null,
50917
+ repo: row.repo ?? null,
50918
+ path: row.path ?? null,
50919
+ db_path: row.dbPath ?? null,
50920
+ branch: row.branch ?? null,
50921
+ worktree: formatWorktree(row),
50922
+ status: row.status ?? null,
50923
+ pid: row.pid ?? null,
50924
+ model: row.model ?? null,
50925
+ backend: row.backend ?? null,
50926
+ chain_id: row.chainId ?? null,
50927
+ chain_root_job_id: row.chainRootJobId ?? null,
50928
+ chain_root_bead_id: row.chainRootBeadId ?? null,
50929
+ event: row.event
50930
+ }));
50931
+ return;
50932
+ }
50933
+ const color = eventColor(row.event);
50934
+ const label = color(bold11(eventLabel(row.event).padEnd(6)));
50935
+ const status = row.status ?? "-";
50936
+ const statusSegment = statusColor2(row.status)(`status=${status}`);
50937
+ const head = [
50938
+ dim9(formatDateTime(row.event.t)),
50939
+ label,
50940
+ row.jobId,
50941
+ bold11(row.specialist),
50942
+ dim9(`bead=${row.beadId ?? "-"}`),
50943
+ row.nodeId ? dim9(`node=${row.nodeId}`) : null,
50944
+ dim9(`worktree=${formatWorktree(row)}`),
50945
+ statusSegment,
50946
+ row.pid !== undefined ? dim9(`pid=${row.pid}`) : null
50947
+ ].filter(Boolean).join(" ");
50948
+ console.log(`${head} ${eventDetail(row.event)}`.trim());
50949
+ }
50950
+ async function run20(argv = process.argv.slice(3)) {
50951
+ let options2;
50952
+ try {
50953
+ options2 = parseArgs12(argv);
50954
+ } catch (error2) {
50955
+ console.error(error2 instanceof Error ? error2.message : String(error2));
50956
+ console.error("Usage: specialists|sp log [job-id] [--specialist <name>] [--bead <id>] [--node <id>] [--since <5m|iso>] [--limit <n>] [-f|--follow] [--json] [--all-events]");
50957
+ process.exit(1);
50958
+ }
50959
+ const targets = discoverDbTargets(process.cwd(), options2.repo);
50960
+ if (targets.length === 0) {
50961
+ const suffix = options2.repo ? ` for repo '${options2.repo}'` : "";
50962
+ console.error(`No specialists observability DB found${suffix}. Run from a repo root or a parent containing repos with .specialists/db/observability.db.`);
50963
+ process.exit(1);
50964
+ }
50965
+ const clients = targets.map((target) => ({ target, client: createObservabilitySqliteClientAtPath(target.dbPath) }));
50966
+ if (clients.every((entry) => !entry.client)) {
50967
+ console.error("Observability SQLite DB is unavailable. Run: specialists db setup");
50968
+ process.exit(1);
50969
+ }
50970
+ const printed = new Set;
50971
+ const printSnapshot2 = () => {
50972
+ const rows = clients.flatMap(({ target, client }) => {
50973
+ if (!client)
50974
+ return [];
50975
+ return toRows(target, client.listStatuses(), options2, (jobId) => client.readEvents(jobId));
50976
+ });
50977
+ rows.sort((a, b) => a.event.t - b.event.t || (a.repo ?? "").localeCompare(b.repo ?? "") || a.jobId.localeCompare(b.jobId) || (a.event.seq ?? 0) - (b.event.seq ?? 0));
50978
+ const limitedRows = rows.slice(Math.max(0, rows.length - options2.limit));
50979
+ for (const row of limitedRows) {
50980
+ const key = `${row.repo ?? ""}:${row.jobId}:${row.event.seq ?? row.event.t}:${row.event.type}`;
50981
+ if (printed.has(key))
50982
+ continue;
50983
+ printed.add(key);
50984
+ printRow(row, options2.json);
50985
+ }
50986
+ if (limitedRows.length === 0 && !options2.json && printed.size === 0)
50987
+ console.error("No matching specialist log rows.");
50988
+ };
50989
+ try {
50990
+ printSnapshot2();
50991
+ if (!options2.follow)
50992
+ return;
50993
+ if (!options2.json)
50994
+ console.error(`Following specialist runtime logs across ${targets.length} repo${targets.length === 1 ? "" : "s"}... (Ctrl+C to stop)`);
50995
+ await new Promise((resolve11) => {
50996
+ const interval = setInterval(printSnapshot2, 750);
50997
+ const stop = () => {
50998
+ clearInterval(interval);
50999
+ resolve11();
51000
+ };
51001
+ process.once("SIGINT", stop);
51002
+ process.once("SIGTERM", stop);
51003
+ });
51004
+ } finally {
51005
+ for (const { client } of clients)
51006
+ client?.close();
51007
+ }
51008
+ }
51009
+ var DISPLAY_DUPLICATE_WINDOW_MS = 2000, RUNTIME_EVENT_TYPES;
51010
+ var init_log = __esm(() => {
51011
+ init_observability_sqlite();
51012
+ init_observability_db();
51013
+ init_format_helpers();
51014
+ RUNTIME_EVENT_TYPES = new Set([
51015
+ "run_start",
51016
+ "run_complete",
51017
+ "status_change",
51018
+ "control_signal",
51019
+ "stale_warning",
51020
+ "error",
51021
+ "extension_error",
51022
+ "model_change",
51023
+ "retry",
51024
+ "compaction",
51025
+ "auto_commit_success",
51026
+ "auto_commit_skipped",
51027
+ "auto_commit_failed",
51028
+ "status_snapshot"
51029
+ ]);
51030
+ });
51031
+
50253
51032
  // src/cli/steer.ts
50254
51033
  var exports_steer = {};
50255
51034
  __export(exports_steer, {
50256
- run: () => run20
51035
+ run: () => run21
50257
51036
  });
50258
51037
  import { writeFileSync as writeFileSync14 } from "fs";
50259
- async function run20() {
51038
+ async function run21() {
50260
51039
  const jobId = process.argv[3];
50261
51040
  const message = process.argv[4];
50262
51041
  if (!jobId || !message) {
@@ -50287,6 +51066,12 @@ async function run20() {
50287
51066
  const payload = JSON.stringify({ type: "steer", message }) + `
50288
51067
  `;
50289
51068
  writeFileSync14(status.fifo_path, payload, { flag: "a" });
51069
+ supervisor.emitControlEvent(jobId, "steer_sent", {
51070
+ source: "cli",
51071
+ previous_status: status.status,
51072
+ fifo_path: status.fifo_path,
51073
+ message_preview: message.replace(/\s+/g, " ").slice(0, 240)
51074
+ });
50290
51075
  process.stdout.write(`${green11("\u2713")} Steer message sent to job ${jobId}
50291
51076
  `);
50292
51077
  } catch (err) {
@@ -50307,10 +51092,10 @@ var init_steer = __esm(() => {
50307
51092
  // src/cli/resume.ts
50308
51093
  var exports_resume = {};
50309
51094
  __export(exports_resume, {
50310
- run: () => run21
51095
+ run: () => run22
50311
51096
  });
50312
51097
  import { writeFileSync as writeFileSync15 } from "fs";
50313
- async function run21() {
51098
+ async function run22() {
50314
51099
  const jobId = process.argv[3];
50315
51100
  const task = process.argv[4];
50316
51101
  if (!jobId || !task) {
@@ -50337,19 +51122,31 @@ async function run21() {
50337
51122
  `);
50338
51123
  process.exit(1);
50339
51124
  }
50340
- try {
50341
- const payload = JSON.stringify({ type: "resume", task }) + `
51125
+ const payload = JSON.stringify({ type: "resume", task }) + `
50342
51126
  `;
51127
+ try {
50343
51128
  writeFileSync15(status.fifo_path, payload, { flag: "a" });
50344
- process.stdout.write(`${green12("\u2713")} Resume sent to job ${jobId}
50345
- `);
50346
- process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
50347
- `);
50348
51129
  } catch (err) {
50349
51130
  process.stderr.write(`${red6("Error:")} Failed to write to steer pipe: ${err?.message}
50350
51131
  `);
50351
51132
  process.exit(1);
50352
51133
  }
51134
+ try {
51135
+ supervisor.emitControlEvent(jobId, "resume_sent", {
51136
+ source: "cli",
51137
+ previous_status: status.status,
51138
+ next_status: "running",
51139
+ fifo_path: status.fifo_path,
51140
+ task_preview: task.replace(/\s+/g, " ").slice(0, 240)
51141
+ });
51142
+ } catch (err) {
51143
+ process.stderr.write(`Warning: resume delivered, but telemetry could not be recorded: ${err?.message ?? String(err)}
51144
+ `);
51145
+ }
51146
+ process.stdout.write(`${green12("\u2713")} Resume sent to job ${jobId}
51147
+ `);
51148
+ process.stdout.write(` Use 'specialists feed ${jobId} --follow' to watch the response.
51149
+ `);
50353
51150
  } finally {
50354
51151
  await supervisor.dispose();
50355
51152
  }
@@ -50363,21 +51160,21 @@ var init_resume = __esm(() => {
50363
51160
  // src/cli/follow-up.ts
50364
51161
  var exports_follow_up = {};
50365
51162
  __export(exports_follow_up, {
50366
- run: () => run22
51163
+ run: () => run23
50367
51164
  });
50368
- async function run22() {
51165
+ async function run23() {
50369
51166
  process.stderr.write("\x1B[33m\u26A0 DEPRECATED:\x1B[0m `specialists follow-up` is deprecated. Use `specialists resume` instead.\n\n");
50370
51167
  const { run: resumeRun } = await Promise.resolve().then(() => (init_resume(), exports_resume));
50371
51168
  return resumeRun();
50372
51169
  }
50373
51170
 
50374
51171
  // src/specialist/worktree-gc.ts
50375
- import { existsSync as existsSync28, readdirSync as readdirSync14, readFileSync as readFileSync28 } from "fs";
50376
- import { join as join32 } from "path";
51172
+ import { existsSync as existsSync29, readdirSync as readdirSync15, readFileSync as readFileSync28 } from "fs";
51173
+ import { join as join33 } from "path";
50377
51174
  import { spawnSync as spawnSync19 } from "child_process";
50378
51175
  function readJobStatus2(jobDir) {
50379
- const statusPath = join32(jobDir, "status.json");
50380
- if (!existsSync28(statusPath))
51176
+ const statusPath = join33(jobDir, "status.json");
51177
+ if (!existsSync29(statusPath))
50381
51178
  return null;
50382
51179
  try {
50383
51180
  return JSON.parse(readFileSync28(statusPath, "utf-8"));
@@ -50402,7 +51199,7 @@ function collectWorktreeGcCandidates(jobsDir) {
50402
51199
  const worktreePath = status.worktree_path;
50403
51200
  if (!worktreePath)
50404
51201
  return null;
50405
- if (!existsSync28(worktreePath))
51202
+ if (!existsSync29(worktreePath))
50406
51203
  return null;
50407
51204
  return {
50408
51205
  jobId: status.id,
@@ -50414,13 +51211,13 @@ function collectWorktreeGcCandidates(jobsDir) {
50414
51211
  }
50415
51212
  if (!getFileFallbackEnabled())
50416
51213
  return [];
50417
- if (!existsSync28(jobsDir))
51214
+ if (!existsSync29(jobsDir))
50418
51215
  return [];
50419
51216
  const candidates = [];
50420
- for (const entry of readdirSync14(jobsDir, { withFileTypes: true })) {
51217
+ for (const entry of readdirSync15(jobsDir, { withFileTypes: true })) {
50421
51218
  if (!entry.isDirectory())
50422
51219
  continue;
50423
- const status = readJobStatus2(join32(jobsDir, entry.name));
51220
+ const status = readJobStatus2(join33(jobsDir, entry.name));
50424
51221
  if (!status)
50425
51222
  continue;
50426
51223
  if (isActive(status.status))
@@ -50430,7 +51227,7 @@ function collectWorktreeGcCandidates(jobsDir) {
50430
51227
  const { worktree_path: worktreePath, branch } = status;
50431
51228
  if (!worktreePath)
50432
51229
  continue;
50433
- if (!existsSync28(worktreePath))
51230
+ if (!existsSync29(worktreePath))
50434
51231
  continue;
50435
51232
  candidates.push({
50436
51233
  jobId: status.id,
@@ -50474,10 +51271,10 @@ var init_worktree_gc = __esm(() => {
50474
51271
  // src/cli/clean.ts
50475
51272
  var exports_clean = {};
50476
51273
  __export(exports_clean, {
50477
- run: () => run23
51274
+ run: () => run24
50478
51275
  });
50479
- import { existsSync as existsSync29, readFileSync as readFileSync29, readdirSync as readdirSync15, rmSync as rmSync4, statSync as statSync7 } from "fs";
50480
- import { join as join33 } from "path";
51276
+ import { existsSync as existsSync30, readFileSync as readFileSync29, readdirSync as readdirSync16, rmSync as rmSync4, statSync as statSync8 } from "fs";
51277
+ import { join as join34 } from "path";
50481
51278
  function parseDuration2(raw) {
50482
51279
  const match = /^(\d+)(ms|s|m|h|d)$/i.exec(raw.trim());
50483
51280
  if (!match)
@@ -50631,16 +51428,16 @@ function parseOptions2(argv) {
50631
51428
  }
50632
51429
  function readDirectorySizeBytes(directoryPath) {
50633
51430
  let totalBytes = 0;
50634
- for (const entry of readdirSync15(directoryPath, { withFileTypes: true })) {
50635
- const entryPath = join33(directoryPath, entry.name);
50636
- const stats = statSync7(entryPath);
51431
+ for (const entry of readdirSync16(directoryPath, { withFileTypes: true })) {
51432
+ const entryPath = join34(directoryPath, entry.name);
51433
+ const stats = statSync8(entryPath);
50637
51434
  totalBytes += stats.isDirectory() ? readDirectorySizeBytes(entryPath) : stats.size;
50638
51435
  }
50639
51436
  return totalBytes;
50640
51437
  }
50641
51438
  function containsProtectedSqliteArtifact(directoryPath) {
50642
- for (const entry of readdirSync15(directoryPath, { withFileTypes: true })) {
50643
- const entryPath = join33(directoryPath, entry.name);
51439
+ for (const entry of readdirSync16(directoryPath, { withFileTypes: true })) {
51440
+ const entryPath = join34(directoryPath, entry.name);
50644
51441
  if (entry.isDirectory()) {
50645
51442
  if (containsProtectedSqliteArtifact(entryPath))
50646
51443
  return true;
@@ -50661,11 +51458,11 @@ function getJobTimestamps(status) {
50661
51458
  function readCompletedJobDirectory(baseDirectory, entry) {
50662
51459
  if (!entry.isDirectory())
50663
51460
  return null;
50664
- const directoryPath = join33(baseDirectory, entry.name);
51461
+ const directoryPath = join34(baseDirectory, entry.name);
50665
51462
  if (containsProtectedSqliteArtifact(directoryPath))
50666
51463
  return null;
50667
- const statusFilePath = join33(directoryPath, "status.json");
50668
- if (!existsSync29(statusFilePath))
51464
+ const statusFilePath = join34(directoryPath, "status.json");
51465
+ if (!existsSync30(statusFilePath))
50669
51466
  return null;
50670
51467
  let statusData;
50671
51468
  try {
@@ -50683,8 +51480,8 @@ function collectCompletedJobs(jobsDirectoryPath) {
50683
51480
  const statuses = sqliteClient?.listStatuses() ?? [];
50684
51481
  if (statuses.length > 0) {
50685
51482
  return statuses.filter((status) => COMPLETED_STATUSES.has(status.status)).map((status) => {
50686
- const directoryPath = join33(jobsDirectoryPath, status.id);
50687
- if (!existsSync29(directoryPath) || containsProtectedSqliteArtifact(directoryPath))
51483
+ const directoryPath = join34(jobsDirectoryPath, status.id);
51484
+ if (!existsSync30(directoryPath) || containsProtectedSqliteArtifact(directoryPath))
50688
51485
  return null;
50689
51486
  const { createdAtMs, completedAtMs } = getJobTimestamps(status);
50690
51487
  return { id: status.id, directoryPath, completedAtMs, createdAtMs, sizeBytes: readDirectorySizeBytes(directoryPath) };
@@ -50692,7 +51489,7 @@ function collectCompletedJobs(jobsDirectoryPath) {
50692
51489
  }
50693
51490
  if (process.env.SPECIALISTS_JOB_FILE_OUTPUT !== "on")
50694
51491
  return [];
50695
- return readdirSync15(jobsDirectoryPath, { withFileTypes: true }).map((entry) => readCompletedJobDirectory(jobsDirectoryPath, entry)).filter((job) => job !== null);
51492
+ return readdirSync16(jobsDirectoryPath, { withFileTypes: true }).map((entry) => readCompletedJobDirectory(jobsDirectoryPath, entry)).filter((job) => job !== null);
50696
51493
  }
50697
51494
  function selectJobsToRemove(completedJobs, options2, protectedJobIds) {
50698
51495
  const jobsByNewest = [...completedJobs].sort((left, right) => {
@@ -50962,7 +51759,7 @@ function removeStaleProcesses(statuses, dryRun) {
50962
51759
  }
50963
51760
  return updatedCount;
50964
51761
  }
50965
- async function run23() {
51762
+ async function run24() {
50966
51763
  let options2;
50967
51764
  try {
50968
51765
  options2 = parseOptions2(process.argv.slice(3));
@@ -51014,7 +51811,7 @@ async function run23() {
51014
51811
  console.log(`Hid ${hiddenCount} terminal row(s) from default ps.`);
51015
51812
  return;
51016
51813
  }
51017
- if (!existsSync29(jobsDirectoryPath)) {
51814
+ if (!existsSync30(jobsDirectoryPath)) {
51018
51815
  console.log("No jobs directory found.");
51019
51816
  return;
51020
51817
  }
@@ -51067,7 +51864,7 @@ var init_clean = __esm(() => {
51067
51864
  // src/cli/end.ts
51068
51865
  var exports_end = {};
51069
51866
  __export(exports_end, {
51070
- run: () => run24
51867
+ run: () => run25
51071
51868
  });
51072
51869
  import { spawnSync as spawnSync20 } from "child_process";
51073
51870
  function parseOptions3(argv) {
@@ -51151,7 +51948,7 @@ async function publishChain(beadId, options2) {
51151
51948
  console.log("Publication mode: direct merge");
51152
51949
  }
51153
51950
  }
51154
- async function run24() {
51951
+ async function run25() {
51155
51952
  let options2;
51156
51953
  try {
51157
51954
  options2 = parseOptions3(process.argv.slice(3));
@@ -51192,7 +51989,7 @@ var init_end = __esm(() => {
51192
51989
  // src/cli/stop.ts
51193
51990
  var exports_stop = {};
51194
51991
  __export(exports_stop, {
51195
- run: () => run25
51992
+ run: () => run26
51196
51993
  });
51197
51994
  function parseStopArgs(argv) {
51198
51995
  let jobId;
@@ -51215,7 +52012,7 @@ function parseStopArgs(argv) {
51215
52012
  }
51216
52013
  return { jobId, force, closeBeadAnyway };
51217
52014
  }
51218
- async function run25() {
52015
+ async function run26() {
51219
52016
  let parsed;
51220
52017
  try {
51221
52018
  parsed = parseStopArgs(process.argv.slice(3));
@@ -51243,13 +52040,13 @@ var init_stop = __esm(() => {
51243
52040
  // src/cli/finalize.ts
51244
52041
  var exports_finalize = {};
51245
52042
  __export(exports_finalize, {
51246
- run: () => run26
52043
+ run: () => run27
51247
52044
  });
51248
52045
  function parseFinalizeArgs(argv) {
51249
52046
  const jobId = argv.find((token) => !token.startsWith("-"));
51250
52047
  return { jobId };
51251
52048
  }
51252
- async function run26() {
52049
+ async function run27() {
51253
52050
  const parsed = parseFinalizeArgs(process.argv.slice(3));
51254
52051
  const jobId = parsed.jobId;
51255
52052
  if (!jobId) {
@@ -51271,9 +52068,9 @@ var init_finalize = __esm(() => {
51271
52068
  // src/cli/attach-tui.ts
51272
52069
  var exports_attach_tui = {};
51273
52070
  __export(exports_attach_tui, {
51274
- run: () => run27
52071
+ run: () => run28
51275
52072
  });
51276
- async function run27(target, deps = {}) {
52073
+ async function run28(target, deps = {}) {
51277
52074
  const piTui = await Promise.resolve().then(() => (init_dist2(), exports_dist));
51278
52075
  const { TUI: TUI2, ProcessTerminal: ProcessTerminal2, Container: Container2, Input: Input2, matchesKey: matchesKey2, Key: Key2 } = piTui;
51279
52076
  const terminal = new ProcessTerminal2;
@@ -51393,7 +52190,7 @@ var init_attach_tui = __esm(() => {
51393
52190
  // src/cli/attach.ts
51394
52191
  var exports_attach = {};
51395
52192
  __export(exports_attach, {
51396
- run: () => run28
52193
+ run: () => run29
51397
52194
  });
51398
52195
  import readline3 from "readline";
51399
52196
  function exitWithError(message) {
@@ -51510,7 +52307,7 @@ function pickTarget(targets) {
51510
52307
  render2();
51511
52308
  });
51512
52309
  }
51513
- async function run28(deps = {}) {
52310
+ async function run29(deps = {}) {
51514
52311
  const [jobId] = process.argv.slice(3);
51515
52312
  if (!jobId) {
51516
52313
  if (!process.stdout.isTTY || !process.stdin.isTTY)
@@ -51526,8 +52323,8 @@ async function run28(deps = {}) {
51526
52323
  }
51527
52324
  async function attachTarget(target, deps) {
51528
52325
  const runTui = deps.runTui ?? (async (resolvedTarget) => {
51529
- const { run: run29 } = await Promise.resolve().then(() => (init_attach_tui(), exports_attach_tui));
51530
- return run29(resolvedTarget, deps);
52326
+ const { run: run30 } = await Promise.resolve().then(() => (init_attach_tui(), exports_attach_tui));
52327
+ return run30(resolvedTarget, deps);
51531
52328
  });
51532
52329
  return runTui(target);
51533
52330
  }
@@ -51536,15 +52333,15 @@ var init_attach = __esm(() => {
51536
52333
  });
51537
52334
 
51538
52335
  // src/specialist/drift-detector.ts
51539
- import { existsSync as existsSync30, readFileSync as readFileSync30, readdirSync as readdirSync16, rmSync as rmSync5 } from "fs";
51540
- import { join as join34, resolve as resolve11, relative as relative3 } from "path";
52336
+ import { existsSync as existsSync31, readFileSync as readFileSync30, readdirSync as readdirSync17, rmSync as rmSync5 } from "fs";
52337
+ import { join as join35, resolve as resolve11, relative as relative3 } from "path";
51541
52338
  function listFiles(root) {
51542
- if (!existsSync30(root))
52339
+ if (!existsSync31(root))
51543
52340
  return [];
51544
52341
  const out = [];
51545
52342
  const visit2 = (dir) => {
51546
- for (const entry of readdirSync16(dir, { withFileTypes: true })) {
51547
- const full = join34(dir, entry.name);
52343
+ for (const entry of readdirSync17(dir, { withFileTypes: true })) {
52344
+ const full = join35(dir, entry.name);
51548
52345
  if (entry.isDirectory()) {
51549
52346
  visit2(full);
51550
52347
  continue;
@@ -51579,12 +52376,12 @@ function detectDriftForRepo(repoRoot) {
51579
52376
  { scope: "user", dir: resolve11(repoRoot, ".specialists/user") }
51580
52377
  ];
51581
52378
  for (const { scope, dir } of scopes) {
51582
- if (!existsSync30(dir))
52379
+ if (!existsSync31(dir))
51583
52380
  continue;
51584
52381
  for (const file of listFiles(dir)) {
51585
52382
  const rel = relPath(file, dir);
51586
- const canonicalPath = join34(asset.canonicalDir, rel);
51587
- if (!existsSync30(canonicalPath))
52383
+ const canonicalPath = join35(asset.canonicalDir, rel);
52384
+ if (!existsSync31(canonicalPath))
51588
52385
  continue;
51589
52386
  const bytesEqual = readFileSync30(file).equals(readFileSync30(canonicalPath));
51590
52387
  findings.push(makeFinding(repoRoot, asset.kind, scope, file, canonicalPath, bytesEqual));
@@ -51605,12 +52402,12 @@ function detectDriftUnderRoot(root) {
51605
52402
  repos.push({ root: dir, findings });
51606
52403
  return;
51607
52404
  }
51608
- for (const entry of readdirSync16(dir, { withFileTypes: true })) {
52405
+ for (const entry of readdirSync17(dir, { withFileTypes: true })) {
51609
52406
  if (!entry.isDirectory())
51610
52407
  continue;
51611
52408
  if (entry.name === "node_modules" || entry.name === ".git")
51612
52409
  continue;
51613
- visit2(join34(dir, entry.name));
52410
+ visit2(join35(dir, entry.name));
51614
52411
  }
51615
52412
  };
51616
52413
  visit2(resolve11(root));
@@ -51650,10 +52447,10 @@ var init_drift_detector = __esm(() => {
51650
52447
  // src/cli/prune-stale-defaults.ts
51651
52448
  var exports_prune_stale_defaults = {};
51652
52449
  __export(exports_prune_stale_defaults, {
51653
- run: () => run29
52450
+ run: () => run30
51654
52451
  });
51655
52452
  import { resolve as resolve12 } from "path";
51656
- function parseArgs12(argv) {
52453
+ function parseArgs13(argv) {
51657
52454
  let dryRun = false;
51658
52455
  let root = process.cwd();
51659
52456
  let help = false;
@@ -51690,8 +52487,8 @@ function printHelp() {
51690
52487
  console.log(" --keep-diverged Preserve diverged .specialists/default entries");
51691
52488
  console.log(" --root Repo root to scan");
51692
52489
  }
51693
- async function run29(argv = process.argv.slice(3)) {
51694
- const { dryRun, root, help, keepDiverged } = parseArgs12(argv);
52490
+ async function run30(argv = process.argv.slice(3)) {
52491
+ const { dryRun, root, help, keepDiverged } = parseArgs13(argv);
51695
52492
  if (help) {
51696
52493
  printHelp();
51697
52494
  return;
@@ -51722,7 +52519,7 @@ var init_prune_stale_defaults = __esm(() => {
51722
52519
  // src/cli/quickstart.ts
51723
52520
  var exports_quickstart = {};
51724
52521
  __export(exports_quickstart, {
51725
- run: () => run30
52522
+ run: () => run31
51726
52523
  });
51727
52524
  function section2(title) {
51728
52525
  const bar = "\u2500".repeat(60);
@@ -51736,7 +52533,7 @@ function cmd2(s) {
51736
52533
  function flag(s) {
51737
52534
  return green13(s);
51738
52535
  }
51739
- async function run30() {
52536
+ async function run31() {
51740
52537
  const lines = [
51741
52538
  "",
51742
52539
  bold12("specialists \xB7 Quick Start Guide"),
@@ -51958,7 +52755,7 @@ var bold12 = (s) => `\x1B[1m${s}\x1B[0m`, dim13 = (s) => `\x1B[2m${s}\x1B[0m`, y
51958
52755
  var exports_doctor = {};
51959
52756
  __export(exports_doctor, {
51960
52757
  setStatusError: () => setStatusError,
51961
- run: () => run31,
52758
+ run: () => run32,
51962
52759
  resolvePackageAssetDir: () => resolvePackageAssetDir,
51963
52760
  renderProcessSummary: () => renderProcessSummary,
51964
52761
  parseVersionTuple: () => parseVersionTuple,
@@ -51967,8 +52764,8 @@ __export(exports_doctor, {
51967
52764
  });
51968
52765
  import { createHash as createHash5 } from "crypto";
51969
52766
  import { spawnSync as spawnSync21 } from "child_process";
51970
- import { existsSync as existsSync31, lstatSync as lstatSync2, mkdirSync as mkdirSync12, readdirSync as readdirSync17, readFileSync as readFileSync31, readlinkSync as readlinkSync3, writeFileSync as writeFileSync16 } from "fs";
51971
- import { dirname as dirname12, join as join35, relative as relative4, resolve as resolve13 } from "path";
52767
+ import { existsSync as existsSync32, lstatSync as lstatSync2, mkdirSync as mkdirSync12, readdirSync as readdirSync18, readFileSync as readFileSync31, readlinkSync as readlinkSync3, writeFileSync as writeFileSync16 } from "fs";
52768
+ import { dirname as dirname13, join as join36, relative as relative4, resolve as resolve13 } from "path";
51972
52769
  function ok3(msg) {
51973
52770
  console.log(` ${green14("\u2713")} ${msg}`);
51974
52771
  }
@@ -51997,7 +52794,7 @@ function isInstalled3(bin) {
51997
52794
  return spawnSync21("which", [bin], { encoding: "utf8", timeout: 2000 }).status === 0;
51998
52795
  }
51999
52796
  function loadJson2(path3) {
52000
- if (!existsSync31(path3))
52797
+ if (!existsSync32(path3))
52001
52798
  return null;
52002
52799
  try {
52003
52800
  return JSON.parse(readFileSync31(path3, "utf8"));
@@ -52043,7 +52840,7 @@ function checkBd() {
52043
52840
  return false;
52044
52841
  }
52045
52842
  ok3(`bd installed ${dim14(sp("bd", ["--version"]).stdout || "")}`);
52046
- if (existsSync31(join35(CWD, ".beads")))
52843
+ if (existsSync32(join36(CWD, ".beads")))
52047
52844
  ok3(".beads/ present in project");
52048
52845
  else
52049
52846
  warn3(".beads/ not found in project");
@@ -52063,18 +52860,18 @@ function checkHooks() {
52063
52860
  section3("Claude Code hooks (2 expected)");
52064
52861
  let allPresent = true;
52065
52862
  for (const name of HOOK_NAMES) {
52066
- const canonicalPath = join35(HOOKS_DIR, name);
52067
- if (!existsSync31(canonicalPath)) {
52863
+ const canonicalPath = join36(HOOKS_DIR, name);
52864
+ if (!existsSync32(canonicalPath)) {
52068
52865
  fail4(`${relative4(CWD, canonicalPath)} ${red7("missing")}`);
52069
52866
  fix("specialists init");
52070
52867
  allPresent = false;
52071
52868
  } else {
52072
52869
  ok3(relative4(CWD, canonicalPath));
52073
52870
  }
52074
- const claudeHookPath = join35(CLAUDE_HOOKS_DIR, name);
52871
+ const claudeHookPath = join36(CLAUDE_HOOKS_DIR, name);
52075
52872
  const symlinkState = isSymlinkTo(claudeHookPath, canonicalPath);
52076
52873
  if (symlinkState.ok) {
52077
- ok3(`${relative4(CWD, claudeHookPath)} -> ${relative4(dirname12(claudeHookPath), canonicalPath)}`);
52874
+ ok3(`${relative4(CWD, claudeHookPath)} -> ${relative4(dirname13(claudeHookPath), canonicalPath)}`);
52078
52875
  continue;
52079
52876
  }
52080
52877
  allPresent = false;
@@ -52152,8 +52949,8 @@ function hashFile(path3) {
52152
52949
  function collectFileHashes(rootDir) {
52153
52950
  const hashes = new Map;
52154
52951
  const visit2 = (dir) => {
52155
- for (const entry of readdirSync17(dir, { withFileTypes: true })) {
52156
- const fullPath = join35(dir, entry.name);
52952
+ for (const entry of readdirSync18(dir, { withFileTypes: true })) {
52953
+ const fullPath = join36(dir, entry.name);
52157
52954
  if (entry.isDirectory()) {
52158
52955
  visit2(fullPath);
52159
52956
  continue;
@@ -52164,12 +52961,12 @@ function collectFileHashes(rootDir) {
52164
52961
  hashes.set(relPath2, hashFile(fullPath));
52165
52962
  }
52166
52963
  };
52167
- if (existsSync31(rootDir))
52964
+ if (existsSync32(rootDir))
52168
52965
  visit2(rootDir);
52169
52966
  return hashes;
52170
52967
  }
52171
52968
  function isSymlinkTo(linkPath, expectedTargetPath) {
52172
- if (!existsSync31(linkPath))
52969
+ if (!existsSync32(linkPath))
52173
52970
  return { ok: false, reason: "missing" };
52174
52971
  let stats;
52175
52972
  try {
@@ -52181,7 +52978,7 @@ function isSymlinkTo(linkPath, expectedTargetPath) {
52181
52978
  return { ok: false, reason: "not-symlink" };
52182
52979
  try {
52183
52980
  const rawTarget = readlinkSync3(linkPath);
52184
- const resolvedTarget = resolve13(dirname12(linkPath), rawTarget);
52981
+ const resolvedTarget = resolve13(dirname13(linkPath), rawTarget);
52185
52982
  const resolvedExpected = resolve13(expectedTargetPath);
52186
52983
  if (resolvedTarget !== resolvedExpected) {
52187
52984
  return { ok: false, reason: "wrong-target", target: rawTarget };
@@ -52192,7 +52989,7 @@ function isSymlinkTo(linkPath, expectedTargetPath) {
52192
52989
  }
52193
52990
  }
52194
52991
  function resolvePackageAssetDir(relativePath) {
52195
- return resolveCanonicalAssetDir(relativePath) ?? (existsSync31(join35(CWD, "config", relativePath)) ? join35(CWD, "config", relativePath) : null);
52992
+ return resolveCanonicalAssetDir(relativePath) ?? (existsSync32(join36(CWD, "config", relativePath)) ? join36(CWD, "config", relativePath) : null);
52196
52993
  }
52197
52994
  function checkSkillDrift() {
52198
52995
  section3("Category A package-live skill sync");
@@ -52202,7 +52999,7 @@ function checkSkillDrift() {
52202
52999
  fix("restore config/skills/ or install package assets");
52203
53000
  return false;
52204
53001
  }
52205
- if (!existsSync31(XTRM_DEFAULT_SKILLS_DIR)) {
53002
+ if (!existsSync32(XTRM_DEFAULT_SKILLS_DIR)) {
52206
53003
  fail4(".xtrm/skills/default/ missing");
52207
53004
  fix("specialists init --sync-skills");
52208
53005
  return false;
@@ -52242,11 +53039,11 @@ function checkSkillDrift() {
52242
53039
  }
52243
53040
  fix("specialists init --sync-skills");
52244
53041
  }
52245
- const defaultSkills = readdirSync17(XTRM_DEFAULT_SKILLS_DIR, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
53042
+ const defaultSkills = readdirSync18(XTRM_DEFAULT_SKILLS_DIR, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
52246
53043
  let linksOk = true;
52247
53044
  for (const skillName of defaultSkills) {
52248
- const activeLinkPath = join35(XTRM_ACTIVE_SKILLS_DIR, skillName);
52249
- const expectedTarget = join35(XTRM_DEFAULT_SKILLS_DIR, skillName);
53045
+ const activeLinkPath = join36(XTRM_ACTIVE_SKILLS_DIR, skillName);
53046
+ const expectedTarget = join36(XTRM_DEFAULT_SKILLS_DIR, skillName);
52250
53047
  const state = isSymlinkTo(activeLinkPath, expectedTarget);
52251
53048
  if (state.ok)
52252
53049
  continue;
@@ -52264,11 +53061,11 @@ function checkSkillDrift() {
52264
53061
  fix("specialists init --sync-skills");
52265
53062
  }
52266
53063
  const legacyActiveRoots = [
52267
- { scope: "claude", root: join35(XTRM_ACTIVE_SKILLS_DIR, "claude") },
52268
- { scope: "pi", root: join35(XTRM_ACTIVE_SKILLS_DIR, "pi") }
53064
+ { scope: "claude", root: join36(XTRM_ACTIVE_SKILLS_DIR, "claude") },
53065
+ { scope: "pi", root: join36(XTRM_ACTIVE_SKILLS_DIR, "pi") }
52269
53066
  ];
52270
53067
  for (const { root } of legacyActiveRoots) {
52271
- if (!existsSync31(root))
53068
+ if (!existsSync32(root))
52272
53069
  continue;
52273
53070
  if (isSymlinkTo(root, XTRM_ACTIVE_SKILLS_DIR).ok)
52274
53071
  continue;
@@ -52281,14 +53078,14 @@ function checkSkillDrift() {
52281
53078
  fix("specialists init --sync-skills");
52282
53079
  }
52283
53080
  const skillRootChecks = [
52284
- { root: join35(CLAUDE_DIR, "skills"), expected: XTRM_ACTIVE_SKILLS_DIR },
52285
- { root: join35(PI_DIR, "skills"), expected: XTRM_ACTIVE_SKILLS_DIR }
53081
+ { root: join36(CLAUDE_DIR, "skills"), expected: XTRM_ACTIVE_SKILLS_DIR },
53082
+ { root: join36(PI_DIR, "skills"), expected: XTRM_ACTIVE_SKILLS_DIR }
52286
53083
  ];
52287
53084
  let rootLinksOk = true;
52288
53085
  for (const check2 of skillRootChecks) {
52289
53086
  const state = isSymlinkTo(check2.root, check2.expected);
52290
53087
  if (state.ok) {
52291
- ok3(`${relative4(CWD, check2.root)} -> ${relative4(dirname12(check2.root), check2.expected)}`);
53088
+ ok3(`${relative4(CWD, check2.root)} -> ${relative4(dirname13(check2.root), check2.expected)}`);
52292
53089
  continue;
52293
53090
  }
52294
53091
  rootLinksOk = false;
@@ -52314,7 +53111,7 @@ function checkManagedMirror(label, canonicalRelativePath, mirrorDir, fixHint) {
52314
53111
  fix(fixHint);
52315
53112
  return false;
52316
53113
  }
52317
- if (!existsSync31(mirrorDir)) {
53114
+ if (!existsSync32(mirrorDir)) {
52318
53115
  fail4(`${label} mirror missing: ${relative4(CWD, mirrorDir)}`);
52319
53116
  fix(fixHint);
52320
53117
  return false;
@@ -52346,31 +53143,31 @@ function checkManagedMirror(label, canonicalRelativePath, mirrorDir, fixHint) {
52346
53143
  function checkManagedAssetMirrors() {
52347
53144
  section3("Category B xtrm-managed asset mirrors");
52348
53145
  const specialistsOk = checkManagedMirror("specialists", "specialists", DEFAULT_SPECIALISTS_DIR, "sp prune-stale-defaults --apply");
52349
- const rulesOk = checkManagedMirror("mandatory-rules", "mandatory-rules", join35(DEFAULT_SPECIALISTS_DIR, "mandatory-rules"), "sp prune-stale-defaults --apply");
52350
- const nodesOk = checkManagedMirror("nodes", "nodes", join35(DEFAULT_SPECIALISTS_DIR, "nodes"), "sp prune-stale-defaults --apply");
53146
+ const rulesOk = checkManagedMirror("mandatory-rules", "mandatory-rules", join36(DEFAULT_SPECIALISTS_DIR, "mandatory-rules"), "sp prune-stale-defaults --apply");
53147
+ const nodesOk = checkManagedMirror("nodes", "nodes", join36(DEFAULT_SPECIALISTS_DIR, "nodes"), "sp prune-stale-defaults --apply");
52351
53148
  return specialistsOk && rulesOk && nodesOk;
52352
53149
  }
52353
53150
  function checkUserOverlayDrift() {
52354
53151
  section3("User specialist overlays");
52355
- if (!existsSync31(USER_SPECIALISTS_DIR)) {
53152
+ if (!existsSync32(USER_SPECIALISTS_DIR)) {
52356
53153
  ok3("no user overlays present");
52357
53154
  return true;
52358
53155
  }
52359
- const overlays = readdirSync17(USER_SPECIALISTS_DIR).filter((name) => name.endsWith(".specialist.json"));
53156
+ const overlays = readdirSync18(USER_SPECIALISTS_DIR).filter((name) => name.endsWith(".specialist.json"));
52360
53157
  if (overlays.length === 0) {
52361
53158
  ok3("no user overlays present");
52362
53159
  return true;
52363
53160
  }
52364
53161
  let allOk = true;
52365
53162
  for (const name of overlays) {
52366
- const userPath = join35(USER_SPECIALISTS_DIR, name);
52367
- const defaultPath = join35(DEFAULT_SPECIALISTS_DIR, name);
53163
+ const userPath = join36(USER_SPECIALISTS_DIR, name);
53164
+ const defaultPath = join36(DEFAULT_SPECIALISTS_DIR, name);
52368
53165
  const userSpec = loadJson2(userPath);
52369
53166
  if (!userSpec) {
52370
53167
  warn3(`${name}: failed to parse \u2014 skipping drift check`);
52371
53168
  continue;
52372
53169
  }
52373
- if (!existsSync31(defaultPath)) {
53170
+ if (!existsSync32(defaultPath)) {
52374
53171
  ok3(`${name}: user-only overlay (no default to drift from)`);
52375
53172
  continue;
52376
53173
  }
@@ -52398,18 +53195,18 @@ function checkUserOverlayDrift() {
52398
53195
  }
52399
53196
  function checkRuntimeDirs() {
52400
53197
  section3(".specialists/ runtime directories");
52401
- const rootDir = join35(CWD, ".specialists");
52402
- const jobsDir = join35(rootDir, "jobs");
52403
- const readyDir = join35(rootDir, "ready");
53198
+ const rootDir = join36(CWD, ".specialists");
53199
+ const jobsDir = join36(rootDir, "jobs");
53200
+ const readyDir = join36(rootDir, "ready");
52404
53201
  let allOk = true;
52405
- if (!existsSync31(rootDir)) {
53202
+ if (!existsSync32(rootDir)) {
52406
53203
  warn3(".specialists/ not found in current project");
52407
53204
  fix("specialists init");
52408
53205
  allOk = false;
52409
53206
  } else {
52410
53207
  ok3(".specialists/ present");
52411
53208
  for (const [subDir, label] of [[jobsDir, "jobs"], [readyDir, "ready"]]) {
52412
- if (!existsSync31(subDir)) {
53209
+ if (!existsSync32(subDir)) {
52413
53210
  warn3(`.specialists/${label}/ missing \u2014 auto-creating`);
52414
53211
  mkdirSync12(subDir, { recursive: true });
52415
53212
  ok3(`.specialists/${label}/ created`);
@@ -52423,8 +53220,8 @@ function checkRuntimeDirs() {
52423
53220
  function checkClaudeMdFragments() {
52424
53221
  section3("CLAUDE.md fragments");
52425
53222
  const projectRoot = process.cwd();
52426
- const claudeMd = join35(projectRoot, "CLAUDE.md");
52427
- if (!existsSync31(claudeMd)) {
53223
+ const claudeMd = join36(projectRoot, "CLAUDE.md");
53224
+ if (!existsSync32(claudeMd)) {
52428
53225
  warn3("No CLAUDE.md in project root \u2014 skipping fragment check");
52429
53226
  return true;
52430
53227
  }
@@ -52592,7 +53389,7 @@ function cleanupProcesses(jobsDir, dryRun) {
52592
53389
  }
52593
53390
  let entries;
52594
53391
  try {
52595
- entries = readdirSync17(jobsDir);
53392
+ entries = readdirSync18(jobsDir);
52596
53393
  } catch {
52597
53394
  entries = [];
52598
53395
  }
@@ -52604,8 +53401,8 @@ function cleanupProcesses(jobsDir, dryRun) {
52604
53401
  zombieJobIds: []
52605
53402
  };
52606
53403
  for (const jobId of entries) {
52607
- const statusPath = join35(jobsDir, jobId, "status.json");
52608
- if (!existsSync31(statusPath))
53404
+ const statusPath = join36(jobsDir, jobId, "status.json");
53405
+ if (!existsSync32(statusPath))
52609
53406
  continue;
52610
53407
  try {
52611
53408
  const status = JSON.parse(readFileSync31(statusPath, "utf8"));
@@ -52693,8 +53490,8 @@ function resolveWatchdogMode() {
52693
53490
  function checkZombieJobs() {
52694
53491
  section3("Background jobs");
52695
53492
  hint(`watchdog mode: ${resolveWatchdogMode()}`);
52696
- const jobsDir = join35(CWD, ".specialists", "jobs");
52697
- if (!existsSync31(jobsDir)) {
53493
+ const jobsDir = join36(CWD, ".specialists", "jobs");
53494
+ if (!existsSync32(jobsDir)) {
52698
53495
  hint("No .specialists/jobs/ \u2014 skipping");
52699
53496
  return true;
52700
53497
  }
@@ -52712,7 +53509,7 @@ function checkZombieJobs() {
52712
53509
  }
52713
53510
  return result.zombies === 0;
52714
53511
  }
52715
- async function run31(argv = process.argv.slice(3)) {
53512
+ async function run32(argv = process.argv.slice(3)) {
52716
53513
  const subcommand = argv[0];
52717
53514
  if (subcommand === "orphans") {
52718
53515
  runDoctorOrphans();
@@ -52760,18 +53557,18 @@ var init_doctor = __esm(() => {
52760
53557
  init_drift_detector();
52761
53558
  init_version_check();
52762
53559
  CWD = process.cwd();
52763
- CLAUDE_DIR = join35(CWD, ".claude");
52764
- PI_DIR = join35(CWD, ".pi");
52765
- XTRM_SKILLS_DIR = join35(CWD, ".xtrm", "skills");
52766
- XTRM_DEFAULT_SKILLS_DIR = join35(XTRM_SKILLS_DIR, "default");
52767
- XTRM_ACTIVE_SKILLS_DIR = join35(XTRM_SKILLS_DIR, "active");
52768
- SPECIALISTS_DIR = join35(CWD, ".specialists");
52769
- DEFAULT_SPECIALISTS_DIR = join35(SPECIALISTS_DIR, "default");
52770
- USER_SPECIALISTS_DIR = join35(SPECIALISTS_DIR, "user");
52771
- HOOKS_DIR = join35(CWD, ".xtrm", "hooks", "specialists");
52772
- CLAUDE_HOOKS_DIR = join35(CLAUDE_DIR, "hooks");
52773
- SETTINGS_FILE = join35(CLAUDE_DIR, "settings.json");
52774
- MCP_FILE2 = join35(CWD, ".mcp.json");
53560
+ CLAUDE_DIR = join36(CWD, ".claude");
53561
+ PI_DIR = join36(CWD, ".pi");
53562
+ XTRM_SKILLS_DIR = join36(CWD, ".xtrm", "skills");
53563
+ XTRM_DEFAULT_SKILLS_DIR = join36(XTRM_SKILLS_DIR, "default");
53564
+ XTRM_ACTIVE_SKILLS_DIR = join36(XTRM_SKILLS_DIR, "active");
53565
+ SPECIALISTS_DIR = join36(CWD, ".specialists");
53566
+ DEFAULT_SPECIALISTS_DIR = join36(SPECIALISTS_DIR, "default");
53567
+ USER_SPECIALISTS_DIR = join36(SPECIALISTS_DIR, "user");
53568
+ HOOKS_DIR = join36(CWD, ".xtrm", "hooks", "specialists");
53569
+ CLAUDE_HOOKS_DIR = join36(CLAUDE_DIR, "hooks");
53570
+ SETTINGS_FILE = join36(CLAUDE_DIR, "settings.json");
53571
+ MCP_FILE2 = join36(CWD, ".mcp.json");
52775
53572
  HOOK_NAMES = [
52776
53573
  "specialists-complete.mjs",
52777
53574
  "specialists-session-start.mjs"
@@ -52781,9 +53578,9 @@ var init_doctor = __esm(() => {
52781
53578
  // src/cli/setup.ts
52782
53579
  var exports_setup = {};
52783
53580
  __export(exports_setup, {
52784
- run: () => run32
53581
+ run: () => run33
52785
53582
  });
52786
- async function run32() {
53583
+ async function run33() {
52787
53584
  console.log("");
52788
53585
  console.log(yellow13("\u26A0 DEPRECATED: `specialists setup` is deprecated"));
52789
53586
  console.log("");
@@ -52804,20 +53601,20 @@ async function run32() {
52804
53601
  var bold14 = (s) => `\x1B[1m${s}\x1B[0m`, yellow13 = (s) => `\x1B[33m${s}\x1B[0m`, dim15 = (s) => `\x1B[2m${s}\x1B[0m`;
52805
53602
 
52806
53603
  // src/cli/serve-hot-reload.ts
52807
- import { existsSync as existsSync32, readdirSync as readdirSync18, statSync as statSync8, watch as fsWatch } from "fs";
52808
- import { join as join36 } from "path";
53604
+ import { existsSync as existsSync33, readdirSync as readdirSync19, statSync as statSync9, watch as fsWatch } from "fs";
53605
+ import { join as join37 } from "path";
52809
53606
  function specialistNameFromFile(file) {
52810
53607
  const match = file.match(/^(.+)\.specialist\.(json|yaml)$/);
52811
53608
  return match ? match[1] : null;
52812
53609
  }
52813
53610
  function snapshotMtimes(dir) {
52814
53611
  const out = new Map;
52815
- if (!existsSync32(dir))
53612
+ if (!existsSync33(dir))
52816
53613
  return out;
52817
- const entries = readdirSync18(dir).filter((name) => specialistNameFromFile(name) !== null);
53614
+ const entries = readdirSync19(dir).filter((name) => specialistNameFromFile(name) !== null);
52818
53615
  for (const name of entries) {
52819
53616
  try {
52820
- out.set(name, statSync8(join36(dir, name)).mtimeMs);
53617
+ out.set(name, statSync9(join37(dir, name)).mtimeMs);
52821
53618
  } catch {}
52822
53619
  }
52823
53620
  return out;
@@ -52874,7 +53671,7 @@ function createUserDirWatcher(opts) {
52874
53671
  for (const file of changed)
52875
53672
  queue(file);
52876
53673
  }, opts.pollMs);
52877
- } else if (existsSync32(opts.userDir)) {
53674
+ } else if (existsSync33(opts.userDir)) {
52878
53675
  try {
52879
53676
  watcher = fsWatch(opts.userDir, { persistent: false }, (_eventType, filename) => {
52880
53677
  queue(filename ? String(filename) : null);
@@ -52906,7 +53703,7 @@ var init_serve_hot_reload = () => {};
52906
53703
  var exports_serve = {};
52907
53704
  __export(exports_serve, {
52908
53705
  startServe: () => startServe,
52909
- run: () => run33,
53706
+ run: () => run34,
52910
53707
  recordAuditFailure: () => recordAuditFailure,
52911
53708
  evaluateReadiness: () => evaluateReadiness2,
52912
53709
  createReadinessState: () => createReadinessState,
@@ -52917,9 +53714,9 @@ import { randomUUID as randomUUID3 } from "crypto";
52917
53714
  import { once } from "events";
52918
53715
  import { spawnSync as spawnSync22 } from "child_process";
52919
53716
  import { access, readdir as readdir2, readFile as readFile5, constants } from "fs/promises";
52920
- import { existsSync as existsSync33 } from "fs";
53717
+ import { existsSync as existsSync34 } from "fs";
52921
53718
  import { homedir as homedir5 } from "os";
52922
- import { join as join37 } from "path";
53719
+ import { join as join38 } from "path";
52923
53720
  function createReadinessState() {
52924
53721
  return { shuttingDown: false, auditFailures: [], dbWriteFailuresTotal: 0 };
52925
53722
  }
@@ -52935,7 +53732,7 @@ function pruneAuditFailures(state, now = Date.now()) {
52935
53732
  }
52936
53733
  }
52937
53734
  async function checkUserDirSpecs(userDir) {
52938
- if (!existsSync33(userDir))
53735
+ if (!existsSync34(userDir))
52939
53736
  return "empty";
52940
53737
  const entries = await readdir2(userDir).catch(() => []);
52941
53738
  const specFiles = entries.filter((name) => name.endsWith(".specialist.json") || name.endsWith(".specialist.yaml"));
@@ -52944,7 +53741,7 @@ async function checkUserDirSpecs(userDir) {
52944
53741
  let validCount = 0;
52945
53742
  for (const file of specFiles) {
52946
53743
  try {
52947
- const content = await readFile5(join37(userDir, file), "utf-8");
53744
+ const content = await readFile5(join38(userDir, file), "utf-8");
52948
53745
  const json = file.endsWith(".json") ? content : null;
52949
53746
  if (!json)
52950
53747
  continue;
@@ -52962,7 +53759,7 @@ async function evaluateReadiness2(opts) {
52962
53759
  if (opts.state.auditFailures.length > opts.auditFailureThreshold) {
52963
53760
  return { ready: false, reason: "degraded:audit" };
52964
53761
  }
52965
- const piConfigPath = opts.piConfigPath ?? join37(homedir5(), ".pi", "agent", "auth.json");
53762
+ const piConfigPath = opts.piConfigPath ?? join38(homedir5(), ".pi", "agent", "auth.json");
52966
53763
  try {
52967
53764
  await access(piConfigPath, constants.R_OK);
52968
53765
  } catch {
@@ -52983,7 +53780,7 @@ async function evaluateReadiness2(opts) {
52983
53780
  warning = canaryFailure;
52984
53781
  }
52985
53782
  }
52986
- const userDir = join37(opts.projectDir, ".specialists", "user");
53783
+ const userDir = join38(opts.projectDir, ".specialists", "user");
52987
53784
  const userDirResult = await checkUserDirSpecs(userDir);
52988
53785
  if (userDirResult === "empty")
52989
53786
  return { ready: false, reason: "empty_user_dir" };
@@ -52991,7 +53788,7 @@ async function evaluateReadiness2(opts) {
52991
53788
  return { ready: false, reason: "invalid_spec_in_user_dir" };
52992
53789
  return warning ? { ready: true, warning } : { ready: true };
52993
53790
  }
52994
- function parseArgs13(argv) {
53791
+ function parseArgs14(argv) {
52995
53792
  let port = 8000;
52996
53793
  let concurrency = 4;
52997
53794
  let queueTimeoutMs = 5000;
@@ -53095,7 +53892,7 @@ async function waitForSlot(limit, timeoutMs, getActive) {
53095
53892
  return true;
53096
53893
  }
53097
53894
  async function startServe(argv = process.argv.slice(3)) {
53098
- const args = parseArgs13(argv);
53895
+ const args = parseArgs14(argv);
53099
53896
  const loader = new SpecialistLoader({ projectDir: args.projectDir });
53100
53897
  const dbLocation = resolveObservabilityDbLocation(args.projectDir);
53101
53898
  const dbPath = args.dbPath ?? dbLocation.dbPath;
@@ -53104,7 +53901,7 @@ async function startServe(argv = process.argv.slice(3)) {
53104
53901
  return createObservabilitySqliteClient(args.projectDir);
53105
53902
  })();
53106
53903
  const readinessState = createReadinessState();
53107
- const userDir = join37(args.projectDir, ".specialists", "user");
53904
+ const userDir = join38(args.projectDir, ".specialists", "user");
53108
53905
  const hotReload = createUserDirWatcher({ loader, userDir, pollMs: args.reloadPollMs });
53109
53906
  let active = 0;
53110
53907
  const children = new Set;
@@ -53269,7 +54066,7 @@ async function startServe(argv = process.argv.slice(3)) {
53269
54066
  console.log(`sp serve listening on ${args.port}`);
53270
54067
  return { server, args, db, readinessState };
53271
54068
  }
53272
- async function run33(argv = process.argv.slice(3)) {
54069
+ async function run34(argv = process.argv.slice(3)) {
53273
54070
  await startServe(argv);
53274
54071
  }
53275
54072
  var AUDIT_WINDOW_MS = 60000, DEFAULT_REQUIRED_PI_FLAGS;
@@ -53287,8 +54084,8 @@ var init_serve = __esm(() => {
53287
54084
  var exports_script = {};
53288
54085
  __export(exports_script, {
53289
54086
  scriptCli: () => scriptCli,
53290
- run: () => run34,
53291
- parseArgs: () => parseArgs14,
54087
+ run: () => run35,
54088
+ parseArgs: () => parseArgs15,
53292
54089
  mapExitCode: () => mapExitCode
53293
54090
  });
53294
54091
  import { spawnSync as spawnSync23 } from "child_process";
@@ -53298,7 +54095,7 @@ function parseVar(entry) {
53298
54095
  throw new Error(`Invalid --vars entry: ${entry}`);
53299
54096
  return [entry.slice(0, index), entry.slice(index + 1)];
53300
54097
  }
53301
- function parseArgs14(argv) {
54098
+ function parseArgs15(argv) {
53302
54099
  if (argv.length === 0)
53303
54100
  throw new Error("Missing specialist name");
53304
54101
  const specialist = argv[0];
@@ -53404,8 +54201,8 @@ function runUnderLock(lockPath, argv) {
53404
54201
  return 75;
53405
54202
  return flock.status ?? 1;
53406
54203
  }
53407
- async function run34(argv = process.argv.slice(3)) {
53408
- const args = parseArgs14(argv);
54204
+ async function run35(argv = process.argv.slice(3)) {
54205
+ const args = parseArgs15(argv);
53409
54206
  if (args.singleInstance && !process.env.SP_SCRIPT_NO_LOCK) {
53410
54207
  process.exit(runUnderLock(args.singleInstance, argv));
53411
54208
  }
@@ -53422,19 +54219,19 @@ var scriptCli;
53422
54219
  var init_script = __esm(() => {
53423
54220
  init_loader();
53424
54221
  init_script_runner();
53425
- scriptCli = { parseArgs: parseArgs14, mapExitCode };
54222
+ scriptCli = { parseArgs: parseArgs15, mapExitCode };
53426
54223
  });
53427
54224
 
53428
54225
  // src/cli/help.ts
53429
54226
  var exports_help = {};
53430
54227
  __export(exports_help, {
53431
- run: () => run35
54228
+ run: () => run36
53432
54229
  });
53433
54230
  function formatCommands(entries) {
53434
54231
  const width = Math.max(...entries.map(([cmd3]) => cmd3.length));
53435
54232
  return entries.map(([cmd3, desc]) => ` ${cmd3.padEnd(width)} ${desc}`);
53436
54233
  }
53437
- async function run35() {
54234
+ async function run36() {
53438
54235
  const lines = [
53439
54236
  "",
53440
54237
  "Specialists lets you run project-scoped specialist agents with a bead-first workflow.",
@@ -53476,14 +54273,15 @@ async function run35() {
53476
54273
  " Async patterns",
53477
54274
  " MCP: use_specialist (foreground, returns result directly)",
53478
54275
  ' CLI: specialists run <name> --prompt "..." # job ID prints on stderr',
53479
- " specialists ps|feed|result <job-id> # observe/progress/final output",
54276
+ " specialists ps|feed|log|result <job-id> # observe/progress/debug/final output",
53480
54277
  ' Shell: specialists run <name> --prompt "..." & # native shell backgrounding',
53481
54278
  "",
53482
54279
  " Background workflow",
53483
54280
  ' specialists run executor --background --prompt "..." # \u2192 prints job-id',
53484
54281
  " specialists list --live # \u2192 interactive session picker",
53485
54282
  " specialists attach <job-id> # \u2192 legacy tmux attach only",
53486
- " specialists feed <job-id> --follow # \u2192 structured event stream",
54283
+ " specialists feed <job-id> --follow # \u2192 compact structured event stream",
54284
+ " specialists log <job-id> -f # \u2192 full runtime/control/error log",
53487
54285
  " specialists merge <bead-id> [--rebuild] # \u2192 publish standalone chain only",
53488
54286
  " specialists epic merge <epic-id> [--pr] # \u2192 publish epic-owned chains",
53489
54287
  " specialists end [--pr] # \u2192 session-close publish helper",
@@ -53507,7 +54305,8 @@ async function run35() {
53507
54305
  " specialists run debugger --bead unitAI-123",
53508
54306
  ' specialists run codebase-explorer --prompt "Map the CLI architecture"',
53509
54307
  " specialists ps abc123 --json # check job status",
53510
- " specialists feed -f # stream all job events",
54308
+ " specialists feed -f # stream compact job events",
54309
+ " specialists log --specialist reviewer -f # full runtime/debug log",
53511
54310
  " specialists chat explorer --bead unitAI-123 # live TUI: feed + result + input",
53512
54311
  ' specialists steer <job-id> "focus only on supervisor.ts"',
53513
54312
  ' specialists resume <job-id> "now write the fix"',
@@ -53530,7 +54329,8 @@ async function run35() {
53530
54329
  " specialists steer --help Mid-run steering details",
53531
54330
  " specialists resume --help Multi-turn keep-alive details",
53532
54331
  " specialists init --help Bootstrap behavior and workflow injection",
53533
- " specialists feed --help Job event streaming details",
54332
+ " specialists feed --help Compact job event streaming details",
54333
+ " specialists log --help Full runtime/control/error log details",
53534
54334
  " specialists chat <name> ... Interactive TUI launch/control surface",
53535
54335
  "",
53536
54336
  dim16("Project model: specialists are project-only; user-scope discovery is deprecated."),
@@ -53554,7 +54354,8 @@ var init_help = __esm(() => {
53554
54354
  ["release", "Deprecated alias: proxy to xt release prepare/publish"],
53555
54355
  ["node", "Run and inspect NodeSupervisor nodes (run/status)"],
53556
54356
  ["epic", "Epic lifecycle management: list/status/resolve wave-bound chain groups"],
53557
- ["feed", "Tail job events; use -f to follow all jobs"],
54357
+ ["feed", "Tail compact job events; use -f to follow all jobs"],
54358
+ ["log", "Full runtime logs with job/bead/repo/path metadata; -f follows control/error events"],
53558
54359
  ["chat", "Launch an interactive TUI: feed-style timeline, status row, result, and steer/resume input"],
53559
54360
  ["result", "Print final output of a completed job; --wait polls until done, --timeout <ms> sets a limit"],
53560
54361
  ["clean", "Clean job dirs, dashboard history (--ps), and orphan processes"],
@@ -61109,7 +61910,7 @@ var next = process.argv[3];
61109
61910
  function wantsHelp() {
61110
61911
  return next === "--help" || next === "-h";
61111
61912
  }
61112
- async function run36() {
61913
+ async function run37() {
61113
61914
  if (sub === "install") {
61114
61915
  if (wantsHelp()) {
61115
61916
  console.log([
@@ -61763,6 +62564,48 @@ async function run36() {
61763
62564
  const { run: handler } = await Promise.resolve().then(() => (init_feed2(), exports_feed));
61764
62565
  return handler();
61765
62566
  }
62567
+ if (sub === "log") {
62568
+ if (wantsHelp()) {
62569
+ console.log([
62570
+ "",
62571
+ "Usage: specialists log [job-id] [options]",
62572
+ " specialists log -f [--specialist <name>] [--bead <id>]",
62573
+ "",
62574
+ "Runtime-oriented specialist log stream. From a repo root it reads that repo;",
62575
+ "from a parent directory it aggregates child repos with observability DBs.",
62576
+ "Unlike feed, it does not suppress",
62577
+ "control/lifecycle rows and every row includes timestamp, job, specialist,",
62578
+ "bead, compact worktree, status, pid, and runtime event detail.",
62579
+ "",
62580
+ "Options:",
62581
+ " --job <id> Filter to one job id",
62582
+ " --specialist <name> Filter by specialist role",
62583
+ " --bead <id> Filter by bead id",
62584
+ " --node <id> Filter by node id",
62585
+ " --repo <name> In parent/global mode, filter to one child repo",
62586
+ " --since <5m|iso> Show rows after a relative/ISO timestamp",
62587
+ " --limit <n> Max rows per snapshot (default 200)",
62588
+ " -f, --follow Continue polling for new rows",
62589
+ " --json Emit NDJSON rows with full event payloads",
62590
+ " --all-events Include agent-internal feed events (tool/turn/text/etc.)",
62591
+ "",
62592
+ "Examples:",
62593
+ " specialists log 49adda",
62594
+ " specialists log --bead unitAI-123 --limit 500",
62595
+ " specialists log --specialist reviewer -f",
62596
+ " specialists log -f --json",
62597
+ "",
62598
+ "Default output is runtime-only; use --all-events for raw agent internals.",
62599
+ "Use feed for compact human progress; use log for debugging crashes,",
62600
+ "dispatch/resume/steer/stop signals, and terminal error provenance.",
62601
+ ""
62602
+ ].join(`
62603
+ `));
62604
+ return;
62605
+ }
62606
+ const { run: handler } = await Promise.resolve().then(() => (init_log(), exports_log));
62607
+ return handler();
62608
+ }
61766
62609
  if (sub === "steer") {
61767
62610
  if (wantsHelp()) {
61768
62611
  console.log([
@@ -62158,7 +63001,7 @@ Run 'specialists help' to see available commands.`);
62158
63001
  const server = new SpecialistsServer;
62159
63002
  await server.start();
62160
63003
  }
62161
- run36().catch((error2) => {
63004
+ run37().catch((error2) => {
62162
63005
  logger.error(`Fatal error: ${error2}`);
62163
63006
  process.exit(1);
62164
63007
  });