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