@jaggerxtrm/specialists 3.18.0 → 3.18.2

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 (55) hide show
  1. package/config/mandatory-rules/code-quality-defaults.md +58 -1
  2. package/config/mandatory-rules/executor-delivery.md +4 -0
  3. package/config/mandatory-rules/test-runner-execution-scope.md +4 -0
  4. package/config/skills/setup-specialists/SKILL.md +1 -1
  5. package/config/skills/using-script-specialists/SKILL.md +5 -5
  6. package/config/skills/using-specialists/SKILL.md +1135 -879
  7. package/config/skills/using-specialists-auto/SKILL.md +21 -21
  8. package/config/specialists/chain-coordinator.specialist.json +63 -0
  9. package/config/specialists/reviewer.specialist.json +1 -1
  10. package/config/specialists/seconder.specialist.json +1 -1
  11. package/dist/asset-contract.json +8 -13
  12. package/dist/index.js +1070 -81
  13. package/dist/lib.js +185 -9
  14. package/dist/types/cli/clean.d.ts.map +1 -1
  15. package/dist/types/cli/doctor.d.ts.map +1 -1
  16. package/dist/types/cli/feed.d.ts.map +1 -1
  17. package/dist/types/cli/format-helpers.d.ts.map +1 -1
  18. package/dist/types/cli/init.d.ts.map +1 -1
  19. package/dist/types/cli/merge.d.ts.map +1 -1
  20. package/dist/types/cli/ps.d.ts.map +1 -1
  21. package/dist/types/cli/run.d.ts +19 -0
  22. package/dist/types/cli/run.d.ts.map +1 -1
  23. package/dist/types/cli/status.d.ts.map +1 -1
  24. package/dist/types/cli/view.d.ts.map +1 -1
  25. package/dist/types/pi/session.d.ts +1 -0
  26. package/dist/types/pi/session.d.ts.map +1 -1
  27. package/dist/types/specialist/control.d.ts.map +1 -1
  28. package/dist/types/specialist/dead-job-audit.d.ts +20 -0
  29. package/dist/types/specialist/dead-job-audit.d.ts.map +1 -0
  30. package/dist/types/specialist/launch.d.ts +8 -0
  31. package/dist/types/specialist/launch.d.ts.map +1 -1
  32. package/dist/types/specialist/loader.d.ts +12 -0
  33. package/dist/types/specialist/loader.d.ts.map +1 -1
  34. package/dist/types/specialist/observability-db.d.ts +1 -1
  35. package/dist/types/specialist/observability-sqlite.d.ts +76 -0
  36. package/dist/types/specialist/observability-sqlite.d.ts.map +1 -1
  37. package/dist/types/specialist/pr-drift-refresh.d.ts +17 -0
  38. package/dist/types/specialist/pr-drift-refresh.d.ts.map +1 -0
  39. package/dist/types/specialist/process-health.d.ts +16 -1
  40. package/dist/types/specialist/process-health.d.ts.map +1 -1
  41. package/dist/types/specialist/runner.d.ts +3 -0
  42. package/dist/types/specialist/runner.d.ts.map +1 -1
  43. package/dist/types/specialist/status-load.d.ts.map +1 -1
  44. package/dist/types/specialist/supervisor.d.ts +10 -0
  45. package/dist/types/specialist/supervisor.d.ts.map +1 -1
  46. package/dist/types/specialist/timeline-events.d.ts +2 -0
  47. package/dist/types/specialist/timeline-events.d.ts.map +1 -1
  48. package/docs/design/roadmap/chain-templates/README.md +1 -1
  49. package/docs/design/roadmap/chains-prompt-evals.md +3054 -0
  50. package/docs/design/roadmap/specialists-roadmap.md +75 -7
  51. package/docs/design/xt-pi-role-pi-flag-passthrough.md +174 -0
  52. package/docs/skills.md +4 -13
  53. package/package.json +2 -2
  54. package/config/skills/using-specialists-v2/SKILL.md +0 -766
  55. package/config/skills/using-specialists-v3/SKILL.md +0 -1267
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)
@@ -21471,6 +21617,14 @@ class SpecialistLoader {
21471
21617
  this.cache.set(name, merged.spec);
21472
21618
  return merged.spec;
21473
21619
  }
21620
+ async getEffective(name) {
21621
+ const merged = await this.buildMergedSpec(name);
21622
+ if (!merged)
21623
+ return null;
21624
+ if (merged.warnings.length)
21625
+ this.blockedFieldWarnings.set(name, merged.warnings);
21626
+ return merged.spec;
21627
+ }
21474
21628
  getBlockedFieldWarnings(name) {
21475
21629
  if (name)
21476
21630
  return this.blockedFieldWarnings.get(name) ?? [];
@@ -21956,6 +22110,24 @@ function findTokenUsage(payload) {
21956
22110
  }
21957
22111
  return normalizeTokenUsage(record3);
21958
22112
  }
22113
+ function extractMessageTextContent(message) {
22114
+ if (!message || typeof message !== "object")
22115
+ return "";
22116
+ const record3 = message;
22117
+ const content = record3.content;
22118
+ if (typeof content === "string")
22119
+ return content;
22120
+ if (!Array.isArray(content))
22121
+ return "";
22122
+ return content.map((part) => {
22123
+ if (!part || typeof part !== "object")
22124
+ return "";
22125
+ const item = part;
22126
+ if (item.type !== undefined && item.type !== "text")
22127
+ return "";
22128
+ return typeof item.text === "string" ? item.text : "";
22129
+ }).join("");
22130
+ }
21959
22131
  function findApiErrorMessage(payload) {
21960
22132
  if (!payload || typeof payload !== "object")
21961
22133
  return;
@@ -22231,6 +22403,9 @@ class PiAgentSession {
22231
22403
  const cavemanPath = join7(piExtDir, "caveman");
22232
22404
  if (existsSync7(cavemanPath))
22233
22405
  args.push("-e", cavemanPath);
22406
+ const nvidiaNimPath = join7(homedir2(), ".pi", "agent", "git", "github.com", "xRyul", "pi-nvidia-nim");
22407
+ if (existsSync7(nvidiaNimPath))
22408
+ args.push("-e", nvidiaNimPath);
22234
22409
  const npmGlobalDir = resolveGlobalNodeModulesDir();
22235
22410
  const excludedExtensions = new Set(this.options.excludeExtensions ?? []);
22236
22411
  if (npmGlobalDir) {
@@ -22446,7 +22621,8 @@ class PiAgentSession {
22446
22621
  if (type === "message_end") {
22447
22622
  const role = event.message?.role;
22448
22623
  if (role === "assistant") {
22449
- this.options.onEvent?.("message_end_assistant");
22624
+ const content = extractMessageTextContent(event.message);
22625
+ this.options.onEvent?.("message_end_assistant", content ? { content, charCount: content.length } : undefined);
22450
22626
  } else if (role === "toolResult") {
22451
22627
  this.options.onEvent?.("message_end_tool_result");
22452
22628
  }
@@ -22475,7 +22651,7 @@ class PiAgentSession {
22475
22651
  const messages = event.messages ?? [];
22476
22652
  const last = [...messages].reverse().find((m) => m.role === "assistant");
22477
22653
  if (last) {
22478
- this._lastOutput = last.content.filter((c) => c.type === "text").map((c) => c.text).join("");
22654
+ this._lastOutput = extractMessageTextContent(last);
22479
22655
  }
22480
22656
  this._updateTokenUsage(findTokenUsage(event), "agent_end");
22481
22657
  this._updateFinishReason(findFinishReason(event), "agent_end");
@@ -22487,7 +22663,11 @@ class PiAgentSession {
22487
22663
  }
22488
22664
  this._agentEndReceived = true;
22489
22665
  this._clearStallTimer();
22490
- this.options.onEvent?.("agent_end");
22666
+ if (this._lastOutput) {
22667
+ this.options.onEvent?.("agent_end", { content: this._lastOutput, charCount: this._lastOutput.length });
22668
+ } else {
22669
+ this.options.onEvent?.("agent_end");
22670
+ }
22491
22671
  this._doneResolve?.();
22492
22672
  return;
22493
22673
  }
@@ -22591,7 +22771,7 @@ class PiAgentSession {
22591
22771
  const delta = typeof ae.delta === "string" ? ae.delta : "";
22592
22772
  if (delta)
22593
22773
  this.options.onToken?.(delta);
22594
- this.options.onEvent?.("text", { charCount: delta.length });
22774
+ this.options.onEvent?.("text", { charCount: delta.length, content: delta });
22595
22775
  break;
22596
22776
  }
22597
22777
  case "thinking_start":
@@ -25346,11 +25526,7 @@ function mapCallbackEventToTimelineEvent(callbackEvent, context) {
25346
25526
  };
25347
25527
  }
25348
25528
  case "text":
25349
- return {
25350
- t,
25351
- type: TIMELINE_EVENT_TYPES.TEXT,
25352
- ...context.charCount !== undefined ? { char_count: context.charCount } : {}
25353
- };
25529
+ return null;
25354
25530
  case "agent_end":
25355
25531
  case "message_done":
25356
25532
  case "done":
@@ -27040,6 +27216,8 @@ class Supervisor {
27040
27216
  } : {},
27041
27217
  ...runOptions.variables?.chain_root_bead_id ? { chain_root_bead_id: runOptions.variables.chain_root_bead_id } : {},
27042
27218
  ...runOptions.workingDirectory ? { worktree_path: runOptions.workingDirectory } : {},
27219
+ ...runOptions.baseShaPinned ? { base_sha_pinned: runOptions.baseShaPinned } : {},
27220
+ ...runOptions.baseShaPinnedAtMs ? { base_sha_pinned_at_ms: runOptions.baseShaPinnedAtMs } : {},
27043
27221
  ...runOptions.workingDirectory ? { branch: resolveCurrentBranch(runOptions.workingDirectory) } : { branch: resolveCurrentBranch() },
27044
27222
  variables_keys: variablesKeys,
27045
27223
  reviewed_job_id_present: variablesKeys.includes("reviewed_job_id"),
@@ -27064,6 +27242,8 @@ class Supervisor {
27064
27242
  ...runOptions.workingDirectory ? { worktree_path: runOptions.workingDirectory } : {},
27065
27243
  ...runOptions.reusedFromJobId ? { reused_from_job_id: runOptions.reusedFromJobId } : {},
27066
27244
  ...runOptions.worktreeOwnerJobId ? { worktree_owner_job_id: runOptions.worktreeOwnerJobId } : {},
27245
+ ...runOptions.baseShaPinned ? { base_sha_pinned: runOptions.baseShaPinned } : {},
27246
+ ...runOptions.baseShaPinnedAtMs ? { base_sha_pinned_at_ms: runOptions.baseShaPinnedAtMs } : {},
27067
27247
  ...runOptions.worktreeOwnerJobId || runOptions.workingDirectory ? {
27068
27248
  chain_kind: "chain",
27069
27249
  chain_id: runOptions.worktreeOwnerJobId ?? id,
@@ -27168,7 +27348,6 @@ class Supervisor {
27168
27348
  execFileSync2("mkfifo", [fifoPath]);
27169
27349
  setStatus({ fifo_path: fifoPath });
27170
27350
  } catch {}
27171
- let textLogged = false;
27172
27351
  let runMetrics = {
27173
27352
  turns: 0,
27174
27353
  tool_calls: 0,
@@ -27184,6 +27363,8 @@ class Supervisor {
27184
27363
  let textCharCount = 0;
27185
27364
  let thinkingCharCount = 0;
27186
27365
  let turnTextAccumulator = "";
27366
+ let assistantMessageAccumulator = "";
27367
+ let lastPersistedAssistantMessage = "";
27187
27368
  let currentContextTokens = 0;
27188
27369
  const toolCallNames = [];
27189
27370
  const activeToolCalls = new Map;
@@ -27439,7 +27620,7 @@ ${appendError}
27439
27620
  writeFileSync5(this.resultPath(id), lastTurnSummaryTextContent || output, "utf-8");
27440
27621
  }
27441
27622
  try {
27442
- this.withSqliteOperation("upsertResult:resume_turn", (client) => client.upsertResult(id, output));
27623
+ this.withSqliteOperation("upsertResult:resume_turn", (client) => client.upsertResult(id, lastTurnSummaryTextContent || output));
27443
27624
  } catch (error2) {
27444
27625
  console.warn(`[supervisor] SQLite upsertResult failed during resume turn: ${String(error2)}`);
27445
27626
  }
@@ -27447,7 +27628,7 @@ ${appendError}
27447
27628
  model: statusSnapshot.model ?? "unknown",
27448
27629
  backend: statusSnapshot.backend ?? "unknown",
27449
27630
  beadId: statusSnapshot.bead_id,
27450
- output
27631
+ output: lastTurnSummaryTextContent || output
27451
27632
  });
27452
27633
  const passFinalize = shouldAutoFinalizeKeepAlive(output);
27453
27634
  const readOnlyClose = shouldAutoCloseReadOnlyKeepAlive(output);
@@ -27665,12 +27846,17 @@ ${appendError}
27665
27846
  textCharCount = 0;
27666
27847
  thinkingCharCount = 0;
27667
27848
  turnTextAccumulator = "";
27849
+ assistantMessageAccumulator = "";
27668
27850
  }
27669
27851
  if (eventType === "message_start_assistant") {
27670
27852
  turnTextAccumulator = "";
27853
+ assistantMessageAccumulator = "";
27671
27854
  }
27672
27855
  if (eventType === "text") {
27673
27856
  textCharCount += details?.charCount ?? 0;
27857
+ if (typeof details?.content === "string") {
27858
+ assistantMessageAccumulator += details.content;
27859
+ }
27674
27860
  }
27675
27861
  if (eventType === "thinking") {
27676
27862
  thinkingCharCount += details?.charCount ?? 0;
@@ -27753,6 +27939,17 @@ ${appendError}
27753
27939
  data: metaDetails?.data
27754
27940
  } : undefined
27755
27941
  });
27942
+ const assistantMessageContent = assistantMessageAccumulator || (typeof details?.content === "string" ? details.content : "");
27943
+ if ((eventType === "message_end_assistant" || eventType === "agent_end") && assistantMessageContent.trim().length > 0 && assistantMessageContent !== lastPersistedAssistantMessage) {
27944
+ appendTimelineEvent({
27945
+ t: Date.now(),
27946
+ type: TIMELINE_EVENT_TYPES.TEXT,
27947
+ char_count: assistantMessageContent.length,
27948
+ content: assistantMessageContent
27949
+ });
27950
+ lastPersistedAssistantMessage = assistantMessageContent;
27951
+ assistantMessageAccumulator = "";
27952
+ }
27756
27953
  if (timelineEvent) {
27757
27954
  appendTimelineEvent(timelineEvent);
27758
27955
  if (eventType === "tool_execution_end") {
@@ -27764,9 +27961,6 @@ ${appendError}
27764
27961
  const nextActiveTool = activeToolCalls.values().next().value?.tool;
27765
27962
  setStatus({ current_tool: nextActiveTool });
27766
27963
  }
27767
- } else if (eventType === "text" && !textLogged) {
27768
- textLogged = true;
27769
- appendTimelineEvent({ t: Date.now(), type: TIMELINE_EVENT_TYPES.TEXT });
27770
27964
  }
27771
27965
  }, (metricEvent) => {
27772
27966
  if (metricEvent.type === "token_usage") {
@@ -27977,14 +28171,15 @@ ${appendError}
27977
28171
  });
27978
28172
  isReadOnlySpecialist = finalResult.permissionRequired === "READ_ONLY";
27979
28173
  autoCommitPolicy = finalResult.autoCommit;
28174
+ const reportedOutput = lastTurnSummaryTextContent || finalResult.output;
27980
28175
  emitRunCompleteForTurn({
27981
28176
  model: finalResult.model,
27982
28177
  backend: finalResult.backend,
27983
28178
  beadId: finalResult.beadId,
27984
- output: finalResult.output
28179
+ output: reportedOutput
27985
28180
  });
27986
28181
  try {
27987
- this.withSqliteOperation("upsertResult:initial_turn", (client) => client.upsertResult(id, finalResult.output));
28182
+ this.withSqliteOperation("upsertResult:initial_turn", (client) => client.upsertResult(id, reportedOutput));
27988
28183
  } catch (error2) {
27989
28184
  console.warn(`[supervisor] SQLite upsertResult failed during initial turn: ${String(error2)}`);
27990
28185
  }
@@ -28119,7 +28314,7 @@ ${appendError}
28119
28314
  exit_reason: runMetrics.exit_reason,
28120
28315
  metrics: enrichedRunMetrics,
28121
28316
  ...gitnexusSummary ? { gitnexus_summary: gitnexusSummary } : {}
28122
- }), latestOutput);
28317
+ }), reportedOutput);
28123
28318
  return true;
28124
28319
  });
28125
28320
  if (completePersisted === undefined) {
@@ -28756,7 +28951,6 @@ var exports_view = {};
28756
28951
  __export(exports_view, {
28757
28952
  run: () => run5
28758
28953
  });
28759
- import { readFile as readFile2 } from "fs/promises";
28760
28954
  import readline2 from "readline/promises";
28761
28955
  import { stdin as input, stdout as output } from "process";
28762
28956
  function permissionBadge2(permission) {
@@ -28921,9 +29115,13 @@ function printFullSpecialist(spec) {
28921
29115
  printBySection(spec, "stall_detection");
28922
29116
  printBySection(spec, "beads");
28923
29117
  }
28924
- async function printRaw(summary) {
28925
- const content = await readFile2(summary.filePath, "utf-8");
28926
- console.log(content);
29118
+ async function printRaw(summary, loader) {
29119
+ const spec = await loader.getEffective(summary.name);
29120
+ if (!spec) {
29121
+ console.error(`Specialist not found: ${summary.name}`);
29122
+ process.exit(1);
29123
+ }
29124
+ console.log(JSON.stringify(spec, null, 2));
28927
29125
  }
28928
29126
  async function run5() {
28929
29127
  let args;
@@ -28961,7 +29159,7 @@ async function run5() {
28961
29159
  selectedSummary = chosen;
28962
29160
  }
28963
29161
  if (args.raw) {
28964
- await printRaw(selectedSummary);
29162
+ await printRaw(selectedSummary, loader);
28965
29163
  return;
28966
29164
  }
28967
29165
  const specialist = await loader.get(selectedSummary.name);
@@ -29107,7 +29305,7 @@ __export(exports_init, {
29107
29305
  runGlobal: () => runGlobal,
29108
29306
  run: () => run7
29109
29307
  });
29110
- import { copyFileSync, cpSync, existsSync as existsSync15, lstatSync, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync13, readlinkSync, renameSync as renameSync3, symlinkSync, unlinkSync, writeFileSync as writeFileSync7 } from "fs";
29308
+ import { copyFileSync, cpSync, existsSync as existsSync15, lstatSync, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync13, readlinkSync, renameSync as renameSync3, rmSync as rmSync3, symlinkSync, unlinkSync, writeFileSync as writeFileSync7 } from "fs";
29111
29309
  import { spawnSync as spawnSync10 } from "child_process";
29112
29310
  import { basename as basename4, dirname as dirname8, join as join15, relative, resolve as resolve6 } from "path";
29113
29311
  function ok(msg) {
@@ -29492,6 +29690,23 @@ function installProjectSkills(cwd, syncSkills) {
29492
29690
  ensureRootSymlink(join15(cwd, ".pi", "skills"), activeRoot);
29493
29691
  let copied = 0;
29494
29692
  let refreshed = 0;
29693
+ let pruned = 0;
29694
+ if (syncSkills) {
29695
+ const currentSkills = new Set(skills);
29696
+ for (const entry of readdirSync4(defaultRoot, { withFileTypes: true })) {
29697
+ if (!entry.isDirectory() || currentSkills.has(entry.name))
29698
+ continue;
29699
+ const retiredDefaultPath = join15(defaultRoot, entry.name);
29700
+ const retiredActivePath = join15(activeRoot, entry.name);
29701
+ try {
29702
+ if (lstatSync(retiredActivePath).isSymbolicLink() && resolve6(dirname8(retiredActivePath), readlinkSync(retiredActivePath)) === resolve6(retiredDefaultPath)) {
29703
+ unlinkSync(retiredActivePath);
29704
+ }
29705
+ } catch {}
29706
+ rmSync3(retiredDefaultPath, { recursive: true, force: true });
29707
+ pruned++;
29708
+ }
29709
+ }
29495
29710
  for (const skill of skills) {
29496
29711
  const src = join15(sourceDir, skill);
29497
29712
  const defaultSkillPath = join15(defaultRoot, skill);
@@ -29510,6 +29725,8 @@ function installProjectSkills(cwd, syncSkills) {
29510
29725
  ok(`copied ${copied} skill${copied === 1 ? "" : "s"} to .xtrm/skills/default/`);
29511
29726
  if (refreshed > 0)
29512
29727
  ok(`re-synced ${refreshed} skill${refreshed === 1 ? "" : "s"} in .xtrm/skills/default/`);
29728
+ if (pruned > 0)
29729
+ ok(`pruned ${pruned} retired managed skill${pruned === 1 ? "" : "s"}`);
29513
29730
  ok("verified active skill symlinks in .xtrm/skills/active/");
29514
29731
  }
29515
29732
  function createSpecialistsDirs(cwd) {
@@ -31762,7 +31979,7 @@ __export(exports_validate, {
31762
31979
  parseArgs: () => parseArgs5,
31763
31980
  ArgParseError: () => ArgParseError3
31764
31981
  });
31765
- import { readFile as readFile3 } from "fs/promises";
31982
+ import { readFile as readFile2 } from "fs/promises";
31766
31983
  import { existsSync as existsSync18 } from "fs";
31767
31984
  function parseArgs5(argv) {
31768
31985
  const value = argv[0];
@@ -31785,7 +32002,7 @@ async function findSpecialist(name) {
31785
32002
  return list.find((item) => item.name === name);
31786
32003
  }
31787
32004
  async function loadSpecFromFile(filePath) {
31788
- const content = await readFile3(filePath, "utf-8");
32005
+ const content = await readFile2(filePath, "utf-8");
31789
32006
  const raw = filePath.endsWith(".yaml") || filePath.endsWith(".yml") ? $parse(content) : JSON.parse(content);
31790
32007
  return SpecialistSchema.parseAsync(raw);
31791
32008
  }
@@ -31847,7 +32064,7 @@ async function run10() {
31847
32064
  }
31848
32065
  process.exit(1);
31849
32066
  }
31850
- const content = await readFile3(summary.filePath, "utf-8");
32067
+ const content = await readFile2(summary.filePath, "utf-8");
31851
32068
  const result = await validateSpecialist(content);
31852
32069
  if (args.json) {
31853
32070
  console.log(JSON.stringify({
@@ -32647,7 +32864,7 @@ var init_edit = __esm(() => {
32647
32864
  });
32648
32865
 
32649
32866
  // src/specialist/resolution-diagnostics.ts
32650
- import { readFile as readFile4 } from "fs/promises";
32867
+ import { readFile as readFile3 } from "fs/promises";
32651
32868
  import { createRequire as createRequire3 } from "module";
32652
32869
  import { execSync as execSync3 } from "child_process";
32653
32870
  import { join as join19 } from "path";
@@ -32744,7 +32961,7 @@ function probeHealth(catalog) {
32744
32961
  }
32745
32962
  async function loadResolvedConfigReport(args) {
32746
32963
  const manifest = readJsonFile3(join19(args.projectDir, "config", "specialists", `${args.specialistName}.specialist.json`));
32747
- const index = loadToolCatalogIndex(await readFile4(args.catalogsPath, "utf-8"));
32964
+ const index = loadToolCatalogIndex(await readFile3(args.catalogsPath, "utf-8"));
32748
32965
  const catalogs = index.catalogs;
32749
32966
  const probes = catalogs.map(probeHealth);
32750
32967
  const extensionState = Object.fromEntries(probes.map((probe) => [probe.name, { health: probe.health, catalogCompatible: probe.drift === "none" }]));
@@ -33027,6 +33244,8 @@ async function launchSpecialist(opts) {
33027
33244
  forceJob: opts.args.forceJob,
33028
33245
  permissionRequired: opts.perm,
33029
33246
  workingDirectory: opts.workingDirectory,
33247
+ baseShaPinned: opts.basePin?.baseShaPinned,
33248
+ baseShaPinnedAtMs: opts.basePin ? Date.now() : undefined,
33030
33249
  reusedFromJobId: opts.reusedFromJobId,
33031
33250
  worktreeOwnerJobId: opts.worktreeOwnerJobId,
33032
33251
  output_file: opts.specialist.specialist.output_file
@@ -33040,6 +33259,19 @@ async function launchSpecialist(opts) {
33040
33259
  `)) : undefined),
33041
33260
  onJobStarted: ({ id }) => {
33042
33261
  opts.onJobStarted?.({ id });
33262
+ if (opts.basePin) {
33263
+ const sqliteClient = createObservabilitySqliteClient(opts.workingDirectory);
33264
+ try {
33265
+ sqliteClient?.updatePrDriftState(id, {
33266
+ pr_base_ref: opts.basePin.branch,
33267
+ pr_base_sha: opts.basePin.baseShaObserved,
33268
+ base_sha_pinned: opts.basePin.baseShaPinned,
33269
+ base_sha_pinned_at_ms: Date.now()
33270
+ });
33271
+ } finally {
33272
+ sqliteClient?.close();
33273
+ }
33274
+ }
33043
33275
  process.stderr.write(dim8(`[job started: ${id}]
33044
33276
  `));
33045
33277
  const handoffPath = process.env.SPECIALISTS_BG_JOB_ID_PATH;
@@ -33114,6 +33346,7 @@ var bold10 = (s) => `\x1B[1m${s}\x1B[0m`, dim8 = (s) => `\x1B[2m${s}\x1B[0m`, gr
33114
33346
  var init_launch = __esm(() => {
33115
33347
  init_supervisor();
33116
33348
  init_runner();
33349
+ init_observability_sqlite();
33117
33350
  });
33118
33351
 
33119
33352
  // node_modules/@earendil-works/pi-tui/dist/fuzzy.js
@@ -42933,6 +43166,9 @@ var init_feed = __esm(() => {
42933
43166
  // src/specialist/status-load.ts
42934
43167
  import { existsSync as existsSync20, readdirSync as readdirSync7, readFileSync as readFileSync18 } from "fs";
42935
43168
  import { join as join24 } from "path";
43169
+ function hasStatusId(status) {
43170
+ return typeof status.id === "string" && status.id.trim().length > 0;
43171
+ }
42936
43172
  function readStatusesFromFiles(jobsDir) {
42937
43173
  if (!existsSync20(jobsDir))
42938
43174
  return [];
@@ -42942,7 +43178,9 @@ function readStatusesFromFiles(jobsDir) {
42942
43178
  if (!existsSync20(statusPath))
42943
43179
  continue;
42944
43180
  try {
42945
- statuses.push(JSON.parse(readFileSync18(statusPath, "utf-8")));
43181
+ const status = JSON.parse(readFileSync18(statusPath, "utf-8"));
43182
+ if (hasStatusId(status))
43183
+ statuses.push(status);
42946
43184
  } catch {}
42947
43185
  }
42948
43186
  return statuses.sort((a, b) => b.started_at_ms - a.started_at_ms);
@@ -42990,27 +43228,148 @@ function enrichStatusesWithDerivedCurrentTool(statuses, jobsDir, sqliteClient) {
42990
43228
  current_tool: resolveDerivedCurrentTool(status, jobsDir, sqliteClient)
42991
43229
  }));
42992
43230
  }
43231
+ function statusFreshnessMs(status) {
43232
+ return Math.max(status.last_event_at_ms ?? 0, status.started_at_ms ?? 0);
43233
+ }
43234
+ function mergeStatuses(fileStatuses, sqliteStatuses) {
43235
+ const merged = new Map;
43236
+ for (const status of fileStatuses) {
43237
+ if (hasStatusId(status))
43238
+ merged.set(status.id, status);
43239
+ }
43240
+ for (const status of sqliteStatuses) {
43241
+ if (!hasStatusId(status))
43242
+ continue;
43243
+ const current = merged.get(status.id);
43244
+ if (!current || statusFreshnessMs(status) >= statusFreshnessMs(current)) {
43245
+ merged.set(status.id, status);
43246
+ }
43247
+ }
43248
+ return [...merged.values()];
43249
+ }
43250
+ function isActiveStatus(status) {
43251
+ return status.status === "starting" || status.status === "running" || status.status === "waiting";
43252
+ }
43253
+ function statusFromRunComplete(status) {
43254
+ if (status === "COMPLETE")
43255
+ return "done";
43256
+ if (status === "CANCELLED")
43257
+ return "cancelled";
43258
+ if (status === "ERROR")
43259
+ return "error";
43260
+ return;
43261
+ }
43262
+ function latestRunComplete(events) {
43263
+ for (let index = events.length - 1;index >= 0; index -= 1) {
43264
+ const event = events[index];
43265
+ if (event?.type === "run_complete")
43266
+ return event;
43267
+ }
43268
+ return;
43269
+ }
43270
+ function mergedMetrics(status, complete) {
43271
+ const tokenUsage = complete.token_usage ?? complete.metrics?.token_usage;
43272
+ const metrics = {
43273
+ ...status.metrics ?? {},
43274
+ ...complete.metrics ?? {},
43275
+ ...tokenUsage ? { token_usage: tokenUsage } : {},
43276
+ ...complete.finish_reason ? { finish_reason: complete.finish_reason } : {},
43277
+ ...complete.tool_calls ? { tool_call_names: complete.tool_calls, tool_calls: complete.tool_calls.length } : {},
43278
+ ...complete.exit_reason ? { exit_reason: complete.exit_reason } : {}
43279
+ };
43280
+ return Object.keys(metrics).length > 0 ? metrics : undefined;
43281
+ }
43282
+ function hasStatusLoadEvidence(events, eventName) {
43283
+ return events.some((event) => {
43284
+ if (event.type !== "meta")
43285
+ return false;
43286
+ const meta = event;
43287
+ return meta.source === "status-load" && meta.data?.component === "status-load" && meta.data?.event === eventName;
43288
+ });
43289
+ }
43290
+ function statusLoadEvent(eventName, status, nextStatus, reason) {
43291
+ return {
43292
+ t: Date.now(),
43293
+ type: "meta",
43294
+ model: eventName,
43295
+ backend: "status-load",
43296
+ source: "status-load",
43297
+ data: {
43298
+ component: "status-load",
43299
+ event: eventName,
43300
+ job_id: status.id,
43301
+ previous_status: status.status,
43302
+ next_status: nextStatus,
43303
+ pid: status.pid,
43304
+ tmux_session: status.tmux_session,
43305
+ reason
43306
+ }
43307
+ };
43308
+ }
43309
+ function persistReconciledStatus(sqliteClient, previous, next, events, reason) {
43310
+ if (!sqliteClient)
43311
+ return;
43312
+ try {
43313
+ const evidence = statusLoadEvent("status_reconciled", previous, next.status, reason);
43314
+ if (hasStatusLoadEvidence(events, "status_reconciled")) {
43315
+ sqliteClient.upsertStatus(next);
43316
+ return;
43317
+ }
43318
+ sqliteClient.upsertStatusWithEvent(next, evidence);
43319
+ } catch {}
43320
+ }
43321
+ function persistDeadJobEvidence(sqliteClient, status, events) {
43322
+ if (!sqliteClient || hasStatusLoadEvidence(events, "dead_job_detected"))
43323
+ return;
43324
+ try {
43325
+ sqliteClient.appendEvent(status.id, status.specialist, status.bead_id, statusLoadEvent("dead_job_detected", status, status.status, "active status has dead pid or tmux session"));
43326
+ } catch {}
43327
+ }
43328
+ function reconcileStatusFromEvents(status, sqliteClient) {
43329
+ if (!isActiveStatus(status) || !sqliteClient)
43330
+ return status;
43331
+ let events = [];
43332
+ try {
43333
+ events = sqliteClient.readEvents(status.id);
43334
+ } catch {
43335
+ events = [];
43336
+ }
43337
+ const complete = latestRunComplete(events);
43338
+ const terminalStatus = statusFromRunComplete(complete?.status);
43339
+ if (complete && terminalStatus && terminalStatus !== status.status) {
43340
+ const reconciled = {
43341
+ ...status,
43342
+ status: terminalStatus,
43343
+ elapsed_s: complete.elapsed_s ?? status.elapsed_s,
43344
+ last_event_at_ms: complete.t,
43345
+ model: complete.model ?? status.model,
43346
+ backend: complete.backend ?? status.backend,
43347
+ bead_id: complete.bead_id ?? status.bead_id,
43348
+ metrics: mergedMetrics(status, complete),
43349
+ ...complete.metrics?.output_type ? { output_type: complete.metrics.output_type } : {},
43350
+ ...complete.error ? { error: complete.error } : {}
43351
+ };
43352
+ persistReconciledStatus(sqliteClient, status, reconciled, events, `terminal run_complete ${complete.status}`);
43353
+ return reconciled;
43354
+ }
43355
+ if (isJobDead(status)) {
43356
+ persistDeadJobEvidence(sqliteClient, status, events);
43357
+ }
43358
+ return status;
43359
+ }
43360
+ function reconcileStatuses(statuses, sqliteClient) {
43361
+ return statuses.map((status) => reconcileStatusFromEvents(status, sqliteClient));
43362
+ }
42993
43363
  function loadStatuses() {
42994
43364
  const sqliteClient = createObservabilitySqliteClient();
42995
43365
  const jobsDir = resolveJobsDir();
42996
43366
  const fileStatuses = readStatusesFromFiles(jobsDir);
42997
43367
  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);
43368
+ const sqliteStatuses = (sqliteClient?.listStatuses() ?? []).filter(hasStatusId);
43369
+ const merged = sqliteStatuses.length === 0 ? fileStatuses : mergeStatuses(fileStatuses, sqliteStatuses);
43370
+ return enrichStatusesWithDerivedCurrentTool(reconcileStatuses(merged, sqliteClient), jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
43012
43371
  } catch {
43013
- return enrichStatusesWithDerivedCurrentTool(fileStatuses, jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
43372
+ return enrichStatusesWithDerivedCurrentTool(fileStatuses.filter(hasStatusId), jobsDir, sqliteClient).sort((a, b) => b.started_at_ms - a.started_at_ms);
43014
43373
  } finally {
43015
43374
  sqliteClient?.close();
43016
43375
  }
@@ -43018,6 +43377,7 @@ function loadStatuses() {
43018
43377
  var init_status_load = __esm(() => {
43019
43378
  init_observability_sqlite();
43020
43379
  init_job_root();
43380
+ init_supervisor();
43021
43381
  init_timeline_events();
43022
43382
  });
43023
43383
 
@@ -43296,6 +43656,20 @@ function formatToolDetail(event) {
43296
43656
  }
43297
43657
  return `${toolName}: ${dim9(event.phase)}`;
43298
43658
  }
43659
+ function formatAssistantTextContent(content) {
43660
+ const normalized = content.replace(/\r\n/g, `
43661
+ `).trimEnd();
43662
+ if (normalized.length <= ASSISTANT_TEXT_RENDER_LIMIT) {
43663
+ return { body: normalized, renderedChars: normalized.length, truncated: false };
43664
+ }
43665
+ const body = normalized.slice(0, ASSISTANT_TEXT_RENDER_LIMIT);
43666
+ return {
43667
+ body: `${body}
43668
+ ${dim9(`[assistant message truncated: ${normalized.length - ASSISTANT_TEXT_RENDER_LIMIT} chars hidden]`)}`,
43669
+ renderedChars: ASSISTANT_TEXT_RENDER_LIMIT,
43670
+ truncated: true
43671
+ };
43672
+ }
43299
43673
  function formatEventLine(event, options2) {
43300
43674
  const ts = dim9(formatTime(event.t));
43301
43675
  const job = options2.colorize(`[${options2.jobId}]`);
@@ -43419,7 +43793,15 @@ function formatEventLine(event, options2) {
43419
43793
  } else if (event.type === "compaction" || event.type === "retry") {
43420
43794
  detailParts.push(`phase=${event.phase}`);
43421
43795
  } else if (event.type === "text") {
43422
- detailParts.push("kind=assistant");
43796
+ if (event.content) {
43797
+ const rendered = formatAssistantTextContent(event.content);
43798
+ detail = `${dim9(`kind=assistant chars=${event.char_count ?? event.content.length} rendered=${rendered.renderedChars}${rendered.truncated ? " truncated=true" : ""}`)}
43799
+ ${rendered.body}`;
43800
+ } else {
43801
+ detailParts.push("kind=assistant");
43802
+ if (event.char_count !== undefined)
43803
+ detailParts.push(`chars=${event.char_count}`);
43804
+ }
43423
43805
  } else if (event.type === "thinking") {
43424
43806
  detailParts.push("kind=model");
43425
43807
  } else if (event.type === "message") {
@@ -43471,7 +43853,7 @@ function formatEventInlineDebounced(event, activePhase) {
43471
43853
  nextPhase: null
43472
43854
  };
43473
43855
  }
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;
43856
+ 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
43857
  var init_format_helpers = __esm(() => {
43476
43858
  JOB_COLORS = [cyan6, yellow10, magenta3, green9, blue3, red2];
43477
43859
  EVENT_LABELS = {
@@ -43570,6 +43952,39 @@ async function stopJob(jobId, opts = {}) {
43570
43952
  if (!status)
43571
43953
  throw new Error(`No job found: ${jobId}`);
43572
43954
  if (status.status === "done" || status.status === "error" || status.status === "cancelled") {
43955
+ const pid2 = status.pid;
43956
+ if (typeof pid2 === "number" && Number.isInteger(pid2) && pid2 > 0 && isProcessAlive(pid2, status.started_at_ms)) {
43957
+ supervisor.emitControlEvent(jobId, "stop_terminal_alive_reaped", {
43958
+ source: "cli",
43959
+ pid: pid2,
43960
+ previous_status: status.status,
43961
+ reason: "registry_terminal_process_alive",
43962
+ signal: "SIGTERM/SIGKILL",
43963
+ tmux_session: status.tmux_session
43964
+ });
43965
+ try {
43966
+ process.kill(-pid2, "SIGTERM");
43967
+ } catch (err) {
43968
+ if (err?.code !== "ESRCH") {
43969
+ try {
43970
+ process.kill(pid2, "SIGTERM");
43971
+ } catch {}
43972
+ }
43973
+ }
43974
+ const exited = await waitForProcessExit(pid2, 3000);
43975
+ if (!exited) {
43976
+ try {
43977
+ process.kill(-pid2, "SIGKILL");
43978
+ } catch {
43979
+ tryKillProcessGroup(pid2);
43980
+ }
43981
+ }
43982
+ if (status.tmux_session)
43983
+ killTmuxSession(status.tmux_session);
43984
+ process.stdout.write(`${green10("\u2713")} Reaped orphaned PID ${pid2} for job ${jobId} (registry was ${status.status}).
43985
+ `);
43986
+ return;
43987
+ }
43573
43988
  process.stderr.write(`${dim10(`Job ${jobId} already finalized (${status.status}).`)}
43574
43989
  `);
43575
43990
  return;
@@ -44810,9 +45225,11 @@ function collectStaleSpecialistJobs(options2 = {}) {
44810
45225
  const procRoot = options2.procRoot ?? "/proc";
44811
45226
  const nowMs = options2.nowMs ?? Date.now();
44812
45227
  const minKeepAliveAgeMs = options2.minKeepAliveAgeMs ?? 30 * 60 * 1000;
45228
+ const minTerminalAliveAgeMs = options2.minTerminalAliveAgeMs ?? 60 * 1000;
44813
45229
  const observabilityClient = options2.observabilityClient ?? createObservabilitySqliteClient();
44814
45230
  const statuses = observabilityClient?.listStatuses() ?? [];
44815
45231
  const staleStatuses = statuses.filter((status) => ["starting", "running", "waiting"].includes(status.status));
45232
+ const terminalStatuses = statuses.filter((status) => ["done", "error", "cancelled"].includes(status.status));
44816
45233
  const uptimeSeconds = readProcUptimeSecondsOrNull(procRoot) ?? nowMs / 1000;
44817
45234
  const candidates = [];
44818
45235
  for (const status of staleStatuses) {
@@ -44848,6 +45265,39 @@ function collectStaleSpecialistJobs(options2 = {}) {
44848
45265
  continue;
44849
45266
  candidates.push({ jobId: status.id, pid, beadId: status.bead_id ?? null, specialist: status.specialist, cwd: snapshot.cwd, ageMs, reason: "dead-toolchain" });
44850
45267
  }
45268
+ for (const status of terminalStatuses) {
45269
+ const pid = status.pid;
45270
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
45271
+ continue;
45272
+ const updatedAtMs = status.updated_at_ms ?? nowMs;
45273
+ const ageMs = Math.max(0, nowMs - updatedAtMs);
45274
+ if (ageMs < minTerminalAliveAgeMs)
45275
+ continue;
45276
+ const snapshot = readProcessSnapshot(pid, procRoot, uptimeSeconds);
45277
+ if (!snapshot) {
45278
+ if (readProcessLiveness(pid, procRoot) !== "alive")
45279
+ continue;
45280
+ candidates.push({
45281
+ jobId: status.id,
45282
+ pid,
45283
+ beadId: status.bead_id ?? null,
45284
+ specialist: status.specialist,
45285
+ cwd: readProcCwdOrNull(pid, procRoot),
45286
+ ageMs,
45287
+ reason: "terminal-alive"
45288
+ });
45289
+ continue;
45290
+ }
45291
+ candidates.push({
45292
+ jobId: status.id,
45293
+ pid,
45294
+ beadId: status.bead_id ?? null,
45295
+ specialist: status.specialist,
45296
+ cwd: snapshot.cwd,
45297
+ ageMs,
45298
+ reason: "terminal-alive"
45299
+ });
45300
+ }
44851
45301
  return candidates.sort((left, right) => left.pid - right.pid);
44852
45302
  }
44853
45303
  var DEFAULT_WARN_PCT = 70, DEFAULT_REFUSE_PCT = 85, CLOCK_TICKS_PER_SECOND = 100;
@@ -45666,7 +46116,7 @@ import {
45666
46116
  mkdirSync as mkdirSync12,
45667
46117
  readFileSync as readFileSync21,
45668
46118
  renameSync as renameSync4,
45669
- rmSync as rmSync3,
46119
+ rmSync as rmSync4,
45670
46120
  statSync as statSync5,
45671
46121
  writeFileSync as writeFileSync13
45672
46122
  } from "fs";
@@ -45735,7 +46185,7 @@ function writeConsoleConfig(config2, cookie = `${process.pid}.${Math.floor(perfo
45735
46185
  renameSync4(tmpPath, location.path);
45736
46186
  } catch (renameError) {
45737
46187
  try {
45738
- rmSync3(tmpPath, { force: true });
46188
+ rmSync4(tmpPath, { force: true });
45739
46189
  } catch {}
45740
46190
  logError("ps", "write_global_config", {
45741
46191
  step: "console_json",
@@ -49189,7 +49639,7 @@ var init_console = __esm(() => {
49189
49639
  });
49190
49640
 
49191
49641
  // src/specialist/worktree.ts
49192
- import { existsSync as existsSync26, symlinkSync as symlinkSync2, mkdirSync as mkdirSync13, rmSync as rmSync4 } from "fs";
49642
+ import { existsSync as existsSync26, symlinkSync as symlinkSync2, mkdirSync as mkdirSync13, rmSync as rmSync5 } from "fs";
49193
49643
  import { join as join30, resolve as resolve9 } from "path";
49194
49644
  import { spawnSync as spawnSync15, execFileSync as execFileSync3 } from "child_process";
49195
49645
  function deriveBranchName(beadId, specialistName) {
@@ -49231,7 +49681,7 @@ function provisionWorktree(options2) {
49231
49681
  createWorktreeViaBd(worktreePath, branch, commonRoot);
49232
49682
  normalizeParentHooksPath(commonRoot);
49233
49683
  try {
49234
- rmSync4(join30(worktreePath, ".beads"), { recursive: true, force: true });
49684
+ rmSync5(join30(worktreePath, ".beads"), { recursive: true, force: true });
49235
49685
  markBeadsSkipWorktree(worktreePath);
49236
49686
  } catch {}
49237
49687
  symlinkPiNpmCache(commonRoot, worktreePath);
@@ -49321,7 +49771,7 @@ var init_worktree = __esm(() => {
49321
49771
  });
49322
49772
 
49323
49773
  // src/specialist/epic-reconciler.ts
49324
- import { mkdirSync as mkdirSync14, openSync as openSync2, readFileSync as readFileSync24, rmSync as rmSync5, writeFileSync as writeFileSync14 } from "fs";
49774
+ import { mkdirSync as mkdirSync14, openSync as openSync2, readFileSync as readFileSync24, rmSync as rmSync6, writeFileSync as writeFileSync14 } from "fs";
49325
49775
  import { join as join31 } from "path";
49326
49776
  function buildEpicLockPath(epicId) {
49327
49777
  const location = resolveObservabilityDbLocation();
@@ -49349,7 +49799,7 @@ function withEpicAdvisoryLock(epicId, action) {
49349
49799
  } finally {
49350
49800
  if (lockFd !== null) {
49351
49801
  try {
49352
- rmSync5(lockPath, { force: true });
49802
+ rmSync6(lockPath, { force: true });
49353
49803
  } catch {}
49354
49804
  }
49355
49805
  }
@@ -50173,7 +50623,7 @@ ${conflicts.map((file) => `- ${file}`).join(`
50173
50623
  function runTypecheckGate(cwd = process.cwd()) {
50174
50624
  const hasTypeScriptConfig = existsSync27(join32(cwd, "tsconfig.json")) || readdirSync11(cwd).some((entry) => entry.startsWith("tsconfig") && entry.endsWith(".json"));
50175
50625
  if (!hasTypeScriptConfig) {
50176
- console.log("TypeScript gate: skipped (no tsconfig)");
50626
+ console.log("TypeScript gate: skipped (no tsconfig \u2014 non-TS repo)");
50177
50627
  return;
50178
50628
  }
50179
50629
  const tsc = runCommand("bunx", ["tsc", "--noEmit"], cwd);
@@ -50184,7 +50634,22 @@ function runTypecheckGate(cwd = process.cwd()) {
50184
50634
  throw new Error(`TypeScript gate failed after merge.
50185
50635
  ${stderr || stdout || "Unknown tsc error"}`);
50186
50636
  }
50637
+ function hasNodeBuildScript(cwd) {
50638
+ const packageJsonPath = join32(cwd, "package.json");
50639
+ if (!existsSync27(packageJsonPath))
50640
+ return false;
50641
+ try {
50642
+ const pkg = JSON.parse(readFileSync25(packageJsonPath, "utf8"));
50643
+ return typeof pkg.scripts?.build === "string" && pkg.scripts.build.length > 0;
50644
+ } catch {
50645
+ return false;
50646
+ }
50647
+ }
50187
50648
  function runRebuild(cwd = process.cwd()) {
50649
+ if (!hasNodeBuildScript(cwd)) {
50650
+ console.log("Rebuild: skipped (no package.json build script \u2014 non-Node repo)");
50651
+ return;
50652
+ }
50188
50653
  const build = runCommand("bun", ["run", "build"], cwd);
50189
50654
  if (build.status === 0)
50190
50655
  return;
@@ -50367,6 +50832,8 @@ var init_merge = __esm(() => {
50367
50832
  var exports_run = {};
50368
50833
  __export(exports_run, {
50369
50834
  run: () => run16,
50835
+ resolveBasePin: () => resolveBasePin,
50836
+ buildTmuxLiveFeedCommand: () => buildTmuxLiveFeedCommand,
50370
50837
  buildInjectedWriterDiffVariables: () => buildInjectedWriterDiffVariables,
50371
50838
  buildInjectedReviewerDiffVariables: () => buildInjectedReviewerDiffVariables
50372
50839
  });
@@ -50377,7 +50844,7 @@ import { spawn as cpSpawn, execSync as execSync5 } from "child_process";
50377
50844
  async function parseArgs8(argv) {
50378
50845
  const name = argv[0];
50379
50846
  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]");
50847
+ 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
50848
  process.exit(1);
50382
50849
  }
50383
50850
  let prompt = "";
@@ -50395,6 +50862,10 @@ async function parseArgs8(argv) {
50395
50862
  let forceJob = false;
50396
50863
  let epicId;
50397
50864
  let forceStaleBase = false;
50865
+ let acceptStaleBase = false;
50866
+ let staleBaseReason;
50867
+ let baseSha;
50868
+ let baseRef;
50398
50869
  for (let i = 1;i < argv.length; i++) {
50399
50870
  const token = argv[i];
50400
50871
  if (token === "--prompt" && argv[i + 1]) {
@@ -50463,11 +50934,35 @@ async function parseArgs8(argv) {
50463
50934
  epicId = argv[++i];
50464
50935
  continue;
50465
50936
  }
50937
+ if (token === "--base-sha" && argv[i + 1]) {
50938
+ baseSha = argv[++i];
50939
+ continue;
50940
+ }
50941
+ if (token === "--base-ref" && argv[i + 1]) {
50942
+ baseRef = argv[++i];
50943
+ continue;
50944
+ }
50945
+ if (token === "--reason" && argv[i + 1]) {
50946
+ staleBaseReason = argv[++i];
50947
+ continue;
50948
+ }
50949
+ if (token === "--accept-stale-base") {
50950
+ acceptStaleBase = true;
50951
+ continue;
50952
+ }
50466
50953
  if (token === "--force-stale-base") {
50954
+ process.stderr.write(`[deprecated] --force-stale-base is deprecated; use --accept-stale-base --reason <text>. Aliased for one release.
50955
+ `);
50467
50956
  forceStaleBase = true;
50957
+ acceptStaleBase = true;
50958
+ staleBaseReason ??= "deprecated --force-stale-base";
50468
50959
  continue;
50469
50960
  }
50470
50961
  }
50962
+ if (acceptStaleBase && !staleBaseReason?.trim()) {
50963
+ console.error("Error: --accept-stale-base requires --reason <text>.");
50964
+ process.exit(1);
50965
+ }
50471
50966
  if (worktree && reuseJobId !== undefined) {
50472
50967
  console.error("Error: --worktree and --job are mutually exclusive. Use one or the other.");
50473
50968
  process.exit(1);
@@ -50515,7 +51010,11 @@ async function parseArgs8(argv) {
50515
51010
  reuseJobId,
50516
51011
  forceJob,
50517
51012
  epicId,
50518
- forceStaleBase
51013
+ forceStaleBase,
51014
+ acceptStaleBase,
51015
+ staleBaseReason,
51016
+ baseSha,
51017
+ baseRef
50519
51018
  };
50520
51019
  }
50521
51020
  function readBeadSummary(beadId) {
@@ -50621,7 +51120,7 @@ function assertNoStaleBaseSiblings(beadId, forceStaleBase) {
50621
51120
  if (staleSiblings.length === 0)
50622
51121
  return;
50623
51122
  if (forceStaleBase) {
50624
- process.stderr.write(dim11(`[stale-base guard bypassed: ${staleSiblings.length} unmerged sibling chain(s) under epic ${epicId}]
51123
+ process.stderr.write(dim11(`[stale-base guard accepted: ${staleSiblings.length} unmerged sibling chain(s) under epic ${epicId}]
50625
51124
  `));
50626
51125
  return;
50627
51126
  }
@@ -50630,14 +51129,14 @@ function assertNoStaleBaseSiblings(beadId, forceStaleBase) {
50630
51129
  throw new Error(`Refusing worktree dispatch for bead '${beadId}': epic '${epicId}' has unmerged sibling chains with substantive commits.
50631
51130
  ` + `${lines}
50632
51131
  ` + `Publish the epic first: sp epic merge ${epicId}
50633
- ` + `If intentional, rerun with --force-stale-base.`);
51132
+ ` + `If intentional, rerun with --accept-stale-base --reason <text>.`);
50634
51133
  } finally {
50635
51134
  sqliteClient.close();
50636
51135
  }
50637
51136
  }
50638
51137
  function resolveWorkingDirectory(args, jobsDir, permissionRequired, readStatus) {
50639
51138
  if (args.worktree) {
50640
- assertNoStaleBaseSiblings(args.beadId, args.forceStaleBase);
51139
+ assertNoStaleBaseSiblings(args.beadId, args.acceptStaleBase);
50641
51140
  const info = provisionWorktree({
50642
51141
  beadId: args.beadId,
50643
51142
  specialistName: args.name
@@ -50766,6 +51265,111 @@ function formatFooterModel2(backend, model) {
50766
51265
  function shellQuote2(value) {
50767
51266
  return `'${value.replace(/'/g, `'\\''`)}'`;
50768
51267
  }
51268
+ function buildTmuxLiveFeedCommand(options2) {
51269
+ const handoffPath = shellQuote2(options2.handoffPath);
51270
+ const logPath = shellQuote2(`${options2.handoffPath}.log`);
51271
+ const script = [
51272
+ `cd ${shellQuote2(options2.cwd)}`,
51273
+ `(${options2.runCommand}) > ${logPath} 2>&1 & run_pid=$!`,
51274
+ "job_id=",
51275
+ `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`,
51276
+ `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`,
51277
+ `cat ${logPath} 2>/dev/null || true`,
51278
+ 'wait "$run_pid"',
51279
+ "exit $?"
51280
+ ].join("; ");
51281
+ return `/bin/bash -c ${shellQuote2(script)}`;
51282
+ }
51283
+ function recordTmuxLiveFeedStarted(options2) {
51284
+ const sqliteClient = createObservabilitySqliteClient(options2.cwd);
51285
+ if (!sqliteClient)
51286
+ return;
51287
+ try {
51288
+ sqliteClient.appendEvent(options2.jobId, options2.specialist, options2.beadId, {
51289
+ t: Date.now(),
51290
+ type: "meta",
51291
+ model: "tmux_live_feed_started",
51292
+ backend: "cli.run",
51293
+ source: "cli.run",
51294
+ data: {
51295
+ component: "cli.run",
51296
+ event: "tmux_live_feed_started",
51297
+ job_id: options2.jobId,
51298
+ tmux_session: options2.tmuxSession,
51299
+ command: "sp feed <job> --follow",
51300
+ outcome: "started"
51301
+ }
51302
+ });
51303
+ } catch {} finally {
51304
+ sqliteClient.close();
51305
+ }
51306
+ }
51307
+ function runGit2(cwd, args) {
51308
+ return execSync5(["git", ...args.map(shellQuote2)].join(" "), {
51309
+ cwd,
51310
+ stdio: "pipe",
51311
+ encoding: "utf-8",
51312
+ timeout: 1e4
51313
+ }).trim();
51314
+ }
51315
+ function formatErrorMessage(error2) {
51316
+ return error2 instanceof Error ? error2.message : String(error2);
51317
+ }
51318
+ function runGitForBasePin(cwd, args, runArgs) {
51319
+ try {
51320
+ return runGit2(cwd, args);
51321
+ } catch (error2) {
51322
+ const envelope = {
51323
+ ok: false,
51324
+ error_code: "base_fetch_failed",
51325
+ blocked_by: ["fetch_or_resolve_failure"],
51326
+ next_safe_action: "verify network/remote/declared base ref is reachable, or rerun with --accept-stale-base --reason <text> if intentional",
51327
+ base_ref: runArgs.baseRef ?? null,
51328
+ base_sha: runArgs.baseSha ?? null,
51329
+ worktree_path: cwd,
51330
+ underlying_error: formatErrorMessage(error2)
51331
+ };
51332
+ throw new Error(JSON.stringify(envelope));
51333
+ }
51334
+ }
51335
+ function resolveBasePin(args, worktreePath) {
51336
+ if (!worktreePath || !args.worktree && !args.baseSha)
51337
+ return;
51338
+ const baseRef = args.baseRef?.trim();
51339
+ if (baseRef) {
51340
+ runGitForBasePin(worktreePath, ["fetch", "origin", baseRef], args);
51341
+ } else {
51342
+ runGitForBasePin(worktreePath, ["fetch", "origin"], args);
51343
+ }
51344
+ const baseShaObserved = runGitForBasePin(worktreePath, ["rev-parse", baseRef ? "FETCH_HEAD" : "refs/remotes/origin/HEAD"], args);
51345
+ const baseShaPinned = args.baseSha ?? baseShaObserved;
51346
+ const currentSha = runGitForBasePin(worktreePath, ["rev-parse", "HEAD"], args);
51347
+ const branch = runGitForBasePin(worktreePath, ["rev-parse", "--abbrev-ref", "HEAD"], args);
51348
+ const commitsBehindText = runGitForBasePin(worktreePath, ["rev-list", "--count", `${currentSha}..${baseShaPinned}`], args);
51349
+ const commitsBehind = Number.parseInt(commitsBehindText, 10) || 0;
51350
+ const hasStaleBase = currentSha !== baseShaPinned || baseShaObserved !== baseShaPinned;
51351
+ if (!hasStaleBase) {
51352
+ return { baseShaPinned, baseShaObserved, currentSha, branch, commitsBehind, override: false };
51353
+ }
51354
+ if (args.acceptStaleBase) {
51355
+ process.stderr.write(dim11(`[stale-base guard accepted: base_sha_pinned=${baseShaPinned} current_sha=${currentSha} reason=${args.staleBaseReason}]
51356
+ `));
51357
+ return { baseShaPinned, baseShaObserved, currentSha, branch, commitsBehind, override: true };
51358
+ }
51359
+ const envelope = {
51360
+ ok: false,
51361
+ error_code: "stale_base",
51362
+ blocked_by: ["worktree_base_mismatch"],
51363
+ next_safe_action: "Fetch/recreate worktree from declared base, or rerun with --accept-stale-base --reason <text> if divergence is intentional.",
51364
+ base_sha_pinned: baseShaPinned,
51365
+ base_sha_observed: baseShaObserved,
51366
+ current_sha: currentSha,
51367
+ branch,
51368
+ worktree_path: worktreePath,
51369
+ commits_behind: commitsBehind
51370
+ };
51371
+ throw new Error(JSON.stringify(envelope));
51372
+ }
50769
51373
  function extractReviewedJobIdOverride(prompt) {
50770
51374
  const match = prompt.match(/(?:^|\n)\s*reviewed_job_id\s*:\s*([^\n]+)/i);
50771
51375
  const candidate = match?.[1]?.trim();
@@ -50950,15 +51554,23 @@ async function run16() {
50950
51554
  const launchStartedAt = Date.now();
50951
51555
  const innerArgs = process.argv.slice(2).filter((a) => a !== "--background");
50952
51556
  const cmd = `${process.execPath} ${process.argv[1]} ${innerArgs.map(shellQuote2).join(" ")}`;
50953
- const tmuxCmd = `/bin/bash -c ${shellQuote2(`cd ${shellQuote2(cwd)} && exec ${cmd}`)}`;
50954
51557
  let childPid;
50955
51558
  let childExitCode;
50956
51559
  let childExitPromise;
50957
51560
  let handoffPath;
51561
+ let tmuxSessionName;
50958
51562
  if (isTmuxAvailable()) {
50959
51563
  const suffix = randomBytes(3).toString("hex");
50960
51564
  const sessionName = buildSessionName(args.name, suffix);
51565
+ tmuxSessionName = sessionName;
50961
51566
  handoffPath = join33(jobsDir2, `.bg-job-id-${sessionName}`);
51567
+ const feedCommandPrefix = [process.execPath, process.argv[1], "feed"].map(shellQuote2).join(" ");
51568
+ const tmuxCmd = buildTmuxLiveFeedCommand({
51569
+ cwd,
51570
+ runCommand: cmd,
51571
+ handoffPath,
51572
+ feedCommandPrefix
51573
+ });
50962
51574
  createTmuxSession(sessionName, cwd, tmuxCmd, { [JOB_ID_HANDOFF_PATH_ENV]: handoffPath });
50963
51575
  } else {
50964
51576
  const child = cpSpawn(process.execPath, [process.argv[1], ...innerArgs], {
@@ -51020,6 +51632,15 @@ async function run16() {
51020
51632
  jobId = resolveNewestJobIdFromJobsDir(jobsDir2, oldLatest, launchStartedAt - 1000);
51021
51633
  }
51022
51634
  if (jobId) {
51635
+ if (tmuxSessionName) {
51636
+ recordTmuxLiveFeedStarted({
51637
+ cwd,
51638
+ jobId,
51639
+ specialist: args.name,
51640
+ beadId: args.beadId,
51641
+ tmuxSession: tmuxSessionName
51642
+ });
51643
+ }
51023
51644
  process.stdout.write(`${jobId}
51024
51645
  `);
51025
51646
  } else {
@@ -51045,7 +51666,9 @@ async function run16() {
51045
51666
  runOptions: { name: args.name, prompt },
51046
51667
  jobsDir
51047
51668
  });
51048
- const { workingDirectory, reusedFromJobId, worktreeOwnerJobId, inferredBeadId } = resolveWorkingDirectory({ ...args, worktree: useWorktree }, jobsDir, perm, (jobId) => statusReader.readStatus(jobId));
51669
+ const effectiveArgs = { ...args, worktree: useWorktree };
51670
+ const { workingDirectory, reusedFromJobId, worktreeOwnerJobId, inferredBeadId } = resolveWorkingDirectory(effectiveArgs, jobsDir, perm, (jobId) => statusReader.readStatus(jobId));
51671
+ const basePin = resolveBasePin(effectiveArgs, workingDirectory);
51049
51672
  await statusReader.dispose();
51050
51673
  if (!effectiveBeadId && inferredBeadId) {
51051
51674
  effectiveBeadId = inferredBeadId;
@@ -51098,6 +51721,7 @@ async function run16() {
51098
51721
  circuitBreaker,
51099
51722
  beadsClient,
51100
51723
  workingDirectory,
51724
+ basePin,
51101
51725
  reusedFromJobId,
51102
51726
  worktreeOwnerJobId,
51103
51727
  effectiveBeadId,
@@ -55213,7 +55837,8 @@ async function run17() {
55213
55837
  jobs = supervisor.listJobs().filter(isStandaloneJob);
55214
55838
  }
55215
55839
  if (jobId) {
55216
- const selectedJob = supervisor?.readStatus(jobId) ?? null;
55840
+ const selectedStatus = loadStatuses().find((status) => status.id.startsWith(jobId)) ?? supervisor?.readStatus(jobId) ?? null;
55841
+ const selectedJob = selectedStatus ? { ...selectedStatus, is_dead: isJobDead(selectedStatus) } : null;
55217
55842
  if (!selectedJob || !isStandaloneJob(selectedJob)) {
55218
55843
  if (jsonMode) {
55219
55844
  console.log(JSON.stringify({ error: `Job not found: ${jobId}` }, null, 2));
@@ -55364,6 +55989,7 @@ ${bold11("specialists status")}
55364
55989
  var init_status2 = __esm(() => {
55365
55990
  init_loader();
55366
55991
  init_supervisor();
55992
+ init_status_load();
55367
55993
  init_job_root();
55368
55994
  init_observability_sqlite();
55369
55995
  init_format_helpers();
@@ -55416,7 +56042,8 @@ function parseArgs9(argv) {
55416
56042
  "--active",
55417
56043
  "--running",
55418
56044
  "--mine",
55419
- "--health"
56045
+ "--health",
56046
+ "--needs-attention"
55420
56047
  ]);
55421
56048
  const valueFlags = new Set(["--node", "--bead", "--since"]);
55422
56049
  let nodeId;
@@ -55459,6 +56086,7 @@ function parseArgs9(argv) {
55459
56086
  running: argv.includes("--running") || argv.includes("--active"),
55460
56087
  mine: argv.includes("--mine"),
55461
56088
  health: argv.includes("--health"),
56089
+ needsAttention: argv.includes("--needs-attention"),
55462
56090
  beadFilter,
55463
56091
  sinceMs,
55464
56092
  nodeId,
@@ -55495,6 +56123,7 @@ function toJobNode(job) {
55495
56123
  context_health: job.context_health,
55496
56124
  metrics: job.metrics,
55497
56125
  startup_payload_json: job.startup_payload_json ?? null,
56126
+ pr_classification: job.pr_classification,
55498
56127
  children: []
55499
56128
  };
55500
56129
  }
@@ -55844,7 +56473,8 @@ function renderJobLine(job, beadTitles, prefix, connector) {
55844
56473
  const width = Math.max(80, (process.stdout.columns ?? 160) - prefix.length - connector.length);
55845
56474
  const themed = renderJobRow(consoleJob, width, 0, false);
55846
56475
  const body = themed.startsWith(" ") ? themed.slice(2) : themed;
55847
- return `${prefix}${connector}${body}`;
56476
+ const driftBadge = job.pr_classification && job.pr_classification !== "clean" ? red2(` [drift:${job.pr_classification}]`) : "";
56477
+ return `${prefix}${connector}${body}${driftBadge}`;
55848
56478
  }
55849
56479
  function renderTreeNodes(nodes, beadTitles, prefix, renderedJobIds) {
55850
56480
  for (let i = 0;i < nodes.length; i++) {
@@ -56049,7 +56679,7 @@ function renderHuman(jobs, nodes, trees, all, includeTerminal, epicReadiness, he
56049
56679
  const includeSuffix = all ? " " + dim9("\xB7 include terminal") : "";
56050
56680
  console.log(renderStatsLine(statsSnapshot, width) + includeSuffix);
56051
56681
  }
56052
- function statusFromRunComplete(status) {
56682
+ function statusFromRunComplete2(status) {
56053
56683
  if (status === "COMPLETE")
56054
56684
  return "done";
56055
56685
  if (status === "CANCELLED")
@@ -56086,7 +56716,7 @@ function synthesizeStatusFromEvents(jobId) {
56086
56716
  return {
56087
56717
  id: jobId,
56088
56718
  specialist,
56089
- status: statusFromRunComplete(runComplete?.status),
56719
+ status: statusFromRunComplete2(runComplete?.status),
56090
56720
  started_at_ms: first.t,
56091
56721
  elapsed_s: runComplete?.elapsed_s ?? Math.max(0, Math.round((last.t - first.t) / 1000)),
56092
56722
  last_event_at_ms: last.t,
@@ -56232,7 +56862,10 @@ function renderJson(jobs, nodes, trees, _all, epicReadiness, args, health) {
56232
56862
  context_health: job.context_health,
56233
56863
  startup_payload_json: job.startup_payload_json ?? null,
56234
56864
  payload_kb: formatPayloadStats2(job.startup_payload_json).payload_kb,
56235
- payload_tokens: formatPayloadStats2(job.startup_payload_json).payload_tokens
56865
+ payload_tokens: formatPayloadStats2(job.startup_payload_json).payload_tokens,
56866
+ attention_reasons: [
56867
+ ...job.pr_classification && job.pr_classification !== "clean" ? [`pr_drift:${job.pr_classification}`] : []
56868
+ ]
56236
56869
  })),
56237
56870
  nodes,
56238
56871
  trees,
@@ -56272,6 +56905,8 @@ function render(args) {
56272
56905
  return false;
56273
56906
  if (mineBeadIds && (!job.bead_id || !mineBeadIds.has(job.bead_id)))
56274
56907
  return false;
56908
+ if (args.needsAttention && (!job.pr_classification || job.pr_classification === "clean"))
56909
+ return false;
56275
56910
  const cleaned = isPsCleaned2(job);
56276
56911
  if (args.all)
56277
56912
  return true;
@@ -56280,7 +56915,7 @@ function render(args) {
56280
56915
  if (cleaned && args.includeCleaned && TERMINAL_STATES2.includes(job.status))
56281
56916
  return true;
56282
56917
  if (job.is_dead)
56283
- return false;
56918
+ return true;
56284
56919
  if (ACTIVE_STATES2.includes(job.status))
56285
56920
  return true;
56286
56921
  if (args.active)
@@ -57101,6 +57736,8 @@ function getHumanEventKey2(event) {
57101
57736
  return `finish_reason:${event.finish_reason}:${event.source}`;
57102
57737
  case "turn_summary":
57103
57738
  return `turn_summary:${event.turn_index}`;
57739
+ case "text":
57740
+ return `text:${event.seq ?? ""}:${event.char_count ?? ""}:${event.content?.slice(0, 80) ?? ""}`;
57104
57741
  case "compaction":
57105
57742
  case "retry":
57106
57743
  return `${event.type}:${event.phase}`;
@@ -59522,7 +60159,7 @@ var exports_clean = {};
59522
60159
  __export(exports_clean, {
59523
60160
  run: () => run27
59524
60161
  });
59525
- import { existsSync as existsSync37, readFileSync as readFileSync34, readdirSync as readdirSync18, rmSync as rmSync6, statSync as statSync12 } from "fs";
60162
+ import { existsSync as existsSync37, readFileSync as readFileSync34, readdirSync as readdirSync18, rmSync as rmSync7, statSync as statSync12 } from "fs";
59526
60163
  import { join as join42 } from "path";
59527
60164
  function parseDuration2(raw) {
59528
60165
  const match = /^(\d+)(ms|s|m|h|d)$/i.exec(raw.trim());
@@ -59932,6 +60569,30 @@ async function reapStaleSpecialistJobs(jobs, dryRun) {
59932
60569
  } catch {}
59933
60570
  } catch {}
59934
60571
  }
60572
+ if (job.reason === "terminal-alive") {
60573
+ try {
60574
+ process.kill(-job.pid, "SIGTERM");
60575
+ } catch (err) {
60576
+ if (err?.code !== "ESRCH") {
60577
+ try {
60578
+ process.kill(job.pid, "SIGTERM");
60579
+ } catch {}
60580
+ }
60581
+ }
60582
+ await new Promise((resolve11) => setTimeout(resolve11, 500));
60583
+ try {
60584
+ process.kill(job.pid, 0);
60585
+ try {
60586
+ process.kill(-job.pid, "SIGKILL");
60587
+ } catch {
60588
+ try {
60589
+ process.kill(job.pid, "SIGKILL");
60590
+ } catch {}
60591
+ }
60592
+ } catch {}
60593
+ reapedCount += 1;
60594
+ continue;
60595
+ }
59935
60596
  sqliteClient.markSpecialistJobCancelled(job.jobId, `cleanup: stale-reaper:${job.reason}`);
59936
60597
  reapedCount += 1;
59937
60598
  }
@@ -59949,7 +60610,7 @@ function printStaleJobSummary(reapedCount) {
59949
60610
  }
59950
60611
  function deleteJobDirectories(jobs) {
59951
60612
  for (const job of jobs) {
59952
- rmSync6(job.directoryPath, { recursive: true, force: true });
60613
+ rmSync7(job.directoryPath, { recursive: true, force: true });
59953
60614
  }
59954
60615
  return jobs.length;
59955
60616
  }
@@ -60582,7 +61243,7 @@ var init_attach = __esm(() => {
60582
61243
  });
60583
61244
 
60584
61245
  // src/specialist/drift-detector.ts
60585
- import { existsSync as existsSync38, readFileSync as readFileSync35, readdirSync as readdirSync19, rmSync as rmSync7 } from "fs";
61246
+ import { existsSync as existsSync38, readFileSync as readFileSync35, readdirSync as readdirSync19, rmSync as rmSync8 } from "fs";
60586
61247
  import { join as join43, resolve as resolve11, relative as relative3 } from "path";
60587
61248
  function listFiles(root) {
60588
61249
  if (!existsSync38(root))
@@ -60678,7 +61339,7 @@ function pruneStaleDefaults(repoRoot, dryRun, keepDiverged = false) {
60678
61339
  const targets = detectDriftForRepo(repoRoot).filter((f) => f.scope === "default" && (keepDiverged ? f.bytes_equal === true : true)).map((f) => f.path);
60679
61340
  if (!dryRun) {
60680
61341
  for (const target of targets)
60681
- rmSync7(target, { recursive: true, force: true });
61342
+ rmSync8(target, { recursive: true, force: true });
60682
61343
  }
60683
61344
  return targets;
60684
61345
  }
@@ -61000,6 +61661,162 @@ async function run34() {
61000
61661
  }
61001
61662
  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
61663
 
61664
+ // src/specialist/pr-drift-refresh.ts
61665
+ import { execSync as execSync6 } from "child_process";
61666
+ import { createHash as createHash6 } from "crypto";
61667
+ function hashSummary(input2) {
61668
+ return createHash6("sha256").update(input2).digest("hex").slice(0, 16);
61669
+ }
61670
+ function parsePrNumberFromUrl(prUrl) {
61671
+ const match = prUrl.match(/\/pull\/(\d+)$/);
61672
+ if (match)
61673
+ return match[1];
61674
+ if (/^\d+$/.test(prUrl))
61675
+ return prUrl;
61676
+ return;
61677
+ }
61678
+ function deriveClassification(raw) {
61679
+ const mergeState = (raw.mergeStateStatus ?? "").toUpperCase();
61680
+ const state = (raw.state ?? "").toLowerCase();
61681
+ if (state === "merged" || state === "closed")
61682
+ return "stale";
61683
+ switch (mergeState) {
61684
+ case "BEHIND":
61685
+ return "needs-rebase";
61686
+ case "DIRTY":
61687
+ return "conflicted";
61688
+ case "BLOCKED":
61689
+ return "blocked";
61690
+ case "CLEAN":
61691
+ case "HAS_HOOKS":
61692
+ case "UNSTABLE":
61693
+ return "clean";
61694
+ default:
61695
+ return "unknown";
61696
+ }
61697
+ }
61698
+ function classifyError(err) {
61699
+ const msg = err instanceof Error ? err.message : String(err);
61700
+ const lower = msg.toLowerCase();
61701
+ if (lower.includes("enoent") || lower.includes("command not found") || lower.includes("no such file")) {
61702
+ return { kind: "gh_unavailable", summary: hashSummary(msg) };
61703
+ }
61704
+ if (lower.includes("not found") || lower.includes("could not resolve")) {
61705
+ return { kind: "no_pr", summary: hashSummary(msg) };
61706
+ }
61707
+ if (lower.includes("json") || lower.includes("parse")) {
61708
+ return { kind: "parse_error", summary: hashSummary(msg) };
61709
+ }
61710
+ if (lower.includes("timeout") || lower.includes("econnrefused") || lower.includes("network")) {
61711
+ return { kind: "network", summary: hashSummary(msg) };
61712
+ }
61713
+ return { kind: "network", summary: hashSummary(msg) };
61714
+ }
61715
+ async function refreshPrDriftForJob(opts) {
61716
+ const { jobId, prUrl, headSha, client } = opts;
61717
+ const now = Date.now();
61718
+ const prNumber = parsePrNumberFromUrl(prUrl);
61719
+ if (!prNumber) {
61720
+ const patch2 = { pr_classification: "unknown", pr_drift_checked_at_ms: now };
61721
+ client.updatePrDriftState(jobId, patch2);
61722
+ return { ok: false, classification: "unknown", error_kind: "parse_error", error_summary: hashSummary("unparseable-pr-url") };
61723
+ }
61724
+ let stdout;
61725
+ try {
61726
+ stdout = execSync6(`gh pr view ${prNumber} --json state,mergeable,mergeStateStatus,baseRefName,baseRefOid,headRefOid,url`, { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "pipe"] });
61727
+ } catch (err) {
61728
+ const { kind, summary } = classifyError(err);
61729
+ const patch2 = { pr_classification: "unknown", pr_drift_checked_at_ms: now };
61730
+ client.updatePrDriftState(jobId, patch2);
61731
+ return { ok: false, classification: "unknown", error_kind: kind, error_summary: summary };
61732
+ }
61733
+ let raw;
61734
+ try {
61735
+ raw = JSON.parse(stdout.trim());
61736
+ } catch (err) {
61737
+ const summary = hashSummary(err instanceof Error ? err.message : String(err));
61738
+ const patch2 = { pr_classification: "unknown", pr_drift_checked_at_ms: now };
61739
+ client.updatePrDriftState(jobId, patch2);
61740
+ return { ok: false, classification: "unknown", error_kind: "parse_error", error_summary: summary };
61741
+ }
61742
+ const classification = deriveClassification(raw);
61743
+ const patch = {
61744
+ pr_url: raw.url ?? prUrl,
61745
+ pr_head_sha: raw.headRefOid ?? headSha ?? null,
61746
+ pr_state: raw.state ?? null,
61747
+ pr_merge_state: raw.mergeStateStatus ?? null,
61748
+ pr_classification: classification,
61749
+ pr_base_ref: raw.baseRefName ?? null,
61750
+ pr_base_sha: raw.baseRefOid ?? null,
61751
+ pr_drift_checked_at_ms: now
61752
+ };
61753
+ client.updatePrDriftState(jobId, patch);
61754
+ return { ok: true, classification, raw };
61755
+ }
61756
+ var init_pr_drift_refresh = () => {};
61757
+
61758
+ // src/specialist/dead-job-audit.ts
61759
+ function defaultIsPidAlive2(pid) {
61760
+ try {
61761
+ process.kill(pid, 0);
61762
+ return true;
61763
+ } catch (error2) {
61764
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ESRCH") {
61765
+ return false;
61766
+ }
61767
+ return true;
61768
+ }
61769
+ }
61770
+ function auditDeadJobs(opts) {
61771
+ const dryRun = opts.dryRun ?? false;
61772
+ const nowMs = opts.nowMs ?? Date.now();
61773
+ const isPidAlive2 = opts.isPidAlive ?? defaultIsPidAlive2;
61774
+ const minAgeMs = opts.minAgeMs ?? 60000;
61775
+ const rows = opts.client.listStaleSpecialistJobs({ minAgeMs, nowMs });
61776
+ const found = [];
61777
+ let cancelled = 0;
61778
+ for (const row of rows) {
61779
+ if (isPidAlive2(row.pid))
61780
+ continue;
61781
+ const age_ms = Math.max(0, nowMs - row.updated_at_ms);
61782
+ found.push({
61783
+ job_id: row.job_id,
61784
+ pid: row.pid,
61785
+ reason: "container-restart-orphan",
61786
+ age_ms
61787
+ });
61788
+ if (!dryRun) {
61789
+ opts.client.markSpecialistJobCancelled(row.job_id, "container-restart-orphan");
61790
+ cancelled += 1;
61791
+ opts.client.appendForensicEvent(row.job_id, row.specialist, row.bead_id ?? undefined, createForensicEvent({
61792
+ event_family: "lifecycle",
61793
+ event_name: "dead_declared",
61794
+ resource: {
61795
+ service_namespace: "xtrm",
61796
+ service_name: "specialists",
61797
+ service_component: "dead-job-audit",
61798
+ deployment_environment: "development",
61799
+ repo: "specialists",
61800
+ participant_kind: "specialist",
61801
+ participant_role: row.specialist
61802
+ },
61803
+ correlation: { job_id: row.job_id, bead_id: row.bead_id ?? undefined },
61804
+ body: {
61805
+ job_id: row.job_id,
61806
+ pid: row.pid,
61807
+ age_ms,
61808
+ reason: "container-restart-orphan",
61809
+ dry_run: dryRun
61810
+ }
61811
+ }));
61812
+ }
61813
+ }
61814
+ return { dryRun, found, cancelled };
61815
+ }
61816
+ var init_dead_job_audit = __esm(() => {
61817
+ init_forensic_events();
61818
+ });
61819
+
61003
61820
  // src/cli/doctor.ts
61004
61821
  var exports_doctor = {};
61005
61822
  __export(exports_doctor, {
@@ -61011,7 +61828,7 @@ __export(exports_doctor, {
61011
61828
  compareVersions: () => compareVersions2,
61012
61829
  cleanupProcesses: () => cleanupProcesses
61013
61830
  });
61014
- import { createHash as createHash6 } from "crypto";
61831
+ import { createHash as createHash7 } from "crypto";
61015
61832
  import { spawnSync as spawnSync23 } from "child_process";
61016
61833
  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
61834
  import { dirname as dirname16, join as join44, relative as relative4, resolve as resolve13 } from "path";
@@ -61191,7 +62008,7 @@ function checkVersion() {
61191
62008
  return true;
61192
62009
  }
61193
62010
  function hashFile(path3) {
61194
- const hash = createHash6("sha256");
62011
+ const hash = createHash7("sha256");
61195
62012
  hash.update(readFileSync36(path3));
61196
62013
  return hash.digest("hex");
61197
62014
  }
@@ -61524,7 +62341,7 @@ function checkClaudeMdFragments() {
61524
62341
  return allOk;
61525
62342
  }
61526
62343
  function parseDoctorArgs(argv) {
61527
- const opts = { json: false, drift: false, specialists: false };
62344
+ const opts = { json: false, drift: false, specialists: false, pr_drift: false, reap_dead_jobs: false, dry_run: false };
61528
62345
  for (let i = 0;i < argv.length; i += 1) {
61529
62346
  const token = argv[i];
61530
62347
  if (token === "--json") {
@@ -61539,6 +62356,18 @@ function parseDoctorArgs(argv) {
61539
62356
  opts.specialists = true;
61540
62357
  continue;
61541
62358
  }
62359
+ if (token === "--pr-drift") {
62360
+ opts.pr_drift = true;
62361
+ continue;
62362
+ }
62363
+ if (token === "--reap-dead-jobs") {
62364
+ opts.reap_dead_jobs = true;
62365
+ continue;
62366
+ }
62367
+ if (token === "--dry-run") {
62368
+ opts.dry_run = true;
62369
+ continue;
62370
+ }
61542
62371
  if (token === "--root") {
61543
62372
  const value = argv[i + 1];
61544
62373
  if (!value || value.startsWith("--"))
@@ -61833,6 +62662,128 @@ function checkZombieJobs() {
61833
62662
  }
61834
62663
  return result.zombies === 0;
61835
62664
  }
62665
+ async function runDoctorPrDrift(json) {
62666
+ const client = createObservabilitySqliteClient();
62667
+ if (!client) {
62668
+ if (json) {
62669
+ console.log(JSON.stringify({ jobs: [], error: "observability sqlite unavailable" }));
62670
+ } else {
62671
+ console.error("observability sqlite unavailable");
62672
+ }
62673
+ process.exitCode = 1;
62674
+ return;
62675
+ }
62676
+ try {
62677
+ const jobs = client.listJobsNeedingPrDriftRefresh();
62678
+ const results = [];
62679
+ for (const job of jobs) {
62680
+ const startedAt = Date.now();
62681
+ console.error(JSON.stringify({
62682
+ component: "pr_drift",
62683
+ event: "refresh_attempted",
62684
+ job_id: job.job_id,
62685
+ duration_ms: 0,
62686
+ gh_stderr_hash: "",
62687
+ branch: job.branch ?? null,
62688
+ checked_at_ms: startedAt
62689
+ }));
62690
+ const result = await refreshPrDriftForJob({
62691
+ jobId: job.job_id,
62692
+ prUrl: job.pr_url,
62693
+ headSha: job.pr_head_sha ?? undefined,
62694
+ client
62695
+ });
62696
+ const durationMs = Date.now() - startedAt;
62697
+ const classification = result.classification;
62698
+ const ghStderrHash = result.error_summary ? result.error_summary.slice(0, 8) : "";
62699
+ const eventOut = {
62700
+ component: "pr_drift",
62701
+ event: result.ok ? "refresh_completed" : "refresh_failed",
62702
+ job_id: job.job_id,
62703
+ duration_ms: durationMs,
62704
+ gh_stderr_hash: ghStderrHash,
62705
+ pr_classification: classification,
62706
+ branch: job.branch ?? null,
62707
+ checked_at_ms: startedAt
62708
+ };
62709
+ console.error(JSON.stringify(eventOut));
62710
+ results.push({
62711
+ job_id: job.job_id,
62712
+ classification,
62713
+ pr_url: job.pr_url,
62714
+ ...result.error_kind ? { error_kind: result.error_kind } : {},
62715
+ duration_ms: durationMs
62716
+ });
62717
+ }
62718
+ if (json) {
62719
+ console.log(JSON.stringify({ jobs: results }, null, 2));
62720
+ } else {
62721
+ console.log(`
62722
+ ${bold13("specialists doctor --pr-drift")}
62723
+ `);
62724
+ if (results.length === 0) {
62725
+ ok3("No PR-linked jobs need drift refresh");
62726
+ } else {
62727
+ for (const r of results) {
62728
+ 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("?");
62729
+ const suffix = r.error_kind ? ` ${dim14(`(${r.error_kind})`)}` : "";
62730
+ console.log(` ${icon} ${r.job_id} ${r.classification}${suffix}`);
62731
+ }
62732
+ }
62733
+ console.log("");
62734
+ }
62735
+ } finally {
62736
+ client.close();
62737
+ }
62738
+ }
62739
+ async function runDoctorReapDeadJobs(opts) {
62740
+ const client = createObservabilitySqliteClient();
62741
+ if (!client) {
62742
+ if (opts.json) {
62743
+ console.log(JSON.stringify({ dryRun: opts.dry_run, found: [], cancelled: 0, error: "observability sqlite unavailable" }));
62744
+ } else {
62745
+ console.error("observability sqlite unavailable");
62746
+ }
62747
+ process.exitCode = 1;
62748
+ return;
62749
+ }
62750
+ try {
62751
+ const result = auditDeadJobs({
62752
+ client,
62753
+ dryRun: opts.dry_run,
62754
+ nowMs: Date.now()
62755
+ });
62756
+ for (const finding of result.found) {
62757
+ const structuredLog = {
62758
+ component: "dead_job_audit",
62759
+ event: "dead_declared",
62760
+ job_id: finding.job_id,
62761
+ age_ms: finding.age_ms,
62762
+ dry_run: opts.dry_run
62763
+ };
62764
+ console.error(JSON.stringify(structuredLog));
62765
+ }
62766
+ if (opts.json) {
62767
+ console.log(JSON.stringify(result, null, 2));
62768
+ } else {
62769
+ console.log(`
62770
+ ${bold13("specialists doctor --reap-dead-jobs")}
62771
+ `);
62772
+ if (result.found.length === 0) {
62773
+ ok3("No dead running/waiting jobs found");
62774
+ } else {
62775
+ const action = opts.dry_run ? "Would cancel" : "Cancelled";
62776
+ console.log(` ${yellow12("\u25CB")} ${result.found.length} dead job(s) found (${result.cancelled} ${action})`);
62777
+ for (const f of result.found) {
62778
+ console.log(` - ${f.job_id} pid=${f.pid} age=${Math.round(f.age_ms / 1000)}s`);
62779
+ }
62780
+ }
62781
+ console.log("");
62782
+ }
62783
+ } finally {
62784
+ client.close();
62785
+ }
62786
+ }
61836
62787
  async function run35(argv = process.argv.slice(3)) {
61837
62788
  const subcommand = argv[0];
61838
62789
  if (subcommand === "orphans") {
@@ -61844,6 +62795,10 @@ async function run35(argv = process.argv.slice(3)) {
61844
62795
  renderDriftTable(opts.root ?? process.cwd(), opts.json);
61845
62796
  return;
61846
62797
  }
62798
+ if (opts.pr_drift) {
62799
+ await runDoctorPrDrift(opts.json);
62800
+ return;
62801
+ }
61847
62802
  if (opts.specialists) {
61848
62803
  console.log(`
61849
62804
  ${bold13("specialists doctor --specialists")}
@@ -61853,6 +62808,10 @@ ${bold13("specialists doctor --specialists")}
61853
62808
  process.exitCode = overridesOk2 ? 0 : 1;
61854
62809
  return;
61855
62810
  }
62811
+ if (opts.reap_dead_jobs) {
62812
+ await runDoctorReapDeadJobs(opts);
62813
+ return;
62814
+ }
61856
62815
  if (subcommand && subcommand !== "--help" && subcommand !== "-h" && !subcommand.startsWith("--")) {
61857
62816
  console.error(`Unknown doctor subcommand: '${subcommand}'`);
61858
62817
  process.exit(1);
@@ -61887,8 +62846,10 @@ ${bold13("specialists doctor")}
61887
62846
  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
62847
  var init_doctor = __esm(() => {
61889
62848
  init_observability_sqlite();
62849
+ init_pr_drift_refresh();
61890
62850
  init_canonical_asset_resolver();
61891
62851
  init_drift_detector();
62852
+ init_dead_job_audit();
61892
62853
  init_loader();
61893
62854
  init_version_check();
61894
62855
  CWD = process.cwd();
@@ -62067,7 +63028,7 @@ var init_benchmarks = __esm(() => {
62067
63028
  });
62068
63029
 
62069
63030
  // src/specialist/model-probes.ts
62070
- import { createHash as createHash7, randomUUID as randomUUID6 } from "crypto";
63031
+ import { createHash as createHash8, randomUUID as randomUUID6 } from "crypto";
62071
63032
  import { mkdirSync as mkdirSync17, readdirSync as readdirSync21, readFileSync as readFileSync38, writeFileSync as writeFileSync21 } from "fs";
62072
63033
  import { homedir as homedir11 } from "os";
62073
63034
  import { dirname as dirname18, join as join46, resolve as resolve14 } from "path";
@@ -62177,7 +63138,7 @@ function getProbeRunDir(model, specName, cacheDir = join46(homedir11(), ".cache"
62177
63138
  return resolve14(getProbeCanonicalPath(model, specName, cacheDir).replace(/\.json$/u, ""), randomUUID6());
62178
63139
  }
62179
63140
  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);
63141
+ const probeId = createHash8("sha256").update(`${model}\x00${specName}\x00${PROBE_TEMPLATE}`).digest("hex").slice(0, 12);
62181
63142
  return join46(cacheDir, `${sanitizePathSegment(model)}-${sanitizePathSegment(specName)}-${probeId}.json`);
62182
63143
  }
62183
63144
  function sanitizePathSegment(value) {
@@ -62822,7 +63783,7 @@ import { createServer } from "http";
62822
63783
  import { randomUUID as randomUUID7 } from "crypto";
62823
63784
  import { once } from "events";
62824
63785
  import { spawnSync as spawnSync25 } from "child_process";
62825
- import { access, readdir as readdir2, readFile as readFile5, constants } from "fs/promises";
63786
+ import { access, readdir as readdir2, readFile as readFile4, constants } from "fs/promises";
62826
63787
  import { existsSync as existsSync42 } from "fs";
62827
63788
  import { homedir as homedir12 } from "os";
62828
63789
  import { join as join48 } from "path";
@@ -62850,7 +63811,7 @@ async function checkUserDirSpecs(userDir) {
62850
63811
  let validCount = 0;
62851
63812
  for (const file of specFiles) {
62852
63813
  try {
62853
- const content = await readFile5(join48(userDir, file), "utf-8");
63814
+ const content = await readFile4(join48(userDir, file), "utf-8");
62854
63815
  const json = file.endsWith(".json") ? content : null;
62855
63816
  if (!json)
62856
63817
  continue;
@@ -71559,8 +72520,18 @@ async function run40() {
71559
72520
  " --epic <id> Explicit epic membership for this job. Defaults to bead.parent.",
71560
72521
  " Useful for prep jobs belonging to a merge-gated epic.",
71561
72522
  " --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.",
72523
+ " --base-sha <sha> Pin the base SHA the run measures against (specialists-05q.3).",
72524
+ " Recorded on the job observability row as base_sha_pinned.",
72525
+ " When omitted, defaults to the observed remote base tip.",
72526
+ " --base-ref <branch> Base branch ref to fetch before pinning (default: origin/HEAD).",
72527
+ " Combined with --base-sha for deterministic precondition checks.",
72528
+ " --accept-stale-base Override the stale-base precondition gate. Requires --reason.",
72529
+ " Logged in stderr + auditable via the structured refusal envelope",
72530
+ " (specialists-05q.3). Prefer over deprecated --force-stale-base.",
72531
+ " --reason <text> Required justification when --accept-stale-base is set.",
72532
+ " Surfaced in stderr audit line + refusal envelope.",
72533
+ " --force-stale-base [deprecated] Alias for --accept-stale-base with default reason.",
72534
+ " Emits a deprecation warning; removal in a future release.",
71564
72535
  "",
71565
72536
  "Examples:",
71566
72537
  " specialists run debugger --bead unitAI-55d",
@@ -72226,6 +73197,8 @@ async function run40() {
72226
73197
  " 7. zombie job detection",
72227
73198
  " 8. CLAUDE.md fragments (XTRM-MANAGED sentinels) \u2014 delegates to xt claude-sync",
72228
73199
  " 9. drift check for stale managed mirrors (--check-drift / --drift)",
73200
+ " 10. PR drift refresh for tracked jobs (--pr-drift) \u2014 specialists-05q.2",
73201
+ " 11. dead-job audit + reap orphans (--reap-dead-jobs [--dry-run]) \u2014 specialists-05q.4",
72229
73202
  "",
72230
73203
  "Behavior:",
72231
73204
  " - prints fix hints for failing checks",
@@ -72237,10 +73210,26 @@ async function run40() {
72237
73210
  " Category A (specialists runtime) and Category B",
72238
73211
  " (filesystem skills/hooks) are distinct; doctor",
72239
73212
  " covers Category A only",
73213
+ " --pr-drift Refresh PR/base drift state for tracked jobs by",
73214
+ " shelling out to `gh pr view --json` (specialists-05q.2).",
73215
+ " Iterates jobs whose pr_drift_checked_at_ms is stale or null;",
73216
+ " updates pr_state/pr_merge_state/pr_classification/pr_base_sha.",
73217
+ " Classifications: clean | needs-rebase | conflicted | blocked |",
73218
+ " stale | unknown. gh failures classify as unknown (never throws).",
73219
+ " --reap-dead-jobs Audit specialist_jobs rows in active states with dead pids",
73220
+ " and mark them cancelled with reason container-restart-orphan",
73221
+ " (specialists-05q.4). Emits xtrm.forensic.v1 lifecycle.dead_declared",
73222
+ " event per finding. Conservative predicate: all of {active status,",
73223
+ " pid set, ESRCH dead pid, age > 60s} must hold.",
73224
+ " --dry-run Pair with --reap-dead-jobs: list findings without mutation.",
73225
+ " --json Emit JSON envelope { dryRun, found:[{job_id, pid, reason,",
73226
+ " age_ms}], cancelled }.",
72240
73227
  "",
72241
73228
  "Examples:",
72242
73229
  " specialists doctor",
72243
73230
  " specialists doctor orphans",
73231
+ " specialists doctor --pr-drift",
73232
+ " specialists doctor --reap-dead-jobs --dry-run --json",
72244
73233
  ""
72245
73234
  ].join(`
72246
73235
  `));