@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.
- package/config/mandatory-rules/code-quality-defaults.md +58 -1
- package/config/mandatory-rules/executor-delivery.md +4 -0
- package/config/mandatory-rules/test-runner-execution-scope.md +4 -0
- package/config/skills/setup-specialists/SKILL.md +1 -1
- package/config/skills/using-script-specialists/SKILL.md +5 -5
- package/config/skills/using-specialists/SKILL.md +1135 -879
- package/config/skills/using-specialists-auto/SKILL.md +21 -21
- package/config/specialists/chain-coordinator.specialist.json +63 -0
- package/config/specialists/reviewer.specialist.json +1 -1
- package/config/specialists/seconder.specialist.json +1 -1
- package/dist/asset-contract.json +8 -13
- package/dist/index.js +1070 -81
- package/dist/lib.js +185 -9
- package/dist/types/cli/clean.d.ts.map +1 -1
- 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/init.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/cli/view.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/control.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/loader.d.ts +12 -0
- package/dist/types/specialist/loader.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/process-health.d.ts +16 -1
- package/dist/types/specialist/process-health.d.ts.map +1 -1
- 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/chain-templates/README.md +1 -1
- package/docs/design/roadmap/chains-prompt-evals.md +3054 -0
- package/docs/design/roadmap/specialists-roadmap.md +75 -7
- package/docs/design/xt-pi-role-pi-flag-passthrough.md +174 -0
- package/docs/skills.md +4 -13
- package/package.json +2 -2
- package/config/skills/using-specialists-v2/SKILL.md +0 -766
- 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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
28179
|
+
output: reportedOutput
|
|
27985
28180
|
});
|
|
27986
28181
|
try {
|
|
27987
|
-
this.withSqliteOperation("upsertResult:initial_turn", (client) => client.upsertResult(id,
|
|
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
|
-
}),
|
|
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
|
|
28926
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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>] [--
|
|
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
|
|
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 --
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
" --
|
|
71563
|
-
"
|
|
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
|
`));
|