@jaggerxtrm/specialists 3.18.0 → 3.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/config/mandatory-rules/executor-delivery.md +4 -0
  2. package/config/mandatory-rules/test-runner-execution-scope.md +4 -0
  3. package/config/skills/using-specialists-v3/SKILL.md +80 -20
  4. package/dist/asset-contract.json +2 -2
  5. package/dist/index.js +919 -53
  6. package/dist/lib.js +177 -9
  7. package/dist/types/cli/doctor.d.ts.map +1 -1
  8. package/dist/types/cli/feed.d.ts.map +1 -1
  9. package/dist/types/cli/format-helpers.d.ts.map +1 -1
  10. package/dist/types/cli/merge.d.ts.map +1 -1
  11. package/dist/types/cli/ps.d.ts.map +1 -1
  12. package/dist/types/cli/run.d.ts +19 -0
  13. package/dist/types/cli/run.d.ts.map +1 -1
  14. package/dist/types/cli/status.d.ts.map +1 -1
  15. package/dist/types/pi/session.d.ts +1 -0
  16. package/dist/types/pi/session.d.ts.map +1 -1
  17. package/dist/types/specialist/dead-job-audit.d.ts +20 -0
  18. package/dist/types/specialist/dead-job-audit.d.ts.map +1 -0
  19. package/dist/types/specialist/launch.d.ts +8 -0
  20. package/dist/types/specialist/launch.d.ts.map +1 -1
  21. package/dist/types/specialist/observability-db.d.ts +1 -1
  22. package/dist/types/specialist/observability-sqlite.d.ts +76 -0
  23. package/dist/types/specialist/observability-sqlite.d.ts.map +1 -1
  24. package/dist/types/specialist/pr-drift-refresh.d.ts +17 -0
  25. package/dist/types/specialist/pr-drift-refresh.d.ts.map +1 -0
  26. package/dist/types/specialist/runner.d.ts +3 -0
  27. package/dist/types/specialist/runner.d.ts.map +1 -1
  28. package/dist/types/specialist/status-load.d.ts.map +1 -1
  29. package/dist/types/specialist/supervisor.d.ts +10 -0
  30. package/dist/types/specialist/supervisor.d.ts.map +1 -1
  31. package/dist/types/specialist/timeline-events.d.ts +2 -0
  32. package/dist/types/specialist/timeline-events.d.ts.map +1 -1
  33. package/docs/design/roadmap/specialists-roadmap.md +69 -1
  34. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -11712,8 +11712,36 @@ function initSchema(db) {
11712
11712
  migrateToV10(db);
11713
11713
  migrateToV11(db);
11714
11714
  migrateToV12(db);
11715
+ migrateToV13(db);
11715
11716
  verifyWalMode(db);
11716
11717
  }
11718
+ function migrateToV13(db) {
11719
+ const hasV13 = db.query("SELECT 1 FROM schema_version WHERE version = 13 LIMIT 1").get();
11720
+ const specialistJobsColumns = new Set(db.query("PRAGMA table_info(specialist_jobs)").all().map((column) => column.name).filter((name) => typeof name === "string" && name.length > 0));
11721
+ for (const column of [
11722
+ { name: "pr_url", definition: "TEXT" },
11723
+ { name: "pr_head_sha", definition: "TEXT" },
11724
+ { name: "pr_state", definition: "TEXT" },
11725
+ { name: "pr_merge_state", definition: "TEXT" },
11726
+ { name: "pr_classification", definition: "TEXT" },
11727
+ { name: "pr_base_ref", definition: "TEXT" },
11728
+ { name: "pr_base_sha", definition: "TEXT" },
11729
+ { name: "pr_drift_checked_at_ms", definition: "INTEGER" },
11730
+ { name: "base_sha_pinned", definition: "TEXT" },
11731
+ { name: "base_sha_pinned_at_ms", definition: "INTEGER" }
11732
+ ]) {
11733
+ if (!specialistJobsColumns.has(column.name)) {
11734
+ db.run(`ALTER TABLE specialist_jobs ADD COLUMN ${column.name} ${column.definition}`);
11735
+ }
11736
+ }
11737
+ if (hasV13) {
11738
+ return;
11739
+ }
11740
+ db.run(`
11741
+ INSERT OR IGNORE INTO schema_version (version, applied_at_ms)
11742
+ VALUES (13, strftime('%s', 'now') * 1000);
11743
+ `);
11744
+ }
11717
11745
  function migrateToV5(db) {
11718
11746
  const hasV5 = db.query("SELECT 1 FROM schema_version WHERE version = 5 LIMIT 1").get();
11719
11747
  if (!hasV5) {
@@ -12624,6 +12652,124 @@ class SqliteClient {
12624
12652
  return statuses;
12625
12653
  }, "listStatuses");
12626
12654
  }
12655
+ readPrDriftState(jobId) {
12656
+ return withRetry(() => {
12657
+ const row = this.db.query(`
12658
+ SELECT pr_url, pr_head_sha, pr_state, pr_merge_state, pr_classification,
12659
+ pr_base_ref, pr_base_sha, pr_drift_checked_at_ms,
12660
+ base_sha_pinned, base_sha_pinned_at_ms
12661
+ FROM specialist_jobs WHERE job_id = ? LIMIT 1
12662
+ `).get(jobId);
12663
+ if (!row)
12664
+ return null;
12665
+ const num = (v) => v === null || v === undefined ? null : typeof v === "bigint" ? Number(v) : typeof v === "number" ? v : null;
12666
+ const str = (v) => v === null || v === undefined ? null : typeof v === "string" ? v : null;
12667
+ return {
12668
+ pr_url: str(row.pr_url),
12669
+ pr_head_sha: str(row.pr_head_sha),
12670
+ pr_state: str(row.pr_state),
12671
+ pr_merge_state: str(row.pr_merge_state),
12672
+ pr_classification: str(row.pr_classification),
12673
+ pr_base_ref: str(row.pr_base_ref),
12674
+ pr_base_sha: str(row.pr_base_sha),
12675
+ pr_drift_checked_at_ms: num(row.pr_drift_checked_at_ms),
12676
+ base_sha_pinned: str(row.base_sha_pinned),
12677
+ base_sha_pinned_at_ms: num(row.base_sha_pinned_at_ms)
12678
+ };
12679
+ }, "readPrDriftState");
12680
+ }
12681
+ updatePrDriftState(jobId, drift) {
12682
+ return withRetry(() => {
12683
+ const ALLOWED = [
12684
+ "pr_url",
12685
+ "pr_head_sha",
12686
+ "pr_state",
12687
+ "pr_merge_state",
12688
+ "pr_classification",
12689
+ "pr_base_ref",
12690
+ "pr_base_sha",
12691
+ "pr_drift_checked_at_ms",
12692
+ "base_sha_pinned",
12693
+ "base_sha_pinned_at_ms"
12694
+ ];
12695
+ const setClauses = [];
12696
+ const params = [];
12697
+ for (const key of ALLOWED) {
12698
+ if (!Object.prototype.hasOwnProperty.call(drift, key))
12699
+ continue;
12700
+ setClauses.push(`${key} = ?`);
12701
+ const value = drift[key];
12702
+ params.push(value === undefined ? null : value);
12703
+ }
12704
+ if (setClauses.length === 0)
12705
+ return false;
12706
+ setClauses.push("updated_at_ms = ?");
12707
+ params.push(Date.now());
12708
+ params.push(jobId);
12709
+ const sql = `UPDATE specialist_jobs SET ${setClauses.join(", ")} WHERE job_id = ?`;
12710
+ const result = this.db.run(sql, params);
12711
+ return (result?.changes ?? 0) > 0;
12712
+ }, "updatePrDriftState");
12713
+ }
12714
+ listStaleSpecialistJobs(opts) {
12715
+ return withRetry(() => {
12716
+ const nowMs = opts?.nowMs ?? Date.now();
12717
+ const minAgeMs = opts?.minAgeMs ?? 60000;
12718
+ const cutoff = nowMs - minAgeMs;
12719
+ const statuses = ["starting", "running", "waiting"];
12720
+ const placeholders = statuses.map(() => "?").join(", ");
12721
+ const rows = this.db.query(`
12722
+ SELECT job_id, specialist, status, status_json,
12723
+ JSON_EXTRACT(status_json, '$.pid') AS pid,
12724
+ updated_at_ms,
12725
+ bead_id,
12726
+ chain_id
12727
+ FROM specialist_jobs
12728
+ WHERE status IN (${placeholders})
12729
+ AND pid IS NOT NULL
12730
+ AND updated_at_ms < ?
12731
+ ORDER BY updated_at_ms ASC
12732
+ LIMIT 200
12733
+ `).all(...statuses, cutoff);
12734
+ const num = (v) => v === null || v === undefined ? 0 : typeof v === "bigint" ? Number(v) : typeof v === "number" ? v : 0;
12735
+ const str = (v) => v === null || v === undefined ? null : typeof v === "string" ? v : null;
12736
+ return rows.filter((row) => {
12737
+ const pid = num(row.pid);
12738
+ return Number.isInteger(pid) && pid > 0;
12739
+ }).map((row) => ({
12740
+ job_id: str(row.job_id) ?? "",
12741
+ specialist: str(row.specialist) ?? "",
12742
+ status: str(row.status) ?? "",
12743
+ pid: num(row.pid),
12744
+ updated_at_ms: num(row.updated_at_ms),
12745
+ bead_id: str(row.bead_id),
12746
+ chain_id: str(row.chain_id)
12747
+ }));
12748
+ }, "listStaleSpecialistJobs");
12749
+ }
12750
+ listJobsNeedingPrDriftRefresh(olderThanMs) {
12751
+ return withRetry(() => {
12752
+ const threshold = olderThanMs ?? Date.now() - 5 * 60 * 1000;
12753
+ const rows = this.db.query(`
12754
+ SELECT job_id, pr_url, pr_head_sha, pr_drift_checked_at_ms,
12755
+ JSON_EXTRACT(status_json, '$.branch') AS branch
12756
+ FROM specialist_jobs
12757
+ WHERE pr_url IS NOT NULL
12758
+ AND (pr_drift_checked_at_ms IS NULL OR pr_drift_checked_at_ms < ?)
12759
+ ORDER BY pr_drift_checked_at_ms ASC NULLS FIRST
12760
+ LIMIT 50
12761
+ `).all(threshold);
12762
+ const num = (v) => v === null || v === undefined ? null : typeof v === "bigint" ? Number(v) : typeof v === "number" ? v : null;
12763
+ const str = (v) => v === null || v === undefined ? null : typeof v === "string" ? v : null;
12764
+ return rows.map((row) => ({
12765
+ job_id: str(row.job_id) ?? "",
12766
+ pr_url: str(row.pr_url) ?? "",
12767
+ pr_head_sha: str(row.pr_head_sha),
12768
+ pr_drift_checked_at_ms: num(row.pr_drift_checked_at_ms),
12769
+ branch: str(row.branch)
12770
+ }));
12771
+ }, "listJobsNeedingPrDriftRefresh");
12772
+ }
12627
12773
  removeJobs(jobIds) {
12628
12774
  return withRetry(() => {
12629
12775
  if (jobIds.length === 0)
@@ -21956,6 +22102,24 @@ function findTokenUsage(payload) {
21956
22102
  }
21957
22103
  return normalizeTokenUsage(record3);
21958
22104
  }
22105
+ function extractMessageTextContent(message) {
22106
+ if (!message || typeof message !== "object")
22107
+ return "";
22108
+ const record3 = message;
22109
+ const content = record3.content;
22110
+ if (typeof content === "string")
22111
+ return content;
22112
+ if (!Array.isArray(content))
22113
+ return "";
22114
+ return content.map((part) => {
22115
+ if (!part || typeof part !== "object")
22116
+ return "";
22117
+ const item = part;
22118
+ if (item.type !== undefined && item.type !== "text")
22119
+ return "";
22120
+ return typeof item.text === "string" ? item.text : "";
22121
+ }).join("");
22122
+ }
21959
22123
  function findApiErrorMessage(payload) {
21960
22124
  if (!payload || typeof payload !== "object")
21961
22125
  return;
@@ -22231,6 +22395,9 @@ class PiAgentSession {
22231
22395
  const cavemanPath = join7(piExtDir, "caveman");
22232
22396
  if (existsSync7(cavemanPath))
22233
22397
  args.push("-e", cavemanPath);
22398
+ const nvidiaNimPath = join7(homedir2(), ".pi", "agent", "git", "github.com", "xRyul", "pi-nvidia-nim");
22399
+ if (existsSync7(nvidiaNimPath))
22400
+ args.push("-e", nvidiaNimPath);
22234
22401
  const npmGlobalDir = resolveGlobalNodeModulesDir();
22235
22402
  const excludedExtensions = new Set(this.options.excludeExtensions ?? []);
22236
22403
  if (npmGlobalDir) {
@@ -22446,7 +22613,8 @@ class PiAgentSession {
22446
22613
  if (type === "message_end") {
22447
22614
  const role = event.message?.role;
22448
22615
  if (role === "assistant") {
22449
- this.options.onEvent?.("message_end_assistant");
22616
+ const content = extractMessageTextContent(event.message);
22617
+ this.options.onEvent?.("message_end_assistant", content ? { content, charCount: content.length } : undefined);
22450
22618
  } else if (role === "toolResult") {
22451
22619
  this.options.onEvent?.("message_end_tool_result");
22452
22620
  }
@@ -22475,7 +22643,7 @@ class PiAgentSession {
22475
22643
  const messages = event.messages ?? [];
22476
22644
  const last = [...messages].reverse().find((m) => m.role === "assistant");
22477
22645
  if (last) {
22478
- this._lastOutput = last.content.filter((c) => c.type === "text").map((c) => c.text).join("");
22646
+ this._lastOutput = extractMessageTextContent(last);
22479
22647
  }
22480
22648
  this._updateTokenUsage(findTokenUsage(event), "agent_end");
22481
22649
  this._updateFinishReason(findFinishReason(event), "agent_end");
@@ -22487,7 +22655,11 @@ class PiAgentSession {
22487
22655
  }
22488
22656
  this._agentEndReceived = true;
22489
22657
  this._clearStallTimer();
22490
- this.options.onEvent?.("agent_end");
22658
+ if (this._lastOutput) {
22659
+ this.options.onEvent?.("agent_end", { content: this._lastOutput, charCount: this._lastOutput.length });
22660
+ } else {
22661
+ this.options.onEvent?.("agent_end");
22662
+ }
22491
22663
  this._doneResolve?.();
22492
22664
  return;
22493
22665
  }
@@ -22591,7 +22763,7 @@ class PiAgentSession {
22591
22763
  const delta = typeof ae.delta === "string" ? ae.delta : "";
22592
22764
  if (delta)
22593
22765
  this.options.onToken?.(delta);
22594
- this.options.onEvent?.("text", { charCount: delta.length });
22766
+ this.options.onEvent?.("text", { charCount: delta.length, content: delta });
22595
22767
  break;
22596
22768
  }
22597
22769
  case "thinking_start":
@@ -25346,11 +25518,7 @@ function mapCallbackEventToTimelineEvent(callbackEvent, context) {
25346
25518
  };
25347
25519
  }
25348
25520
  case "text":
25349
- return {
25350
- t,
25351
- type: TIMELINE_EVENT_TYPES.TEXT,
25352
- ...context.charCount !== undefined ? { char_count: context.charCount } : {}
25353
- };
25521
+ return null;
25354
25522
  case "agent_end":
25355
25523
  case "message_done":
25356
25524
  case "done":
@@ -27040,6 +27208,8 @@ class Supervisor {
27040
27208
  } : {},
27041
27209
  ...runOptions.variables?.chain_root_bead_id ? { chain_root_bead_id: runOptions.variables.chain_root_bead_id } : {},
27042
27210
  ...runOptions.workingDirectory ? { worktree_path: runOptions.workingDirectory } : {},
27211
+ ...runOptions.baseShaPinned ? { base_sha_pinned: runOptions.baseShaPinned } : {},
27212
+ ...runOptions.baseShaPinnedAtMs ? { base_sha_pinned_at_ms: runOptions.baseShaPinnedAtMs } : {},
27043
27213
  ...runOptions.workingDirectory ? { branch: resolveCurrentBranch(runOptions.workingDirectory) } : { branch: resolveCurrentBranch() },
27044
27214
  variables_keys: variablesKeys,
27045
27215
  reviewed_job_id_present: variablesKeys.includes("reviewed_job_id"),
@@ -27064,6 +27234,8 @@ class Supervisor {
27064
27234
  ...runOptions.workingDirectory ? { worktree_path: runOptions.workingDirectory } : {},
27065
27235
  ...runOptions.reusedFromJobId ? { reused_from_job_id: runOptions.reusedFromJobId } : {},
27066
27236
  ...runOptions.worktreeOwnerJobId ? { worktree_owner_job_id: runOptions.worktreeOwnerJobId } : {},
27237
+ ...runOptions.baseShaPinned ? { base_sha_pinned: runOptions.baseShaPinned } : {},
27238
+ ...runOptions.baseShaPinnedAtMs ? { base_sha_pinned_at_ms: runOptions.baseShaPinnedAtMs } : {},
27067
27239
  ...runOptions.worktreeOwnerJobId || runOptions.workingDirectory ? {
27068
27240
  chain_kind: "chain",
27069
27241
  chain_id: runOptions.worktreeOwnerJobId ?? id,
@@ -27168,7 +27340,6 @@ class Supervisor {
27168
27340
  execFileSync2("mkfifo", [fifoPath]);
27169
27341
  setStatus({ fifo_path: fifoPath });
27170
27342
  } catch {}
27171
- let textLogged = false;
27172
27343
  let runMetrics = {
27173
27344
  turns: 0,
27174
27345
  tool_calls: 0,
@@ -27184,6 +27355,8 @@ class Supervisor {
27184
27355
  let textCharCount = 0;
27185
27356
  let thinkingCharCount = 0;
27186
27357
  let turnTextAccumulator = "";
27358
+ let assistantMessageAccumulator = "";
27359
+ let lastPersistedAssistantMessage = "";
27187
27360
  let currentContextTokens = 0;
27188
27361
  const toolCallNames = [];
27189
27362
  const activeToolCalls = new Map;
@@ -27665,12 +27838,17 @@ ${appendError}
27665
27838
  textCharCount = 0;
27666
27839
  thinkingCharCount = 0;
27667
27840
  turnTextAccumulator = "";
27841
+ assistantMessageAccumulator = "";
27668
27842
  }
27669
27843
  if (eventType === "message_start_assistant") {
27670
27844
  turnTextAccumulator = "";
27845
+ assistantMessageAccumulator = "";
27671
27846
  }
27672
27847
  if (eventType === "text") {
27673
27848
  textCharCount += details?.charCount ?? 0;
27849
+ if (typeof details?.content === "string") {
27850
+ assistantMessageAccumulator += details.content;
27851
+ }
27674
27852
  }
27675
27853
  if (eventType === "thinking") {
27676
27854
  thinkingCharCount += details?.charCount ?? 0;
@@ -27753,6 +27931,17 @@ ${appendError}
27753
27931
  data: metaDetails?.data
27754
27932
  } : undefined
27755
27933
  });
27934
+ const assistantMessageContent = assistantMessageAccumulator || (typeof details?.content === "string" ? details.content : "");
27935
+ if ((eventType === "message_end_assistant" || eventType === "agent_end") && assistantMessageContent.trim().length > 0 && assistantMessageContent !== lastPersistedAssistantMessage) {
27936
+ appendTimelineEvent({
27937
+ t: Date.now(),
27938
+ type: TIMELINE_EVENT_TYPES.TEXT,
27939
+ char_count: assistantMessageContent.length,
27940
+ content: assistantMessageContent
27941
+ });
27942
+ lastPersistedAssistantMessage = assistantMessageContent;
27943
+ assistantMessageAccumulator = "";
27944
+ }
27756
27945
  if (timelineEvent) {
27757
27946
  appendTimelineEvent(timelineEvent);
27758
27947
  if (eventType === "tool_execution_end") {
@@ -27764,9 +27953,6 @@ ${appendError}
27764
27953
  const nextActiveTool = activeToolCalls.values().next().value?.tool;
27765
27954
  setStatus({ current_tool: nextActiveTool });
27766
27955
  }
27767
- } else if (eventType === "text" && !textLogged) {
27768
- textLogged = true;
27769
- appendTimelineEvent({ t: Date.now(), type: TIMELINE_EVENT_TYPES.TEXT });
27770
27956
  }
27771
27957
  }, (metricEvent) => {
27772
27958
  if (metricEvent.type === "token_usage") {
@@ -33027,6 +33213,8 @@ async function launchSpecialist(opts) {
33027
33213
  forceJob: opts.args.forceJob,
33028
33214
  permissionRequired: opts.perm,
33029
33215
  workingDirectory: opts.workingDirectory,
33216
+ baseShaPinned: opts.basePin?.baseShaPinned,
33217
+ baseShaPinnedAtMs: opts.basePin ? Date.now() : undefined,
33030
33218
  reusedFromJobId: opts.reusedFromJobId,
33031
33219
  worktreeOwnerJobId: opts.worktreeOwnerJobId,
33032
33220
  output_file: opts.specialist.specialist.output_file
@@ -33040,6 +33228,19 @@ async function launchSpecialist(opts) {
33040
33228
  `)) : undefined),
33041
33229
  onJobStarted: ({ id }) => {
33042
33230
  opts.onJobStarted?.({ id });
33231
+ if (opts.basePin) {
33232
+ const sqliteClient = createObservabilitySqliteClient(opts.workingDirectory);
33233
+ try {
33234
+ sqliteClient?.updatePrDriftState(id, {
33235
+ pr_base_ref: opts.basePin.branch,
33236
+ pr_base_sha: opts.basePin.baseShaObserved,
33237
+ base_sha_pinned: opts.basePin.baseShaPinned,
33238
+ base_sha_pinned_at_ms: Date.now()
33239
+ });
33240
+ } finally {
33241
+ sqliteClient?.close();
33242
+ }
33243
+ }
33043
33244
  process.stderr.write(dim8(`[job started: ${id}]
33044
33245
  `));
33045
33246
  const handoffPath = process.env.SPECIALISTS_BG_JOB_ID_PATH;
@@ -33114,6 +33315,7 @@ var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim8 = (s) => `\x1B[2m${s}\x1B[0m`, gr
33114
33315
  var init_launch = __esm(() => {
33115
33316
  init_supervisor();
33116
33317
  init_runner();
33318
+ init_observability_sqlite();
33117
33319
  });
33118
33320
 
33119
33321
  // node_modules/@earendil-works/pi-tui/dist/fuzzy.js
@@ -42933,6 +43135,9 @@ var init_feed = __esm(() => {
42933
43135
  // src/specialist/status-load.ts
42934
43136
  import { existsSync as existsSync20, readdirSync as readdirSync7, readFileSync as readFileSync18 } from "fs";
42935
43137
  import { join as join24 } from "path";
43138
+ function hasStatusId(status) {
43139
+ return typeof status.id === "string" && status.id.trim().length > 0;
43140
+ }
42936
43141
  function readStatusesFromFiles(jobsDir) {
42937
43142
  if (!existsSync20(jobsDir))
42938
43143
  return [];
@@ -42942,7 +43147,9 @@ function readStatusesFromFiles(jobsDir) {
42942
43147
  if (!existsSync20(statusPath))
42943
43148
  continue;
42944
43149
  try {
42945
- statuses.push(JSON.parse(readFileSync18(statusPath, "utf-8")));
43150
+ const status = JSON.parse(readFileSync18(statusPath, "utf-8"));
43151
+ if (hasStatusId(status))
43152
+ statuses.push(status);
42946
43153
  } catch {}
42947
43154
  }
42948
43155
  return statuses.sort((a, b) => b.started_at_ms - a.started_at_ms);
@@ -42990,27 +43197,148 @@ function enrichStatusesWithDerivedCurrentTool(statuses, jobsDir, sqliteClient) {
42990
43197
  current_tool: resolveDerivedCurrentTool(status, jobsDir, sqliteClient)
42991
43198
  }));
42992
43199
  }
43200
+ function statusFreshnessMs(status) {
43201
+ return Math.max(status.last_event_at_ms ?? 0, status.started_at_ms ?? 0);
43202
+ }
43203
+ function mergeStatuses(fileStatuses, sqliteStatuses) {
43204
+ const merged = new Map;
43205
+ for (const status of fileStatuses) {
43206
+ if (hasStatusId(status))
43207
+ merged.set(status.id, status);
43208
+ }
43209
+ for (const status of sqliteStatuses) {
43210
+ if (!hasStatusId(status))
43211
+ continue;
43212
+ const current = merged.get(status.id);
43213
+ if (!current || statusFreshnessMs(status) >= statusFreshnessMs(current)) {
43214
+ merged.set(status.id, status);
43215
+ }
43216
+ }
43217
+ return [...merged.values()];
43218
+ }
43219
+ function isActiveStatus(status) {
43220
+ return status.status === "starting" || status.status === "running" || status.status === "waiting";
43221
+ }
43222
+ function statusFromRunComplete(status) {
43223
+ if (status === "COMPLETE")
43224
+ return "done";
43225
+ if (status === "CANCELLED")
43226
+ return "cancelled";
43227
+ if (status === "ERROR")
43228
+ return "error";
43229
+ return;
43230
+ }
43231
+ function latestRunComplete(events) {
43232
+ for (let index = events.length - 1;index >= 0; index -= 1) {
43233
+ const event = events[index];
43234
+ if (event?.type === "run_complete")
43235
+ return event;
43236
+ }
43237
+ return;
43238
+ }
43239
+ function mergedMetrics(status, complete) {
43240
+ const tokenUsage = complete.token_usage ?? complete.metrics?.token_usage;
43241
+ const metrics = {
43242
+ ...status.metrics ?? {},
43243
+ ...complete.metrics ?? {},
43244
+ ...tokenUsage ? { token_usage: tokenUsage } : {},
43245
+ ...complete.finish_reason ? { finish_reason: complete.finish_reason } : {},
43246
+ ...complete.tool_calls ? { tool_call_names: complete.tool_calls, tool_calls: complete.tool_calls.length } : {},
43247
+ ...complete.exit_reason ? { exit_reason: complete.exit_reason } : {}
43248
+ };
43249
+ return Object.keys(metrics).length > 0 ? metrics : undefined;
43250
+ }
43251
+ function hasStatusLoadEvidence(events, eventName) {
43252
+ return events.some((event) => {
43253
+ if (event.type !== "meta")
43254
+ return false;
43255
+ const meta = event;
43256
+ return meta.source === "status-load" && meta.data?.component === "status-load" && meta.data?.event === eventName;
43257
+ });
43258
+ }
43259
+ function statusLoadEvent(eventName, status, nextStatus, reason) {
43260
+ return {
43261
+ t: Date.now(),
43262
+ type: "meta",
43263
+ model: eventName,
43264
+ backend: "status-load",
43265
+ source: "status-load",
43266
+ data: {
43267
+ component: "status-load",
43268
+ event: eventName,
43269
+ job_id: status.id,
43270
+ previous_status: status.status,
43271
+ next_status: nextStatus,
43272
+ pid: status.pid,
43273
+ tmux_session: status.tmux_session,
43274
+ reason
43275
+ }
43276
+ };
43277
+ }
43278
+ function persistReconciledStatus(sqliteClient, previous, next, events, reason) {
43279
+ if (!sqliteClient)
43280
+ return;
43281
+ try {
43282
+ const evidence = statusLoadEvent("status_reconciled", previous, next.status, reason);
43283
+ if (hasStatusLoadEvidence(events, "status_reconciled")) {
43284
+ sqliteClient.upsertStatus(next);
43285
+ return;
43286
+ }
43287
+ sqliteClient.upsertStatusWithEvent(next, evidence);
43288
+ } catch {}
43289
+ }
43290
+ function persistDeadJobEvidence(sqliteClient, status, events) {
43291
+ if (!sqliteClient || hasStatusLoadEvidence(events, "dead_job_detected"))
43292
+ return;
43293
+ try {
43294
+ sqliteClient.appendEvent(status.id, status.specialist, status.bead_id, statusLoadEvent("dead_job_detected", status, status.status, "active status has dead pid or tmux session"));
43295
+ } catch {}
43296
+ }
43297
+ function reconcileStatusFromEvents(status, sqliteClient) {
43298
+ if (!isActiveStatus(status) || !sqliteClient)
43299
+ return status;
43300
+ let events = [];
43301
+ try {
43302
+ events = sqliteClient.readEvents(status.id);
43303
+ } catch {
43304
+ events = [];
43305
+ }
43306
+ const complete = latestRunComplete(events);
43307
+ const terminalStatus = statusFromRunComplete(complete?.status);
43308
+ if (complete && terminalStatus && terminalStatus !== status.status) {
43309
+ const reconciled = {
43310
+ ...status,
43311
+ status: terminalStatus,
43312
+ elapsed_s: complete.elapsed_s ?? status.elapsed_s,
43313
+ last_event_at_ms: complete.t,
43314
+ model: complete.model ?? status.model,
43315
+ backend: complete.backend ?? status.backend,
43316
+ bead_id: complete.bead_id ?? status.bead_id,
43317
+ metrics: mergedMetrics(status, complete),
43318
+ ...complete.metrics?.output_type ? { output_type: complete.metrics.output_type } : {},
43319
+ ...complete.error ? { error: complete.error } : {}
43320
+ };
43321
+ persistReconciledStatus(sqliteClient, status, reconciled, events, `terminal run_complete ${complete.status}`);
43322
+ return reconciled;
43323
+ }
43324
+ if (isJobDead(status)) {
43325
+ persistDeadJobEvidence(sqliteClient, status, events);
43326
+ }
43327
+ return status;
43328
+ }
43329
+ function reconcileStatuses(statuses, sqliteClient) {
43330
+ return statuses.map((status) => reconcileStatusFromEvents(status, sqliteClient));
43331
+ }
42993
43332
  function loadStatuses() {
42994
43333
  const sqliteClient = createObservabilitySqliteClient();
42995
43334
  const jobsDir = resolveJobsDir();
42996
43335
  const fileStatuses = readStatusesFromFiles(jobsDir);
42997
43336
  try {
42998
- const sqliteStatuses = sqliteClient?.listStatuses() ?? [];
42999
- if (sqliteStatuses.length === 0) {
43000
- return enrichStatusesWithDerivedCurrentTool(fileStatuses, jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
43001
- }
43002
- const merged = new Map;
43003
- for (const status of fileStatuses)
43004
- merged.set(status.id, status);
43005
- for (const status of sqliteStatuses) {
43006
- const current = merged.get(status.id);
43007
- if (!current || status.started_at_ms >= current.started_at_ms) {
43008
- merged.set(status.id, status);
43009
- }
43010
- }
43011
- return enrichStatusesWithDerivedCurrentTool([...merged.values()], jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
43337
+ const sqliteStatuses = (sqliteClient?.listStatuses() ?? []).filter(hasStatusId);
43338
+ const merged = sqliteStatuses.length === 0 ? fileStatuses : mergeStatuses(fileStatuses, sqliteStatuses);
43339
+ return enrichStatusesWithDerivedCurrentTool(reconcileStatuses(merged, sqliteClient), jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
43012
43340
  } catch {
43013
- return enrichStatusesWithDerivedCurrentTool(fileStatuses, jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
43341
+ return enrichStatusesWithDerivedCurrentTool(fileStatuses.filter(hasStatusId), jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
43014
43342
  } finally {
43015
43343
  sqliteClient?.close();
43016
43344
  }
@@ -43018,6 +43346,7 @@ function loadStatuses() {
43018
43346
  var init_status_load = __esm(() => {
43019
43347
  init_observability_sqlite();
43020
43348
  init_job_root();
43349
+ init_supervisor();
43021
43350
  init_timeline_events();
43022
43351
  });
43023
43352
 
@@ -43296,6 +43625,20 @@ function formatToolDetail(event) {
43296
43625
  }
43297
43626
  return `${toolName}: ${dim9(event.phase)}`;
43298
43627
  }
43628
+ function formatAssistantTextContent(content) {
43629
+ const normalized = content.replace(/\r\n/g, `
43630
+ `).trimEnd();
43631
+ if (normalized.length <= ASSISTANT_TEXT_RENDER_LIMIT) {
43632
+ return { body: normalized, renderedChars: normalized.length, truncated: false };
43633
+ }
43634
+ const body = normalized.slice(0, ASSISTANT_TEXT_RENDER_LIMIT);
43635
+ return {
43636
+ body: `${body}
43637
+ ${dim9(`[assistant message truncated: ${normalized.length - ASSISTANT_TEXT_RENDER_LIMIT} chars hidden]`)}`,
43638
+ renderedChars: ASSISTANT_TEXT_RENDER_LIMIT,
43639
+ truncated: true
43640
+ };
43641
+ }
43299
43642
  function formatEventLine(event, options2) {
43300
43643
  const ts = dim9(formatTime(event.t));
43301
43644
  const job = options2.colorize(`[${options2.jobId}]`);
@@ -43419,7 +43762,15 @@ function formatEventLine(event, options2) {
43419
43762
  } else if (event.type === "compaction" || event.type === "retry") {
43420
43763
  detailParts.push(`phase=${event.phase}`);
43421
43764
  } else if (event.type === "text") {
43422
- detailParts.push("kind=assistant");
43765
+ if (event.content) {
43766
+ const rendered = formatAssistantTextContent(event.content);
43767
+ detail = `${dim9(`kind=assistant chars=${event.char_count ?? event.content.length} rendered=${rendered.renderedChars}${rendered.truncated ? " truncated=true" : ""}`)}
43768
+ ${rendered.body}`;
43769
+ } else {
43770
+ detailParts.push("kind=assistant");
43771
+ if (event.char_count !== undefined)
43772
+ detailParts.push(`chars=${event.char_count}`);
43773
+ }
43423
43774
  } else if (event.type === "thinking") {
43424
43775
  detailParts.push("kind=model");
43425
43776
  } else if (event.type === "message") {
@@ -43471,7 +43822,7 @@ function formatEventInlineDebounced(event, activePhase) {
43471
43822
  nextPhase: null
43472
43823
  };
43473
43824
  }
43474
- var dim9 = (s) => `\x1B[2m${s}\x1B[0m`, bold11 = (s) => `\x1B[1m${s}\x1B[0m`, cyan6 = (s) => `\x1B[36m${s}\x1B[0m`, yellow10 = (s) => `\x1B[33m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`, green9 = (s) => `\x1B[32m${s}\x1B[0m`, blue3 = (s) => `\x1B[34m${s}\x1B[0m`, magenta3 = (s) => `\x1B[35m${s}\x1B[0m`, JOB_COLORS, EVENT_LABELS;
43825
+ var dim9 = (s) => `\x1B[2m${s}\x1B[0m`, bold11 = (s) => `\x1B[1m${s}\x1B[0m`, cyan6 = (s) => `\x1B[36m${s}\x1B[0m`, yellow10 = (s) => `\x1B[33m${s}\x1B[0m`, red2 = (s) => `\x1B[31m${s}\x1B[0m`, green9 = (s) => `\x1B[32m${s}\x1B[0m`, blue3 = (s) => `\x1B[34m${s}\x1B[0m`, magenta3 = (s) => `\x1B[35m${s}\x1B[0m`, JOB_COLORS, EVENT_LABELS, ASSISTANT_TEXT_RENDER_LIMIT = 4000;
43475
43826
  var init_format_helpers = __esm(() => {
43476
43827
  JOB_COLORS = [cyan6, yellow10, magenta3, green9, blue3, red2];
43477
43828
  EVENT_LABELS = {
@@ -50173,7 +50524,7 @@ ${conflicts.map((file) => `- ${file}`).join(`
50173
50524
  function runTypecheckGate(cwd = process.cwd()) {
50174
50525
  const hasTypeScriptConfig = existsSync27(join32(cwd, "tsconfig.json")) || readdirSync11(cwd).some((entry) => entry.startsWith("tsconfig") && entry.endsWith(".json"));
50175
50526
  if (!hasTypeScriptConfig) {
50176
- console.log("TypeScript gate: skipped (no tsconfig)");
50527
+ console.log("TypeScript gate: skipped (no tsconfig \u2014 non-TS repo)");
50177
50528
  return;
50178
50529
  }
50179
50530
  const tsc = runCommand("bunx", ["tsc", "--noEmit"], cwd);
@@ -50184,7 +50535,22 @@ function runTypecheckGate(cwd = process.cwd()) {
50184
50535
  throw new Error(`TypeScript gate failed after merge.
50185
50536
  ${stderr || stdout || "Unknown tsc error"}`);
50186
50537
  }
50538
+ function hasNodeBuildScript(cwd) {
50539
+ const packageJsonPath = join32(cwd, "package.json");
50540
+ if (!existsSync27(packageJsonPath))
50541
+ return false;
50542
+ try {
50543
+ const pkg = JSON.parse(readFileSync25(packageJsonPath, "utf8"));
50544
+ return typeof pkg.scripts?.build === "string" && pkg.scripts.build.length > 0;
50545
+ } catch {
50546
+ return false;
50547
+ }
50548
+ }
50187
50549
  function runRebuild(cwd = process.cwd()) {
50550
+ if (!hasNodeBuildScript(cwd)) {
50551
+ console.log("Rebuild: skipped (no package.json build script \u2014 non-Node repo)");
50552
+ return;
50553
+ }
50188
50554
  const build = runCommand("bun", ["run", "build"], cwd);
50189
50555
  if (build.status === 0)
50190
50556
  return;
@@ -50367,6 +50733,8 @@ var init_merge = __esm(() => {
50367
50733
  var exports_run = {};
50368
50734
  __export(exports_run, {
50369
50735
  run: () => run16,
50736
+ resolveBasePin: () => resolveBasePin,
50737
+ buildTmuxLiveFeedCommand: () => buildTmuxLiveFeedCommand,
50370
50738
  buildInjectedWriterDiffVariables: () => buildInjectedWriterDiffVariables,
50371
50739
  buildInjectedReviewerDiffVariables: () => buildInjectedReviewerDiffVariables
50372
50740
  });
@@ -50377,7 +50745,7 @@ import { spawn as cpSpawn, execSync as execSync5 } from "child_process";
50377
50745
  async function parseArgs8(argv) {
50378
50746
  const name = argv[0];
50379
50747
  if (!name || name.startsWith("--")) {
50380
- console.error('Usage: specialists|sp run <name> [--prompt "..."] [--bead <id>] ' + "[--worktree] [--job <id>] [--force-job] [--epic <id>] [--force-stale-base] [--context-depth <n>] [--model <model>] " + "[--no-beads] [--no-bead-notes] [--keep-alive|--no-keep-alive] [--json|--raw]");
50748
+ console.error('Usage: specialists|sp run <name> [--prompt "..."] [--bead <id>] ' + "[--worktree] [--job <id>] [--force-job] [--epic <id>] [--base-sha <sha>] [--base-ref <branch>] [--accept-stale-base --reason <text>] [--context-depth <n>] [--model <model>] " + "[--no-beads] [--no-bead-notes] [--keep-alive|--no-keep-alive] [--json|--raw]");
50381
50749
  process.exit(1);
50382
50750
  }
50383
50751
  let prompt = "";
@@ -50395,6 +50763,10 @@ async function parseArgs8(argv) {
50395
50763
  let forceJob = false;
50396
50764
  let epicId;
50397
50765
  let forceStaleBase = false;
50766
+ let acceptStaleBase = false;
50767
+ let staleBaseReason;
50768
+ let baseSha;
50769
+ let baseRef;
50398
50770
  for (let i = 1;i < argv.length; i++) {
50399
50771
  const token = argv[i];
50400
50772
  if (token === "--prompt" && argv[i + 1]) {
@@ -50463,11 +50835,35 @@ async function parseArgs8(argv) {
50463
50835
  epicId = argv[++i];
50464
50836
  continue;
50465
50837
  }
50838
+ if (token === "--base-sha" && argv[i + 1]) {
50839
+ baseSha = argv[++i];
50840
+ continue;
50841
+ }
50842
+ if (token === "--base-ref" && argv[i + 1]) {
50843
+ baseRef = argv[++i];
50844
+ continue;
50845
+ }
50846
+ if (token === "--reason" && argv[i + 1]) {
50847
+ staleBaseReason = argv[++i];
50848
+ continue;
50849
+ }
50850
+ if (token === "--accept-stale-base") {
50851
+ acceptStaleBase = true;
50852
+ continue;
50853
+ }
50466
50854
  if (token === "--force-stale-base") {
50855
+ process.stderr.write(`[deprecated] --force-stale-base is deprecated; use --accept-stale-base --reason <text>. Aliased for one release.
50856
+ `);
50467
50857
  forceStaleBase = true;
50858
+ acceptStaleBase = true;
50859
+ staleBaseReason ??= "deprecated --force-stale-base";
50468
50860
  continue;
50469
50861
  }
50470
50862
  }
50863
+ if (acceptStaleBase && !staleBaseReason?.trim()) {
50864
+ console.error("Error: --accept-stale-base requires --reason <text>.");
50865
+ process.exit(1);
50866
+ }
50471
50867
  if (worktree && reuseJobId !== undefined) {
50472
50868
  console.error("Error: --worktree and --job are mutually exclusive. Use one or the other.");
50473
50869
  process.exit(1);
@@ -50515,7 +50911,11 @@ async function parseArgs8(argv) {
50515
50911
  reuseJobId,
50516
50912
  forceJob,
50517
50913
  epicId,
50518
- forceStaleBase
50914
+ forceStaleBase,
50915
+ acceptStaleBase,
50916
+ staleBaseReason,
50917
+ baseSha,
50918
+ baseRef
50519
50919
  };
50520
50920
  }
50521
50921
  function readBeadSummary(beadId) {
@@ -50621,7 +51021,7 @@ function assertNoStaleBaseSiblings(beadId, forceStaleBase) {
50621
51021
  if (staleSiblings.length === 0)
50622
51022
  return;
50623
51023
  if (forceStaleBase) {
50624
- process.stderr.write(dim11(`[stale-base guard bypassed: ${staleSiblings.length} unmerged sibling chain(s) under epic ${epicId}]
51024
+ process.stderr.write(dim11(`[stale-base guard accepted: ${staleSiblings.length} unmerged sibling chain(s) under epic ${epicId}]
50625
51025
  `));
50626
51026
  return;
50627
51027
  }
@@ -50630,14 +51030,14 @@ function assertNoStaleBaseSiblings(beadId, forceStaleBase) {
50630
51030
  throw new Error(`Refusing worktree dispatch for bead '${beadId}': epic '${epicId}' has unmerged sibling chains with substantive commits.
50631
51031
  ` + `${lines}
50632
51032
  ` + `Publish the epic first: sp epic merge ${epicId}
50633
- ` + `If intentional, rerun with --force-stale-base.`);
51033
+ ` + `If intentional, rerun with --accept-stale-base --reason <text>.`);
50634
51034
  } finally {
50635
51035
  sqliteClient.close();
50636
51036
  }
50637
51037
  }
50638
51038
  function resolveWorkingDirectory(args, jobsDir, permissionRequired, readStatus) {
50639
51039
  if (args.worktree) {
50640
- assertNoStaleBaseSiblings(args.beadId, args.forceStaleBase);
51040
+ assertNoStaleBaseSiblings(args.beadId, args.acceptStaleBase);
50641
51041
  const info = provisionWorktree({
50642
51042
  beadId: args.beadId,
50643
51043
  specialistName: args.name
@@ -50766,6 +51166,111 @@ function formatFooterModel2(backend, model) {
50766
51166
  function shellQuote2(value) {
50767
51167
  return `'${value.replace(/'/g, `'\\''`)}'`;
50768
51168
  }
51169
+ function buildTmuxLiveFeedCommand(options2) {
51170
+ const handoffPath = shellQuote2(options2.handoffPath);
51171
+ const logPath = shellQuote2(`${options2.handoffPath}.log`);
51172
+ const script = [
51173
+ `cd ${shellQuote2(options2.cwd)}`,
51174
+ `(${options2.runCommand}) > ${logPath} 2>&1 & run_pid=$!`,
51175
+ "job_id=",
51176
+ `for _ in $(seq 1 150); do if [ -s ${handoffPath} ]; then job_id=$(tr -d '\\r\\n' < ${handoffPath}); break; fi; if ! kill -0 "$run_pid" 2>/dev/null; then break; fi; sleep 0.1; done`,
51177
+ `if [ -n "$job_id" ]; then printf '\\n[tmux live feed: %s]\\n' "$job_id"; ${options2.feedCommandPrefix} "$job_id" --follow; wait "$run_pid"; run_status=$?; exit "$run_status"; fi`,
51178
+ `cat ${logPath} 2>/dev/null || true`,
51179
+ 'wait "$run_pid"',
51180
+ "exit $?"
51181
+ ].join("; ");
51182
+ return `/bin/bash -c ${shellQuote2(script)}`;
51183
+ }
51184
+ function recordTmuxLiveFeedStarted(options2) {
51185
+ const sqliteClient = createObservabilitySqliteClient(options2.cwd);
51186
+ if (!sqliteClient)
51187
+ return;
51188
+ try {
51189
+ sqliteClient.appendEvent(options2.jobId, options2.specialist, options2.beadId, {
51190
+ t: Date.now(),
51191
+ type: "meta",
51192
+ model: "tmux_live_feed_started",
51193
+ backend: "cli.run",
51194
+ source: "cli.run",
51195
+ data: {
51196
+ component: "cli.run",
51197
+ event: "tmux_live_feed_started",
51198
+ job_id: options2.jobId,
51199
+ tmux_session: options2.tmuxSession,
51200
+ command: "sp feed <job> --follow",
51201
+ outcome: "started"
51202
+ }
51203
+ });
51204
+ } catch {} finally {
51205
+ sqliteClient.close();
51206
+ }
51207
+ }
51208
+ function runGit2(cwd, args) {
51209
+ return execSync5(["git", ...args.map(shellQuote2)].join(" "), {
51210
+ cwd,
51211
+ stdio: "pipe",
51212
+ encoding: "utf-8",
51213
+ timeout: 1e4
51214
+ }).trim();
51215
+ }
51216
+ function formatErrorMessage(error2) {
51217
+ return error2 instanceof Error ? error2.message : String(error2);
51218
+ }
51219
+ function runGitForBasePin(cwd, args, runArgs) {
51220
+ try {
51221
+ return runGit2(cwd, args);
51222
+ } catch (error2) {
51223
+ const envelope = {
51224
+ ok: false,
51225
+ error_code: "base_fetch_failed",
51226
+ blocked_by: ["fetch_or_resolve_failure"],
51227
+ next_safe_action: "verify network/remote/declared base ref is reachable, or rerun with --accept-stale-base --reason <text> if intentional",
51228
+ base_ref: runArgs.baseRef ?? null,
51229
+ base_sha: runArgs.baseSha ?? null,
51230
+ worktree_path: cwd,
51231
+ underlying_error: formatErrorMessage(error2)
51232
+ };
51233
+ throw new Error(JSON.stringify(envelope));
51234
+ }
51235
+ }
51236
+ function resolveBasePin(args, worktreePath) {
51237
+ if (!worktreePath || !args.worktree && !args.baseSha)
51238
+ return;
51239
+ const baseRef = args.baseRef?.trim();
51240
+ if (baseRef) {
51241
+ runGitForBasePin(worktreePath, ["fetch", "origin", baseRef], args);
51242
+ } else {
51243
+ runGitForBasePin(worktreePath, ["fetch", "origin"], args);
51244
+ }
51245
+ const baseShaObserved = runGitForBasePin(worktreePath, ["rev-parse", baseRef ? "FETCH_HEAD" : "refs/remotes/origin/HEAD"], args);
51246
+ const baseShaPinned = args.baseSha ?? baseShaObserved;
51247
+ const currentSha = runGitForBasePin(worktreePath, ["rev-parse", "HEAD"], args);
51248
+ const branch = runGitForBasePin(worktreePath, ["rev-parse", "--abbrev-ref", "HEAD"], args);
51249
+ const commitsBehindText = runGitForBasePin(worktreePath, ["rev-list", "--count", `${currentSha}..${baseShaPinned}`], args);
51250
+ const commitsBehind = Number.parseInt(commitsBehindText, 10) || 0;
51251
+ const hasStaleBase = currentSha !== baseShaPinned || baseShaObserved !== baseShaPinned;
51252
+ if (!hasStaleBase) {
51253
+ return { baseShaPinned, baseShaObserved, currentSha, branch, commitsBehind, override: false };
51254
+ }
51255
+ if (args.acceptStaleBase) {
51256
+ process.stderr.write(dim11(`[stale-base guard accepted: base_sha_pinned=${baseShaPinned} current_sha=${currentSha} reason=${args.staleBaseReason}]
51257
+ `));
51258
+ return { baseShaPinned, baseShaObserved, currentSha, branch, commitsBehind, override: true };
51259
+ }
51260
+ const envelope = {
51261
+ ok: false,
51262
+ error_code: "stale_base",
51263
+ blocked_by: ["worktree_base_mismatch"],
51264
+ next_safe_action: "Fetch/recreate worktree from declared base, or rerun with --accept-stale-base --reason <text> if divergence is intentional.",
51265
+ base_sha_pinned: baseShaPinned,
51266
+ base_sha_observed: baseShaObserved,
51267
+ current_sha: currentSha,
51268
+ branch,
51269
+ worktree_path: worktreePath,
51270
+ commits_behind: commitsBehind
51271
+ };
51272
+ throw new Error(JSON.stringify(envelope));
51273
+ }
50769
51274
  function extractReviewedJobIdOverride(prompt) {
50770
51275
  const match = prompt.match(/(?:^|\n)\s*reviewed_job_id\s*:\s*([^\n]+)/i);
50771
51276
  const candidate = match?.[1]?.trim();
@@ -50950,15 +51455,23 @@ async function run16() {
50950
51455
  const launchStartedAt = Date.now();
50951
51456
  const innerArgs = process.argv.slice(2).filter((a) => a !== "--background");
50952
51457
  const cmd = `${process.execPath} ${process.argv[1]} ${innerArgs.map(shellQuote2).join(" ")}`;
50953
- const tmuxCmd = `/bin/bash -c ${shellQuote2(`cd ${shellQuote2(cwd)} && exec ${cmd}`)}`;
50954
51458
  let childPid;
50955
51459
  let childExitCode;
50956
51460
  let childExitPromise;
50957
51461
  let handoffPath;
51462
+ let tmuxSessionName;
50958
51463
  if (isTmuxAvailable()) {
50959
51464
  const suffix = randomBytes(3).toString("hex");
50960
51465
  const sessionName = buildSessionName(args.name, suffix);
51466
+ tmuxSessionName = sessionName;
50961
51467
  handoffPath = join33(jobsDir2, `.bg-job-id-${sessionName}`);
51468
+ const feedCommandPrefix = [process.execPath, process.argv[1], "feed"].map(shellQuote2).join(" ");
51469
+ const tmuxCmd = buildTmuxLiveFeedCommand({
51470
+ cwd,
51471
+ runCommand: cmd,
51472
+ handoffPath,
51473
+ feedCommandPrefix
51474
+ });
50962
51475
  createTmuxSession(sessionName, cwd, tmuxCmd, { [JOB_ID_HANDOFF_PATH_ENV]: handoffPath });
50963
51476
  } else {
50964
51477
  const child = cpSpawn(process.execPath, [process.argv[1], ...innerArgs], {
@@ -51020,6 +51533,15 @@ async function run16() {
51020
51533
  jobId = resolveNewestJobIdFromJobsDir(jobsDir2, oldLatest, launchStartedAt - 1000);
51021
51534
  }
51022
51535
  if (jobId) {
51536
+ if (tmuxSessionName) {
51537
+ recordTmuxLiveFeedStarted({
51538
+ cwd,
51539
+ jobId,
51540
+ specialist: args.name,
51541
+ beadId: args.beadId,
51542
+ tmuxSession: tmuxSessionName
51543
+ });
51544
+ }
51023
51545
  process.stdout.write(`${jobId}
51024
51546
  `);
51025
51547
  } else {
@@ -51045,7 +51567,9 @@ async function run16() {
51045
51567
  runOptions: { name: args.name, prompt },
51046
51568
  jobsDir
51047
51569
  });
51048
- const { workingDirectory, reusedFromJobId, worktreeOwnerJobId, inferredBeadId } = resolveWorkingDirectory({ ...args, worktree: useWorktree }, jobsDir, perm, (jobId) => statusReader.readStatus(jobId));
51570
+ const effectiveArgs = { ...args, worktree: useWorktree };
51571
+ const { workingDirectory, reusedFromJobId, worktreeOwnerJobId, inferredBeadId } = resolveWorkingDirectory(effectiveArgs, jobsDir, perm, (jobId) => statusReader.readStatus(jobId));
51572
+ const basePin = resolveBasePin(effectiveArgs, workingDirectory);
51049
51573
  await statusReader.dispose();
51050
51574
  if (!effectiveBeadId && inferredBeadId) {
51051
51575
  effectiveBeadId = inferredBeadId;
@@ -51098,6 +51622,7 @@ async function run16() {
51098
51622
  circuitBreaker,
51099
51623
  beadsClient,
51100
51624
  workingDirectory,
51625
+ basePin,
51101
51626
  reusedFromJobId,
51102
51627
  worktreeOwnerJobId,
51103
51628
  effectiveBeadId,
@@ -55213,7 +55738,8 @@ async function run17() {
55213
55738
  jobs = supervisor.listJobs().filter(isStandaloneJob);
55214
55739
  }
55215
55740
  if (jobId) {
55216
- const selectedJob = supervisor?.readStatus(jobId) ?? null;
55741
+ const selectedStatus = loadStatuses().find((status) => status.id.startsWith(jobId)) ?? supervisor?.readStatus(jobId) ?? null;
55742
+ const selectedJob = selectedStatus ? { ...selectedStatus, is_dead: isJobDead(selectedStatus) } : null;
55217
55743
  if (!selectedJob || !isStandaloneJob(selectedJob)) {
55218
55744
  if (jsonMode) {
55219
55745
  console.log(JSON.stringify({ error: `Job not found: ${jobId}` }, null, 2));
@@ -55364,6 +55890,7 @@ ${bold11("specialists status")}
55364
55890
  var init_status2 = __esm(() => {
55365
55891
  init_loader();
55366
55892
  init_supervisor();
55893
+ init_status_load();
55367
55894
  init_job_root();
55368
55895
  init_observability_sqlite();
55369
55896
  init_format_helpers();
@@ -55416,7 +55943,8 @@ function parseArgs9(argv) {
55416
55943
  "--active",
55417
55944
  "--running",
55418
55945
  "--mine",
55419
- "--health"
55946
+ "--health",
55947
+ "--needs-attention"
55420
55948
  ]);
55421
55949
  const valueFlags = new Set(["--node", "--bead", "--since"]);
55422
55950
  let nodeId;
@@ -55459,6 +55987,7 @@ function parseArgs9(argv) {
55459
55987
  running: argv.includes("--running") || argv.includes("--active"),
55460
55988
  mine: argv.includes("--mine"),
55461
55989
  health: argv.includes("--health"),
55990
+ needsAttention: argv.includes("--needs-attention"),
55462
55991
  beadFilter,
55463
55992
  sinceMs,
55464
55993
  nodeId,
@@ -55495,6 +56024,7 @@ function toJobNode(job) {
55495
56024
  context_health: job.context_health,
55496
56025
  metrics: job.metrics,
55497
56026
  startup_payload_json: job.startup_payload_json ?? null,
56027
+ pr_classification: job.pr_classification,
55498
56028
  children: []
55499
56029
  };
55500
56030
  }
@@ -55844,7 +56374,8 @@ function renderJobLine(job, beadTitles, prefix, connector) {
55844
56374
  const width = Math.max(80, (process.stdout.columns ?? 160) - prefix.length - connector.length);
55845
56375
  const themed = renderJobRow(consoleJob, width, 0, false);
55846
56376
  const body = themed.startsWith(" ") ? themed.slice(2) : themed;
55847
- return `${prefix}${connector}${body}`;
56377
+ const driftBadge = job.pr_classification && job.pr_classification !== "clean" ? red2(` [drift:${job.pr_classification}]`) : "";
56378
+ return `${prefix}${connector}${body}${driftBadge}`;
55848
56379
  }
55849
56380
  function renderTreeNodes(nodes, beadTitles, prefix, renderedJobIds) {
55850
56381
  for (let i = 0;i < nodes.length; i++) {
@@ -56049,7 +56580,7 @@ function renderHuman(jobs, nodes, trees, all, includeTerminal, epicReadiness, he
56049
56580
  const includeSuffix = all ? " " + dim9("\xB7 include terminal") : "";
56050
56581
  console.log(renderStatsLine(statsSnapshot, width) + includeSuffix);
56051
56582
  }
56052
- function statusFromRunComplete(status) {
56583
+ function statusFromRunComplete2(status) {
56053
56584
  if (status === "COMPLETE")
56054
56585
  return "done";
56055
56586
  if (status === "CANCELLED")
@@ -56086,7 +56617,7 @@ function synthesizeStatusFromEvents(jobId) {
56086
56617
  return {
56087
56618
  id: jobId,
56088
56619
  specialist,
56089
- status: statusFromRunComplete(runComplete?.status),
56620
+ status: statusFromRunComplete2(runComplete?.status),
56090
56621
  started_at_ms: first.t,
56091
56622
  elapsed_s: runComplete?.elapsed_s ?? Math.max(0, Math.round((last.t - first.t) / 1000)),
56092
56623
  last_event_at_ms: last.t,
@@ -56232,7 +56763,10 @@ function renderJson(jobs, nodes, trees, _all, epicReadiness, args, health) {
56232
56763
  context_health: job.context_health,
56233
56764
  startup_payload_json: job.startup_payload_json ?? null,
56234
56765
  payload_kb: formatPayloadStats2(job.startup_payload_json).payload_kb,
56235
- payload_tokens: formatPayloadStats2(job.startup_payload_json).payload_tokens
56766
+ payload_tokens: formatPayloadStats2(job.startup_payload_json).payload_tokens,
56767
+ attention_reasons: [
56768
+ ...job.pr_classification && job.pr_classification !== "clean" ? [`pr_drift:${job.pr_classification}`] : []
56769
+ ]
56236
56770
  })),
56237
56771
  nodes,
56238
56772
  trees,
@@ -56272,6 +56806,8 @@ function render(args) {
56272
56806
  return false;
56273
56807
  if (mineBeadIds && (!job.bead_id || !mineBeadIds.has(job.bead_id)))
56274
56808
  return false;
56809
+ if (args.needsAttention && (!job.pr_classification || job.pr_classification === "clean"))
56810
+ return false;
56275
56811
  const cleaned = isPsCleaned2(job);
56276
56812
  if (args.all)
56277
56813
  return true;
@@ -56280,7 +56816,7 @@ function render(args) {
56280
56816
  if (cleaned && args.includeCleaned && TERMINAL_STATES2.includes(job.status))
56281
56817
  return true;
56282
56818
  if (job.is_dead)
56283
- return false;
56819
+ return true;
56284
56820
  if (ACTIVE_STATES2.includes(job.status))
56285
56821
  return true;
56286
56822
  if (args.active)
@@ -57101,6 +57637,8 @@ function getHumanEventKey2(event) {
57101
57637
  return `finish_reason:${event.finish_reason}:${event.source}`;
57102
57638
  case "turn_summary":
57103
57639
  return `turn_summary:${event.turn_index}`;
57640
+ case "text":
57641
+ return `text:${event.seq ?? ""}:${event.char_count ?? ""}:${event.content?.slice(0, 80) ?? ""}`;
57104
57642
  case "compaction":
57105
57643
  case "retry":
57106
57644
  return `${event.type}:${event.phase}`;
@@ -61000,6 +61538,162 @@ async function run34() {
61000
61538
  }
61001
61539
  var bold12 = (s) => `\x1B[1m${s}\x1B[0m`, dim13 = (s) => `\x1B[2m${s}\x1B[0m`, yellow11 = (s) => `\x1B[33m${s}\x1B[0m`, cyan7 = (s) => `\x1B[36m${s}\x1B[0m`, blue4 = (s) => `\x1B[34m${s}\x1B[0m`, green13 = (s) => `\x1B[32m${s}\x1B[0m`;
61002
61540
 
61541
+ // src/specialist/pr-drift-refresh.ts
61542
+ import { execSync as execSync6 } from "child_process";
61543
+ import { createHash as createHash6 } from "crypto";
61544
+ function hashSummary(input2) {
61545
+ return createHash6("sha256").update(input2).digest("hex").slice(0, 16);
61546
+ }
61547
+ function parsePrNumberFromUrl(prUrl) {
61548
+ const match = prUrl.match(/\/pull\/(\d+)$/);
61549
+ if (match)
61550
+ return match[1];
61551
+ if (/^\d+$/.test(prUrl))
61552
+ return prUrl;
61553
+ return;
61554
+ }
61555
+ function deriveClassification(raw) {
61556
+ const mergeState = (raw.mergeStateStatus ?? "").toUpperCase();
61557
+ const state = (raw.state ?? "").toLowerCase();
61558
+ if (state === "merged" || state === "closed")
61559
+ return "stale";
61560
+ switch (mergeState) {
61561
+ case "BEHIND":
61562
+ return "needs-rebase";
61563
+ case "DIRTY":
61564
+ return "conflicted";
61565
+ case "BLOCKED":
61566
+ return "blocked";
61567
+ case "CLEAN":
61568
+ case "HAS_HOOKS":
61569
+ case "UNSTABLE":
61570
+ return "clean";
61571
+ default:
61572
+ return "unknown";
61573
+ }
61574
+ }
61575
+ function classifyError(err) {
61576
+ const msg = err instanceof Error ? err.message : String(err);
61577
+ const lower = msg.toLowerCase();
61578
+ if (lower.includes("enoent") || lower.includes("command not found") || lower.includes("no such file")) {
61579
+ return { kind: "gh_unavailable", summary: hashSummary(msg) };
61580
+ }
61581
+ if (lower.includes("not found") || lower.includes("could not resolve")) {
61582
+ return { kind: "no_pr", summary: hashSummary(msg) };
61583
+ }
61584
+ if (lower.includes("json") || lower.includes("parse")) {
61585
+ return { kind: "parse_error", summary: hashSummary(msg) };
61586
+ }
61587
+ if (lower.includes("timeout") || lower.includes("econnrefused") || lower.includes("network")) {
61588
+ return { kind: "network", summary: hashSummary(msg) };
61589
+ }
61590
+ return { kind: "network", summary: hashSummary(msg) };
61591
+ }
61592
+ async function refreshPrDriftForJob(opts) {
61593
+ const { jobId, prUrl, headSha, client } = opts;
61594
+ const now = Date.now();
61595
+ const prNumber = parsePrNumberFromUrl(prUrl);
61596
+ if (!prNumber) {
61597
+ const patch2 = { pr_classification: "unknown", pr_drift_checked_at_ms: now };
61598
+ client.updatePrDriftState(jobId, patch2);
61599
+ return { ok: false, classification: "unknown", error_kind: "parse_error", error_summary: hashSummary("unparseable-pr-url") };
61600
+ }
61601
+ let stdout;
61602
+ try {
61603
+ stdout = execSync6(`gh pr view ${prNumber} --json state,mergeable,mergeStateStatus,baseRefName,baseRefOid,headRefOid,url`, { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "pipe"] });
61604
+ } catch (err) {
61605
+ const { kind, summary } = classifyError(err);
61606
+ const patch2 = { pr_classification: "unknown", pr_drift_checked_at_ms: now };
61607
+ client.updatePrDriftState(jobId, patch2);
61608
+ return { ok: false, classification: "unknown", error_kind: kind, error_summary: summary };
61609
+ }
61610
+ let raw;
61611
+ try {
61612
+ raw = JSON.parse(stdout.trim());
61613
+ } catch (err) {
61614
+ const summary = hashSummary(err instanceof Error ? err.message : String(err));
61615
+ const patch2 = { pr_classification: "unknown", pr_drift_checked_at_ms: now };
61616
+ client.updatePrDriftState(jobId, patch2);
61617
+ return { ok: false, classification: "unknown", error_kind: "parse_error", error_summary: summary };
61618
+ }
61619
+ const classification = deriveClassification(raw);
61620
+ const patch = {
61621
+ pr_url: raw.url ?? prUrl,
61622
+ pr_head_sha: raw.headRefOid ?? headSha ?? null,
61623
+ pr_state: raw.state ?? null,
61624
+ pr_merge_state: raw.mergeStateStatus ?? null,
61625
+ pr_classification: classification,
61626
+ pr_base_ref: raw.baseRefName ?? null,
61627
+ pr_base_sha: raw.baseRefOid ?? null,
61628
+ pr_drift_checked_at_ms: now
61629
+ };
61630
+ client.updatePrDriftState(jobId, patch);
61631
+ return { ok: true, classification, raw };
61632
+ }
61633
+ var init_pr_drift_refresh = () => {};
61634
+
61635
+ // src/specialist/dead-job-audit.ts
61636
+ function defaultIsPidAlive2(pid) {
61637
+ try {
61638
+ process.kill(pid, 0);
61639
+ return true;
61640
+ } catch (error2) {
61641
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ESRCH") {
61642
+ return false;
61643
+ }
61644
+ return true;
61645
+ }
61646
+ }
61647
+ function auditDeadJobs(opts) {
61648
+ const dryRun = opts.dryRun ?? false;
61649
+ const nowMs = opts.nowMs ?? Date.now();
61650
+ const isPidAlive2 = opts.isPidAlive ?? defaultIsPidAlive2;
61651
+ const minAgeMs = opts.minAgeMs ?? 60000;
61652
+ const rows = opts.client.listStaleSpecialistJobs({ minAgeMs, nowMs });
61653
+ const found = [];
61654
+ let cancelled = 0;
61655
+ for (const row of rows) {
61656
+ if (isPidAlive2(row.pid))
61657
+ continue;
61658
+ const age_ms = Math.max(0, nowMs - row.updated_at_ms);
61659
+ found.push({
61660
+ job_id: row.job_id,
61661
+ pid: row.pid,
61662
+ reason: "container-restart-orphan",
61663
+ age_ms
61664
+ });
61665
+ if (!dryRun) {
61666
+ opts.client.markSpecialistJobCancelled(row.job_id, "container-restart-orphan");
61667
+ cancelled += 1;
61668
+ opts.client.appendForensicEvent(row.job_id, row.specialist, row.bead_id ?? undefined, createForensicEvent({
61669
+ event_family: "lifecycle",
61670
+ event_name: "dead_declared",
61671
+ resource: {
61672
+ service_namespace: "xtrm",
61673
+ service_name: "specialists",
61674
+ service_component: "dead-job-audit",
61675
+ deployment_environment: "development",
61676
+ repo: "specialists",
61677
+ participant_kind: "specialist",
61678
+ participant_role: row.specialist
61679
+ },
61680
+ correlation: { job_id: row.job_id, bead_id: row.bead_id ?? undefined },
61681
+ body: {
61682
+ job_id: row.job_id,
61683
+ pid: row.pid,
61684
+ age_ms,
61685
+ reason: "container-restart-orphan",
61686
+ dry_run: dryRun
61687
+ }
61688
+ }));
61689
+ }
61690
+ }
61691
+ return { dryRun, found, cancelled };
61692
+ }
61693
+ var init_dead_job_audit = __esm(() => {
61694
+ init_forensic_events();
61695
+ });
61696
+
61003
61697
  // src/cli/doctor.ts
61004
61698
  var exports_doctor = {};
61005
61699
  __export(exports_doctor, {
@@ -61011,7 +61705,7 @@ __export(exports_doctor, {
61011
61705
  compareVersions: () => compareVersions2,
61012
61706
  cleanupProcesses: () => cleanupProcesses
61013
61707
  });
61014
- import { createHash as createHash6 } from "crypto";
61708
+ import { createHash as createHash7 } from "crypto";
61015
61709
  import { spawnSync as spawnSync23 } from "child_process";
61016
61710
  import { existsSync as existsSync39, lstatSync as lstatSync2, mkdirSync as mkdirSync15, readdirSync as readdirSync20, readFileSync as readFileSync36, readlinkSync as readlinkSync3, writeFileSync as writeFileSync19 } from "fs";
61017
61711
  import { dirname as dirname16, join as join44, relative as relative4, resolve as resolve13 } from "path";
@@ -61191,7 +61885,7 @@ function checkVersion() {
61191
61885
  return true;
61192
61886
  }
61193
61887
  function hashFile(path3) {
61194
- const hash = createHash6("sha256");
61888
+ const hash = createHash7("sha256");
61195
61889
  hash.update(readFileSync36(path3));
61196
61890
  return hash.digest("hex");
61197
61891
  }
@@ -61524,7 +62218,7 @@ function checkClaudeMdFragments() {
61524
62218
  return allOk;
61525
62219
  }
61526
62220
  function parseDoctorArgs(argv) {
61527
- const opts = { json: false, drift: false, specialists: false };
62221
+ const opts = { json: false, drift: false, specialists: false, pr_drift: false, reap_dead_jobs: false, dry_run: false };
61528
62222
  for (let i = 0;i < argv.length; i += 1) {
61529
62223
  const token = argv[i];
61530
62224
  if (token === "--json") {
@@ -61539,6 +62233,18 @@ function parseDoctorArgs(argv) {
61539
62233
  opts.specialists = true;
61540
62234
  continue;
61541
62235
  }
62236
+ if (token === "--pr-drift") {
62237
+ opts.pr_drift = true;
62238
+ continue;
62239
+ }
62240
+ if (token === "--reap-dead-jobs") {
62241
+ opts.reap_dead_jobs = true;
62242
+ continue;
62243
+ }
62244
+ if (token === "--dry-run") {
62245
+ opts.dry_run = true;
62246
+ continue;
62247
+ }
61542
62248
  if (token === "--root") {
61543
62249
  const value = argv[i + 1];
61544
62250
  if (!value || value.startsWith("--"))
@@ -61833,6 +62539,128 @@ function checkZombieJobs() {
61833
62539
  }
61834
62540
  return result.zombies === 0;
61835
62541
  }
62542
+ async function runDoctorPrDrift(json) {
62543
+ const client = createObservabilitySqliteClient();
62544
+ if (!client) {
62545
+ if (json) {
62546
+ console.log(JSON.stringify({ jobs: [], error: "observability sqlite unavailable" }));
62547
+ } else {
62548
+ console.error("observability sqlite unavailable");
62549
+ }
62550
+ process.exitCode = 1;
62551
+ return;
62552
+ }
62553
+ try {
62554
+ const jobs = client.listJobsNeedingPrDriftRefresh();
62555
+ const results = [];
62556
+ for (const job of jobs) {
62557
+ const startedAt = Date.now();
62558
+ console.error(JSON.stringify({
62559
+ component: "pr_drift",
62560
+ event: "refresh_attempted",
62561
+ job_id: job.job_id,
62562
+ duration_ms: 0,
62563
+ gh_stderr_hash: "",
62564
+ branch: job.branch ?? null,
62565
+ checked_at_ms: startedAt
62566
+ }));
62567
+ const result = await refreshPrDriftForJob({
62568
+ jobId: job.job_id,
62569
+ prUrl: job.pr_url,
62570
+ headSha: job.pr_head_sha ?? undefined,
62571
+ client
62572
+ });
62573
+ const durationMs = Date.now() - startedAt;
62574
+ const classification = result.classification;
62575
+ const ghStderrHash = result.error_summary ? result.error_summary.slice(0, 8) : "";
62576
+ const eventOut = {
62577
+ component: "pr_drift",
62578
+ event: result.ok ? "refresh_completed" : "refresh_failed",
62579
+ job_id: job.job_id,
62580
+ duration_ms: durationMs,
62581
+ gh_stderr_hash: ghStderrHash,
62582
+ pr_classification: classification,
62583
+ branch: job.branch ?? null,
62584
+ checked_at_ms: startedAt
62585
+ };
62586
+ console.error(JSON.stringify(eventOut));
62587
+ results.push({
62588
+ job_id: job.job_id,
62589
+ classification,
62590
+ pr_url: job.pr_url,
62591
+ ...result.error_kind ? { error_kind: result.error_kind } : {},
62592
+ duration_ms: durationMs
62593
+ });
62594
+ }
62595
+ if (json) {
62596
+ console.log(JSON.stringify({ jobs: results }, null, 2));
62597
+ } else {
62598
+ console.log(`
62599
+ ${bold13("specialists doctor --pr-drift")}
62600
+ `);
62601
+ if (results.length === 0) {
62602
+ ok3("No PR-linked jobs need drift refresh");
62603
+ } else {
62604
+ for (const r of results) {
62605
+ const icon = r.classification === "clean" ? green14("\u2713") : r.classification === "needs-rebase" ? yellow12("\u25CB") : r.classification === "conflicted" ? red7("\u2717") : r.classification === "blocked" ? yellow12("\u25A0") : r.classification === "stale" ? dim14("\u25CB") : yellow12("?");
62606
+ const suffix = r.error_kind ? ` ${dim14(`(${r.error_kind})`)}` : "";
62607
+ console.log(` ${icon} ${r.job_id} ${r.classification}${suffix}`);
62608
+ }
62609
+ }
62610
+ console.log("");
62611
+ }
62612
+ } finally {
62613
+ client.close();
62614
+ }
62615
+ }
62616
+ async function runDoctorReapDeadJobs(opts) {
62617
+ const client = createObservabilitySqliteClient();
62618
+ if (!client) {
62619
+ if (opts.json) {
62620
+ console.log(JSON.stringify({ dryRun: opts.dry_run, found: [], cancelled: 0, error: "observability sqlite unavailable" }));
62621
+ } else {
62622
+ console.error("observability sqlite unavailable");
62623
+ }
62624
+ process.exitCode = 1;
62625
+ return;
62626
+ }
62627
+ try {
62628
+ const result = auditDeadJobs({
62629
+ client,
62630
+ dryRun: opts.dry_run,
62631
+ nowMs: Date.now()
62632
+ });
62633
+ for (const finding of result.found) {
62634
+ const structuredLog = {
62635
+ component: "dead_job_audit",
62636
+ event: "dead_declared",
62637
+ job_id: finding.job_id,
62638
+ age_ms: finding.age_ms,
62639
+ dry_run: opts.dry_run
62640
+ };
62641
+ console.error(JSON.stringify(structuredLog));
62642
+ }
62643
+ if (opts.json) {
62644
+ console.log(JSON.stringify(result, null, 2));
62645
+ } else {
62646
+ console.log(`
62647
+ ${bold13("specialists doctor --reap-dead-jobs")}
62648
+ `);
62649
+ if (result.found.length === 0) {
62650
+ ok3("No dead running/waiting jobs found");
62651
+ } else {
62652
+ const action = opts.dry_run ? "Would cancel" : "Cancelled";
62653
+ console.log(` ${yellow12("\u25CB")} ${result.found.length} dead job(s) found (${result.cancelled} ${action})`);
62654
+ for (const f of result.found) {
62655
+ console.log(` - ${f.job_id} pid=${f.pid} age=${Math.round(f.age_ms / 1000)}s`);
62656
+ }
62657
+ }
62658
+ console.log("");
62659
+ }
62660
+ } finally {
62661
+ client.close();
62662
+ }
62663
+ }
61836
62664
  async function run35(argv = process.argv.slice(3)) {
61837
62665
  const subcommand = argv[0];
61838
62666
  if (subcommand === "orphans") {
@@ -61844,6 +62672,10 @@ async function run35(argv = process.argv.slice(3)) {
61844
62672
  renderDriftTable(opts.root ?? process.cwd(), opts.json);
61845
62673
  return;
61846
62674
  }
62675
+ if (opts.pr_drift) {
62676
+ await runDoctorPrDrift(opts.json);
62677
+ return;
62678
+ }
61847
62679
  if (opts.specialists) {
61848
62680
  console.log(`
61849
62681
  ${bold13("specialists doctor --specialists")}
@@ -61853,6 +62685,10 @@ ${bold13("specialists doctor --specialists")}
61853
62685
  process.exitCode = overridesOk2 ? 0 : 1;
61854
62686
  return;
61855
62687
  }
62688
+ if (opts.reap_dead_jobs) {
62689
+ await runDoctorReapDeadJobs(opts);
62690
+ return;
62691
+ }
61856
62692
  if (subcommand && subcommand !== "--help" && subcommand !== "-h" && !subcommand.startsWith("--")) {
61857
62693
  console.error(`Unknown doctor subcommand: '${subcommand}'`);
61858
62694
  process.exit(1);
@@ -61887,8 +62723,10 @@ ${bold13("specialists doctor")}
61887
62723
  var bold13 = (s) => `\x1B[1m${s}\x1B[0m`, dim14 = (s) => `\x1B[2m${s}\x1B[0m`, green14 = (s) => `\x1B[32m${s}\x1B[0m`, yellow12 = (s) => `\x1B[33m${s}\x1B[0m`, red7 = (s) => `\x1B[31m${s}\x1B[0m`, CWD, CLAUDE_DIR, PI_DIR, XTRM_SKILLS_DIR, XTRM_DEFAULT_SKILLS_DIR, XTRM_ACTIVE_SKILLS_DIR, SPECIALISTS_DIR, DEFAULT_SPECIALISTS_DIR, USER_SPECIALISTS_DIR, HOOKS_DIR, CLAUDE_HOOKS_DIR, SETTINGS_FILE, MCP_FILE2, HOOK_NAMES;
61888
62724
  var init_doctor = __esm(() => {
61889
62725
  init_observability_sqlite();
62726
+ init_pr_drift_refresh();
61890
62727
  init_canonical_asset_resolver();
61891
62728
  init_drift_detector();
62729
+ init_dead_job_audit();
61892
62730
  init_loader();
61893
62731
  init_version_check();
61894
62732
  CWD = process.cwd();
@@ -62067,7 +62905,7 @@ var init_benchmarks = __esm(() => {
62067
62905
  });
62068
62906
 
62069
62907
  // src/specialist/model-probes.ts
62070
- import { createHash as createHash7, randomUUID as randomUUID6 } from "crypto";
62908
+ import { createHash as createHash8, randomUUID as randomUUID6 } from "crypto";
62071
62909
  import { mkdirSync as mkdirSync17, readdirSync as readdirSync21, readFileSync as readFileSync38, writeFileSync as writeFileSync21 } from "fs";
62072
62910
  import { homedir as homedir11 } from "os";
62073
62911
  import { dirname as dirname18, join as join46, resolve as resolve14 } from "path";
@@ -62177,7 +63015,7 @@ function getProbeRunDir(model, specName, cacheDir = join46(homedir11(), ".cache"
62177
63015
  return resolve14(getProbeCanonicalPath(model, specName, cacheDir).replace(/\.json$/u, ""), randomUUID6());
62178
63016
  }
62179
63017
  function getProbeCanonicalPath(model, specName, cacheDir = join46(homedir11(), ".cache", "specialists", "probes")) {
62180
- const probeId = createHash7("sha256").update(`${model}\x00${specName}\x00${PROBE_TEMPLATE}`).digest("hex").slice(0, 12);
63018
+ const probeId = createHash8("sha256").update(`${model}\x00${specName}\x00${PROBE_TEMPLATE}`).digest("hex").slice(0, 12);
62181
63019
  return join46(cacheDir, `${sanitizePathSegment(model)}-${sanitizePathSegment(specName)}-${probeId}.json`);
62182
63020
  }
62183
63021
  function sanitizePathSegment(value) {
@@ -71559,8 +72397,18 @@ async function run40() {
71559
72397
  " --epic <id> Explicit epic membership for this job. Defaults to bead.parent.",
71560
72398
  " Useful for prep jobs belonging to a merge-gated epic.",
71561
72399
  " --force-job Bypass concurrency guard for active worktrees (MEDIUM/HIGH).",
71562
- " --force-stale-base Bypass stale-base guard when epic sibling chains have unmerged",
71563
- " substantive commits. Use at risk of later merge conflicts.",
72400
+ " --base-sha <sha> Pin the base SHA the run measures against (specialists-05q.3).",
72401
+ " Recorded on the job observability row as base_sha_pinned.",
72402
+ " When omitted, defaults to the observed remote base tip.",
72403
+ " --base-ref <branch> Base branch ref to fetch before pinning (default: origin/HEAD).",
72404
+ " Combined with --base-sha for deterministic precondition checks.",
72405
+ " --accept-stale-base Override the stale-base precondition gate. Requires --reason.",
72406
+ " Logged in stderr + auditable via the structured refusal envelope",
72407
+ " (specialists-05q.3). Prefer over deprecated --force-stale-base.",
72408
+ " --reason <text> Required justification when --accept-stale-base is set.",
72409
+ " Surfaced in stderr audit line + refusal envelope.",
72410
+ " --force-stale-base [deprecated] Alias for --accept-stale-base with default reason.",
72411
+ " Emits a deprecation warning; removal in a future release.",
71564
72412
  "",
71565
72413
  "Examples:",
71566
72414
  " specialists run debugger --bead unitAI-55d",
@@ -72226,6 +73074,8 @@ async function run40() {
72226
73074
  " 7. zombie job detection",
72227
73075
  " 8. CLAUDE.md fragments (XTRM-MANAGED sentinels) \u2014 delegates to xt claude-sync",
72228
73076
  " 9. drift check for stale managed mirrors (--check-drift / --drift)",
73077
+ " 10. PR drift refresh for tracked jobs (--pr-drift) \u2014 specialists-05q.2",
73078
+ " 11. dead-job audit + reap orphans (--reap-dead-jobs [--dry-run]) \u2014 specialists-05q.4",
72229
73079
  "",
72230
73080
  "Behavior:",
72231
73081
  " - prints fix hints for failing checks",
@@ -72237,10 +73087,26 @@ async function run40() {
72237
73087
  " Category A (specialists runtime) and Category B",
72238
73088
  " (filesystem skills/hooks) are distinct; doctor",
72239
73089
  " covers Category A only",
73090
+ " --pr-drift Refresh PR/base drift state for tracked jobs by",
73091
+ " shelling out to `gh pr view --json` (specialists-05q.2).",
73092
+ " Iterates jobs whose pr_drift_checked_at_ms is stale or null;",
73093
+ " updates pr_state/pr_merge_state/pr_classification/pr_base_sha.",
73094
+ " Classifications: clean | needs-rebase | conflicted | blocked |",
73095
+ " stale | unknown. gh failures classify as unknown (never throws).",
73096
+ " --reap-dead-jobs Audit specialist_jobs rows in active states with dead pids",
73097
+ " and mark them cancelled with reason container-restart-orphan",
73098
+ " (specialists-05q.4). Emits xtrm.forensic.v1 lifecycle.dead_declared",
73099
+ " event per finding. Conservative predicate: all of {active status,",
73100
+ " pid set, ESRCH dead pid, age > 60s} must hold.",
73101
+ " --dry-run Pair with --reap-dead-jobs: list findings without mutation.",
73102
+ " --json Emit JSON envelope { dryRun, found:[{job_id, pid, reason,",
73103
+ " age_ms}], cancelled }.",
72240
73104
  "",
72241
73105
  "Examples:",
72242
73106
  " specialists doctor",
72243
73107
  " specialists doctor orphans",
73108
+ " specialists doctor --pr-drift",
73109
+ " specialists doctor --reap-dead-jobs --dry-run --json",
72244
73110
  ""
72245
73111
  ].join(`
72246
73112
  `));